Attributes / Annotations (PHP 8.0)
debt(d5/e3/b3/t5)
Closest to 'specialist tool catches' (d5), Rector and PHPStan (per detection_hints.tools) can flag missing #[Attribute] on attribute classes, missing target flags, and docblock annotations that should be migrated — but a default editor/linter won't catch misuse.
Closest to 'simple parameterised fix' (e3), quick_fix is replacing @annotation docblocks with #[Attribute] syntax — a mechanical pattern swap, often Rector-automatable, but typically touches multiple annotated classes rather than a single line.
Closest to 'localised tax' (b3), attributes apply within the metadata layer (routing, ORM, validation) of a PHP 8+ codebase; they are a localised idiom rather than a system-shaping choice, though they touch multiple contexts (web/cli/queue).
Closest to 'notable trap' (t5), misconception field states devs assume attributes have runtime cost like docblock annotations — they're actually lazy via Reflection with zero cost unless read. Plus the common mistake of forgetting #[Attribute] on the attribute class itself is a documented gotcha.
Also Known As
TL;DR
Explanation
PHP 8.0 attributes replace docblock-based annotations (used by PHPDoc, Doctrine, Symfony) with a first-class language feature. Attributes are declared with #[Attribute] on a class and applied with #[MyAttribute(args)] to target declarations. They are read at runtime via the Reflection API, making them suitable for routing, validation, serialisation, ORM mappings, and dependency injection metadata. Unlike docblocks, attributes are parsed by PHP itself, so syntax errors are caught at compile time.
Common Misconception
Why It Matters
Common Mistakes
- Defining an Attribute class without #[Attribute] itself — the class is not recognised as an attribute.
- Not specifying the Attribute target flags — an attribute intended only for methods can accidentally be applied to classes.
- Accessing attribute data with manual regex parsing of docblocks instead of using ReflectionClass->getAttributes().
- Using attributes for runtime behaviour without understanding they require Reflection to read — they add no runtime behaviour themselves.
Code Examples
// Attribute class missing the #[Attribute] marker:
class Route { // Not recognised as an attribute
public function __construct(public string $path) {}
}
#[Route('/users')] // Silent no-op
class UserController {}
// Define a custom attribute
#[Attribute(Attribute::TARGET_METHOD)]
class Route {
public function __construct(
public readonly string $path,
public readonly string $method = 'GET',
) {}
}
// Apply it
class UserController {
#[Route('/users', 'GET')]
public function index(): array { return []; }
#[Route('/users', 'POST')]
public function store(array $data): User { ... }
}
// Read it via Reflection
$ref = new ReflectionClass(UserController::class);
foreach ($ref->getMethods() as $method) {
$attrs = $method->getAttributes(Route::class);
foreach ($attrs as $attr) {
$route = $attr->newInstance(); // Route object
echo "{$route->method} {$route->path}\n";
}
}