Explore the inner workings of attributes, readonly, first-class callables and PHP 8's strict typing.
Open this lesson in KodokonAttributes replace docblock annotations with real classes, validated by the engine. An attribute is nothing but dormant metadata: writing #[Route('/users')] creates no object. It only comes to life when some code queries it through reflection and calls newInstance() - which is exactly what frameworks do at startup to build their routing tables. The allowed target (TARGET_METHOD, TARGET_CLASS...) is itself declared by an attribute on the attribute class.
<?php
declare(strict_types=1);
#[Attribute(Attribute::TARGET_METHOD)]
final class Route
{
public function __construct(
public readonly string $path,
public readonly string $method = 'GET'
) {}
}
final class UserController
{
#[Route('/users', method: 'GET')]
public function index(): array
{
return ['users' => []];
}
}
$ref = new ReflectionMethod(
UserController::class,
'index'
);
foreach ($ref->getAttributes(Route::class) as $attr) {
$route = $attr->newInstance();
echo $route->method, ' ', $route->path, PHP_EOL;
}readonly - properties in 8.1, whole classes in 8.2 - guarantees that a property is written only once, from within the class's scope. It is the tool for value objects: any "modification" becomes the construction of a new object, which eliminates an entire family of shared-state bugs. A readonly class requires all of its properties to be typed and non-static.
<?php
declare(strict_types=1);
final readonly class Money
{
public function __construct(
public int $amount,
public string $currency
) {}
public function add(Money $other): self
{
if ($other->currency !== $this->currency) {
throw new InvalidArgumentException(
'Currency mismatch'
);
}
return new self(
$this->amount + $other->amount,
$this->currency
);
}
}
$total = new Money(500, 'EUR');
$total = $total->add(new Money(250, 'EUR'));
echo $total->amount;The first-class callable syntax strtoupper(...) (PHP 8.1) creates a Closure from any function or method. Unlike strings such as 'strtoupper' or arrays like [$obj, 'method'], the symbol is verified at creation time and static analysis tools follow the types end to end. For an instance method, the closure captures the object; for a static method, only the class.
<?php
declare(strict_types=1);
$words = ['php', 'rust', 'go'];
$upper = strtoupper(...);
var_dump($upper('php'));
print_r(array_map(strtoupper(...), $words));
final class Mailer
{
public function send(string $to): bool
{
return $to !== '';
}
public static function ping(): string
{
return 'pong';
}
}
$mailer = new Mailer();
$send = $mailer->send(...);
$ping = Mailer::ping(...);
var_dump($send('dev@example.org'), $ping());