PHP Attributes
Use native attributes for metadata instead of docblock annotations (PHP 8.0+).
Bad Example
<?php
declare(strict_types=1);
// Using docblock annotations - parsed as strings, no type safety
class UserController
{
/**
* @Route("/users/{id}", methods={"GET"})
* @Cache(maxage=3600)
* @Security("is_granted('VIEW', user)")
*/
public function show(int $id)
{
// Annotations are just comments - no IDE support for validation
}
}
// Validation using docblocks
class CreateUserRequest
{
/**
* @Assert\NotBlank()
* @Assert\Email()
*/
public string $email;
/**
* @Assert\NotBlank()
* @Assert\Length(min=8)
*/
public string $password;
}Good Example
<?php
declare(strict_types=1);
// Define custom attributes
#[Attribute(Attribute::TARGET_METHOD)]
class Route
{
public function __construct(
public string $path,
public array $methods = ['GET'],
public ?string $name = null,
) {}
}
#[Attribute(Attribute::TARGET_METHOD)]
class Cache
{
public function __construct(
public int $maxAge = 0,
public bool $public = true,
) {}
}
#[Attribute(Attribute::TARGET_PROPERTY)]
class Validate
{
public function __construct(
public array $rules = [],
) {}
}
#[Attribute(Attribute::TARGET_CLASS)]
class Entity
{
public function __construct(
public string $table,
) {}
}
#[Attribute(Attribute::TARGET_PROPERTY)]
class Column
{
public function __construct(
public string $name,
public string $type = 'string',
public bool $nullable = false,
) {}
}
// Using attributes on a controller
class UserController
{
#[Route('/users/{id}', methods: ['GET'], name: 'user.show')]
#[Cache(maxAge: 3600)]
public function show(int $id): Response
{
return new Response($this->userService->find($id));
}
#[Route('/users', methods: ['POST'], name: 'user.create')]
public function create(CreateUserRequest $request): Response
{
return new Response($this->userService->create($request));
}
}
// Using attributes for validation
class CreateUserRequest
{
#[Validate(rules: ['required', 'email', 'unique:users'])]
public string $email;
#[Validate(rules: ['required', 'min:8', 'confirmed'])]
public string $password;
#[Validate(rules: ['required', 'string', 'max:255'])]
public string $name;
}
// Entity with ORM attributes
#[Entity(table: 'users')]
class User
{
#[Column(name: 'id', type: 'integer')]
public int $id;
#[Column(name: 'email', type: 'string')]
public string $email;
#[Column(name: 'created_at', type: 'datetime', nullable: true)]
public ?DateTimeImmutable $createdAt;
}
// Reading attributes via reflection
function getRoutes(object $controller): array
{
$routes = [];
$reflection = new ReflectionClass($controller);
foreach ($reflection->getMethods() as $method) {
$attributes = $method->getAttributes(Route::class);
foreach ($attributes as $attribute) {
$route = $attribute->newInstance();
$routes[] = [
'path' => $route->path,
'methods' => $route->methods,
'handler' => [$controller, $method->getName()],
];
}
}
return $routes;
}Why
- Type Safety: Attributes are real classes with typed constructors
- IDE Support: Full autocompletion, refactoring, and navigation
- Validation: Invalid attribute usage caught by static analysis tools
- Native Feature: Built into PHP, no external parser needed
- Performance: Faster than parsing docblocks at runtime
- Named Arguments: Clear parameter names in attribute usage