Lesson 100 of 100
Reflection15 min5 checksPHP 8.5

Read Metadata with PHP Attributes

Learn how to declare custom PHP attributes, attach them to classes, properties and methods, and read them back with Reflection to build deterministic data.

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 name
  • getArguments() for the raw arguments, with positional ones as list items and named ones as string keys
  • newInstance() 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.

Example
<?php
#[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";
Output
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.

Example
<?php
#[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";
Output
{"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.

Example
<?php
#[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";
}
Output
OnlyMethod
Error caught

Common mistakes

  • Expecting an attribute to run code by itself. Something must read it.
  • Forgetting #[Attribute] on the attribute class, which makes newInstance() fail.
  • Comparing against a short name instead of using Route::class.
  • Assuming getAttributes() validates targets. Validation happens in newInstance().
  • 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.

Before you start

Functions · Arrays · Types

You’ll practice
  • Use local custom attributes and Reflection to return JSON data
  • Implement and test a deterministic PHP 8.4 function
Your task

Make list_routes() pass all 5 checks. Run your code as often as you like, then check your answer.

exercise.phpSaved in this browserPHP 8.5
CONSOLE & CHECKS● Ready

Ready when you are

Your output will appear here.
Make it work. Then make it pass.

PHP loads on your first run. Each check starts fresh. Your code stays in this browser; clearing site data removes your progress. A check or the task looks wrong? Report a problem with this exercise.