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 example
<?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 example
<?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 example
<?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 example
<?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:

PHP example
/**
 * @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 Route attribute 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 example
<?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.