advanced php language
Attributes
Attributes are structured metadata attached to PHP code. They let classes, methods, properties, parameters, functions, and constants carry information that tools, frameworks, or your own code can read with reflection.
Attributes do not run by themselves. They are data attached to code. Something else must read them and decide what they mean.
You will see attributes in routing, validation, ORM mapping, dependency injection, test configuration, serialization, security, event listeners, and framework integrations.
A Simple Attribute
An attribute is a normal PHP class marked with PHP's built-in Attribute attribute.
<?php
declare(strict_types=1);
#[Attribute(Attribute::TARGET_METHOD)]
final readonly class Route
{
public function __construct(
public string $method,
public string $path,
) {
}
}
final class UserController
{
#[Route('GET', '/users')]
public function index(): string
{
return 'User list';
}
}
Route is metadata on the index() method. By itself, it does not register a route. A router or scanner must inspect the method and read the attribute.
Reading Attributes With Reflection
Reflection lets code inspect classes and methods at runtime.
<?php
declare(strict_types=1);
#[Attribute(Attribute::TARGET_METHOD)]
final readonly class Route
{
public function __construct(
public string $method,
public string $path,
) {
}
}
final class UserController
{
#[Route('GET', '/users')]
public function index(): string
{
return 'User list';
}
}
$method = new ReflectionMethod(UserController::class, 'index');
$attributes = $method->getAttributes(Route::class);
foreach ($attributes as $attribute) {
$route = $attribute->newInstance();
echo $route->method . ' ' . $route->path . PHP_EOL;
}
// Prints:
// GET /users
getAttributes() returns reflection objects. newInstance() creates the actual attribute object using the arguments written in the attribute.
Attribute Targets
Attributes can declare where they are allowed.
<?php
declare(strict_types=1);
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
final readonly class RequiresRole
{
public function __construct(
public string $role,
) {
}
}
#[RequiresRole('admin')]
final class AdminController
{
#[RequiresRole('editor')]
public function publish(): void
{
}
}
Targets help catch mistakes. A route attribute should probably apply to methods, not random properties.
Repeatable Attributes
Some metadata may appear more than once. Use Attribute::IS_REPEATABLE.
<?php
declare(strict_types=1);
#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
final readonly class Tag
{
public function __construct(
public string $name,
) {
}
}
#[Tag('billing')]
#[Tag('reports')]
final class InvoiceReport
{
}
$class = new ReflectionClass(InvoiceReport::class);
foreach ($class->getAttributes(Tag::class) as $attribute) {
echo $attribute->newInstance()->name . PHP_EOL;
}
// Prints:
// billing
// reports
Use repeatable attributes only when multiple entries make sense.
Attributes Versus Comments
Before attributes, PHP projects often used docblock annotations:
/**
* @Route("GET", "/users")
*/
Attributes are better for structured metadata because PHP parses them as code. Attribute classes can have constructors, typed arguments, target restrictions, and reflection support.
Comments are still useful for explanations and static analysis annotations, but they should not be the main mechanism for runtime metadata when attributes fit the problem.
Where Attributes Belong
Attributes are good when metadata belongs directly beside the code it describes.
Examples:
- route path beside a controller method
- validation rule beside a DTO property
- ORM column mapping beside an entity property
- test case metadata beside a test method
- listener metadata beside an event handler
Attributes are less useful when the metadata changes by environment, tenant, database row, or admin setting. Those values usually belong in configuration or data storage.
Common Mistakes
A common mistake is expecting attributes to do something automatically. They only matter if a framework, library, or your own reflection code reads them.
Another mistake is putting business logic inside attribute classes. Attribute constructors should usually store metadata, not query databases or call services.
Also avoid scattering too much behaviour across attributes. If a class has many attributes controlling unrelated systems, it can become hard to understand what happens at runtime.
What You Should Be Able To Do
After this lesson, you should be able to define a custom attribute, apply it to a class or method, read it with reflection, understand targets and repeatable attributes, and decide when attributes are better than config or comments.
For junior work, this matters because modern PHP frameworks use attributes heavily. You need to know that attributes are metadata and that runtime behaviour comes from the code that reads them.
Practice
Practice: Read Route Attributes
Create a small PHP example that defines and reads a route attribute.
Task
Build:
- a
Routeattribute for methods - a controller method with a route attribute
- reflection code that reads the attribute and prints the HTTP method and path
Use strict types. Keep the expected output in the PHP code block as printed lines or comments.
Check Your Work
Confirm:
- the attribute is limited to methods
- the controller method has route metadata
- reflection reads the metadata
- the attribute does not register a route by itself
Afterward, explain why attributes are metadata rather than behaviour.
Show solution
This solution defines route metadata and reads it with reflection.
<?php
declare(strict_types=1);
#[Attribute(Attribute::TARGET_METHOD)]
final readonly class Route
{
public function __construct(
public string $method,
public string $path,
) {
}
}
final class UserController
{
#[Route('GET', '/users')]
public function index(): string
{
return 'User list';
}
}
$method = new ReflectionMethod(UserController::class, 'index');
$attributes = $method->getAttributes(Route::class);
foreach ($attributes as $attribute) {
$route = $attribute->newInstance();
echo $route->method . ' ' . $route->path . PHP_EOL;
}
// Prints:
// GET /users
The attribute is metadata because it only describes the method. A router, scanner, or other runtime code must read that metadata before it changes application behaviour.