Attributes (PHP 8.0+) — #[Attribute], reading with Reflection
Concept
Attributes (PHP 8.0) provide a structured, machine-readable way to attach metadata to classes, methods, properties, parameters, and function. They replace the PHPDoc annotation approach used by Doctrine and Symfony, providing first-class language support for what was previously convention-based text parsing.
Syntax: #[AttributeName(arg1, arg2)] before the target element. Multiple attributes can be applied: #[Route('/users'), Auth]. Attributes can be applied to: classes, functions, methods, properties, class constants, parameters, and function/method return types.
Defining an attribute: A regular PHP class with #[\Attribute] applied to it. The \Attribute constructor accepts flags controlling where it can be applied: \Attribute::TARGET_CLASS, \Attribute::TARGET_METHOD, etc.
Reading attributes: Via the Reflection API. $reflector->getAttributes() returns an array of ReflectionAttribute objects. $attr->newInstance() instantiates the attribute class with its constructor arguments — this is where validation can occur.
Comparison with PHPDoc annotations: PHPDoc annotations (@Route, @ORM\Column) are strings parsed at runtime by the library. They're not validated by the PHP engine, have no autocompletion beyond what the IDE infers, and can't be statically analyzed. PHP Attributes are proper PHP code — validated by the type system, autocompletable, and statically analyzable. Doctrine ORM 3.x, Symfony 6, and Laravel support attributes as first-class metadata.
Code Example
<?php
declare(strict_types=1);
// Define a Route attribute
#[\Attribute(\Attribute::TARGET_METHOD)]
class Route
{
public function __construct(
public readonly string $path,
public readonly string $method = 'GET',
) {}
}
// Define a Middleware attribute
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
class Middleware
{
public function __construct(public readonly string $name) {}
}
// Use on a controller
#[Middleware('auth')]
class UserController
{
#[Route('/users', 'GET')]
#[Middleware('throttle:60,1')]
public function index(): array { return []; }
#[Route('/users/{id}', 'GET')]
public function show(int $id): array { return []; }
#[Route('/users', 'POST')]
#[Middleware('verified')]
public function store(): array { return []; }
}
// Read attributes to build a route table
function extractRoutes(string $controllerClass): array
{
$rc = new ReflectionClass($controllerClass);
$routes = [];
foreach ($rc->getMethods() as $method) {
foreach ($method->getAttributes(Route::class) as $attr) {
$route = $attr->newInstance(); // Route($path, $method)
$routes[] = [
'path' => $route->path,
'http' => $route->method,
'handler' => [$controllerClass, $method->getName()],
];
}
}
return $routes;
}
$routes = extractRoutes(UserController::class);
// [
// ['path' => '/users', 'http' => 'GET', 'handler' => ['UserController', 'index']],
// ['path' => '/users/{id}', 'http' => 'GET', 'handler' => ['UserController', 'show']],
// ['path' => '/users', 'http' => 'POST', 'handler' => ['UserController', 'store']],
// ]
// Attribute for validation (like Symfony Constraints)
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class NotBlank
{
public function __construct(public readonly string $message = 'This field cannot be blank.') {}
}
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class Length
{
public function __construct(public readonly int $min, public readonly int $max) {}
}
class CreateUserDto
{
#[NotBlank]
#[Length(min: 2, max: 50)]
public string $name = '';
#[NotBlank]
public string $email = '';
}