100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
PHP

Attributes in PHP

Attributes are PHP's native, structured metadata syntax (introduced in PHP 8.0) that let you annotate classes, methods, and properties with machine-readable data read via reflection.

Modern PHP FeaturesIntermediate9 min readJul 9, 2026
Analogies

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

A 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_REPEATABLE allows 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

Was this page helpful?

Topics covered

#PHP#PHPProgrammingStudyNotes#Programming#AttributesInPHP#Attributes#Declaring#Applying#Attribute#StudyNotes#SkillVeris