01 · THE IDEAREAD → TRY → LEARN
What an attribute is
An attribute is structured metadata written directly in front of a declaration. It does nothing on its own. Your code, or a framework, decides what the metadata means by reading it through the Reflection API. This lets you describe a method or property where it is declared instead of keeping a separate configuration array that can drift out of date.
The syntax is #[Name] or #[Name(arguments)]. The name refers to an ordinary class. That class is marked as usable for attributes by putting #[Attribute] on it. Attributes can be placed on classes, functions, methods, properties, class constants and parameters.
Declaring and reading one
The first step is to declare the attribute class and attach it. The constructor of the attribute class receives the arguments you write at the usage site. Reflection objects such as ReflectionClass, ReflectionMethod and ReflectionProperty all provide getAttributes(). It returns a list of ReflectionAttribute objects, and each one offers:
getName()for the attribute class namegetArguments()for the raw arguments, with positional ones as list items and named ones as string keysnewInstance()to build the attribute object by calling its constructor
Important: getAttributes() does not construct anything or check that the class exists. Only newInstance() does that.
#[Attribute]
class Label
{
public function __construct(public string $text) {}
}
#[Label('Invoice')]
class Document {}
$class = new ReflectionClass(Document::class);
$attributes = $class->getAttributes(Label::class);
echo count($attributes), "\n";
echo $attributes[0]->getName(), "\n";
echo json_encode($attributes[0]->getArguments(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
echo $attributes[0]->newInstance()->text, "\n";1 Label ["Invoice"] Invoice
Worked example: collecting rules from properties
A common pattern is to scan a class and turn its attributes into plain data. The example below walks the properties of a class, reads a MaxLength rule where present, and builds an array keyed by property name. Properties without the attribute are simply skipped because getAttributes() returns an empty list for them. The attribute also uses a named argument, which works exactly like a named argument in a function call.
#[Attribute(Attribute::TARGET_PROPERTY)]
class MaxLength
{
public function __construct(public int $limit) {}
}
class Profile
{
#[MaxLength(limit: 12)]
public string $name = '';
public string $bio = '';
}
$rules = [];
foreach ((new ReflectionClass(Profile::class))->getProperties() as $property) {
foreach ($property->getAttributes(MaxLength::class) as $attribute) {
$rules[$property->getName()] = $attribute->newInstance()->limit;
}
}
echo json_encode($rules, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";{"name":12}
Targets and repeating
The flags you pass to #[Attribute(...)] restrict where the attribute may appear. Attribute::TARGET_METHOD allows methods only, and you can combine flags with |. Add Attribute::IS_REPEATABLE if the same attribute may appear more than once on one declaration. These rules are enforced when you call newInstance(), not when the file is compiled. A wrong target or an unauthorized repeat throws an Error.
#[Attribute(Attribute::TARGET_METHOD)]
class OnlyMethod {}
#[OnlyMethod]
class Wrong {}
$attribute = (new ReflectionClass(Wrong::class))->getAttributes()[0];
echo $attribute->getName(), "\n";
try {
$attribute->newInstance();
} catch (Error $e) {
echo "Error caught\n";
}OnlyMethod Error caught
Common mistakes
- Expecting an attribute to run code by itself. Something must read it.
- Forgetting
#[Attribute]on the attribute class, which makesnewInstance()fail. - Comparing against a short name instead of using
Route::class. - Assuming
getAttributes()validates targets. Validation happens innewInstance(). - Passing non-constant expressions as attribute arguments. Arguments must be constant expressions.
A filter given to getAttributes() matches the exact class name by default. Passing ReflectionAttribute::IS_INSTANCEOF as the second argument also accepts subclasses.
Exercise contract
The starter file defines a repeatable Route attribute with path and method (default 'GET'), and a UserController whose methods carry Route attributes. One method has two routes and one has none. Implement list_routes(string $httpMethod): array. It must use Reflection on UserController, read every Route attribute on every method in declaration order, and return the list of paths whose HTTP method equals the argument. The comparison ignores case, so 'post' behaves like 'POST'. An unknown or empty method returns an empty array. Tests compare the returned JSON arrays, so keep the paths in the order they are found.
Recap
Attributes attach metadata to declarations. getAttributes() lists them, getArguments() exposes raw arguments, and newInstance() builds a validated object. Convert what you read into plain arrays and your results stay easy to test.
Functions · Arrays · Types
- Use local custom attributes and Reflection to return JSON data
- Implement and test a deterministic PHP 8.4 function