Attributes in PHP
Before PHP 8.0, frameworks that needed to attach metadata to classes and methods — routing paths, validation rules, ORM column mappings, dependency-injection hints — relied on parsing structured comments written inside PHPDoc blocks, such as @Route("/users"). This worked, but PHPDoc annotations are just comments to the PHP engine: they have no syntax validation, no autocompletion support without extra tooling, and require a separate annotation-parsing library to extract at runtime. PHP 8.0 introduced attributes, a first-class language construct using #[...] syntax that lets you attach structured, typed metadata directly to classes, methods, properties, parameters, and constants, which the engine parses and validates as real PHP syntax and which is retrievable at runtime through the standard Reflection API.
Cricket analogy: Before DRS technology, umpires relied on notes and gut-feel comments scribbled in a scorebook to flag no-balls; DRS made the call a validated, machine-checked signal, just as PHP 8 attributes replaced loosely-parsed PHPDoc comments like @Route with engine-validated syntax.
Declaring and applying an attribute
An attribute is just a normal PHP class, marked with the built-in #[Attribute] attribute on its own declaration to tell PHP it's meant to be used as metadata. The attribute class's constructor defines what arguments the attribute accepts when applied, and those arguments support named arguments just like any other constructor call. Once declared, the attribute is attached to a target using #[AttributeName(args)] immediately above (or, for parameters, inline with) the class/method/property/parameter it annotates. The #[Attribute] declaration itself can restrict which kinds of targets are valid via flags like Attribute::TARGET_METHOD | Attribute::TARGET_CLASS, and whether the same attribute can repeat on one target via Attribute::IS_REPEATABLE.
Cricket analogy: Declaring an attribute class is like registering a player with the ICC: the constructor is the player's official stats sheet, its named arguments are fields like battingStyle, and Attribute::TARGET_METHOD restricts where that 'player' (metadata) can be fielded.
<?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 [];
}
#[Route('/users', method: 'POST')]
public function store(): array
{
return [];
}
}Reading attributes with Reflection
Attributes are inert by themselves — attaching #[Route(...)] to a method does nothing at runtime on its own. A framework (or your own bootstrap code) must use the Reflection API to discover them: ReflectionMethod::getAttributes(Route::class) returns an array of ReflectionAttribute objects matching that attribute class, and calling ->newInstance() on one of those instantiates the actual Route object with its constructor arguments filled in, ready to inspect. This is exactly how modern PHP frameworks implement declarative routing, ORM entity mapping, and validation without any custom docblock parser.
Cricket analogy: Attaching #[Route] without a framework reading it is like a bowler's field placement chart pinned in the dressing room that nobody consults; only when the captain (Reflection API) actively reads getAttributes() and calls newInstance() does the field actually get set.
<?php
declare(strict_types=1);
$reflection = new \ReflectionClass(UserController::class);
foreach ($reflection->getMethods() as $method) {
foreach ($method->getAttributes(Route::class) as $attribute) {
/** @var Route $route */
$route = $attribute->newInstance();
printf("%s %s -> %s::%s\n", $route->method, $route->path, $method->class, $method->name);
}
}
// GET /users -> UserController::index
// POST /users -> UserController::storeA helpful mental model: attributes are to metadata what type declarations are to types — both replace a convention living only in comments with a construct the PHP engine actually parses, validates, and exposes through a stable, official API (Reflection), rather than requiring a bespoke regex-based docblock parser shipped by every framework.
Attributes are parsed at compile time but have zero runtime effect unless something explicitly reads them via Reflection. A common beginner mistake is expecting #[Route('/users')] to 'just work' — the attribute is inert metadata; it is the framework's routing layer (or your own code) that must call getAttributes() and act on the result.
- Attributes use
#[AttributeName(args)]syntax to attach structured, native metadata to classes, methods, properties, parameters, and constants. - An attribute is defined as an ordinary PHP class marked with
#[\Attribute]on its own declaration. Attribute::TARGET_*flags restrict where an attribute may legally be applied;IS_REPEATABLEallows repeated use on the same target.- Attributes are inert on their own — they must be read via the Reflection API (
getAttributes()+newInstance()) to have any runtime effect. - Attributes replace the older convention of parsing metadata from PHPDoc comment annotations, gaining engine-level syntax validation.
- Common real-world uses include routing definitions, ORM entity/column mapping, and validation rule declarations.
Practice what you learned
1. What is the correct syntax to attach an attribute to a PHP method?
2. What must an attribute class itself be marked with to be usable as an attribute?
3. By default, what runtime effect does attaching #[Route('/users')] to a method have?
4. Which Reflection method call retrieves attribute instances of a specific class attached to a method?
5. What does the `Attribute::IS_REPEATABLE` flag control?
Was this page helpful?
You May Also Like
Enums in PHP
PHP 8.1 enums provide a first-class way to define a fixed set of named values, with optional scalar backing, methods, interfaces, and constants attached.
Type Declarations and Strict Types
PHP supports optional scalar and object type declarations on parameters, return values, and properties, with strict_types controlling how aggressively the engine coerces mismatched values.
Classes and Objects
Explore PHP's object-oriented foundations: declaring classes, constructing objects, typed properties, visibility, and constructor promotion.
Named Arguments and the Nullsafe Operator
Named arguments let callers pass function parameters by name in any order, and the nullsafe operator short-circuits chained property/method access safely when any link is null.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics