PHP 8 introduced attributes — a native way to attach structured metadata to classes, methods, properties, and parameters. Before them, frameworks encoded metadata in docblock comments (@Route, @ORM\Column) that PHP itself ignored and libraries parsed from strings. Attributes make that metadata a real language feature: structured, validated, and readable through reflection rather than by parsing comments. It is a genuine improvement in a specific set of situations.
Real metadata, not comments
An attribute is declared with the #[...] syntax and is an actual PHP construct the engine understands, not a comment. Because attributes are real, they are type-checked, IDE-aware, and cannot silently rot into invalid syntax the way a docblock annotation can. The metadata now has the same standing as the code it annotates:
#[Attribute]
class Route {
public function __construct(public string $path, public string $method = 'GET') {}
}
class UserController {
#[Route('/users', method: 'POST')]
public function store() { /* ... */ }
}
Read them via reflection
Attributes are inert on their own — something has to read them and act. That something uses PHP's reflection API to find the attributes on a class or method and instantiate them, turning the declared metadata into behaviour. A router scans controllers for Route attributes to build its routing table; an ORM reads mapping attributes to understand a schema. The attribute declares the intent; reflection-based code interprets it. Understanding this two-part model — declare, then read — is the key to both using and building with attributes.
Where they help
Attributes shine for declarative configuration that belongs next to the code it configures — routing, ORM mapping, validation rules, dependency-injection hints, event listeners. Placing the configuration inline with the method or property it governs keeps related things together and readable, which is why modern PHP frameworks have embraced them. They are not for everything: dynamic or environment-specific configuration still belongs in config files. Use attributes for static, code-adjacent metadata, not as a config system.
A common misconception is that adding an attribute makes something happen. It does not — an attribute is passive metadata until code reflects over it and acts. When an attribute seems to have no effect, the question is whether anything is actually reading it. This declare-then-read separation is the whole model, and forgetting it is the usual source of confusion.
PHP 8 attributes turn docblock annotations into first-class, structured metadata that the language understands and reflection reads — type-checked, IDE-aware, and living right next to the code it configures. Reach for them for declarative, static configuration like routing, mapping, and validation, remember that they are inert until something reflects over them, and leave dynamic configuration in config files where it belongs.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.