0

PHP 8.0 — Attributes (#[Route], #[Inject], etc.)

Advanced5 min read·php-09-006

Concept

PHP 8.0 Attributes were covered in depth in the OOP section (php-08-014). This lesson focuses on the ecosystem of attribute-driven frameworks and practical patterns for defining and reading attributes in real applications.

Attribute-driven routing: Symfony 6+ and Laravel (via third-party packages) support #[Route] attributes on controller methods. At compile/boot time, the framework reflects all controller classes, reads route attributes, and builds the routing table.

Attribute-driven validation: Symfony's Validator component uses attributes like #[Assert\NotBlank], #[Assert\Email]. The validator reflects the object's properties, reads their constraint attributes, and runs the validations.

Attribute-driven serialization: Tools like symfony/serializer use #[SerializedName('user_name')] to map PHP property names to JSON keys.

Creating reusable attribute libraries: When building a package or application framework, define attribute classes with clear semantics. Use \Attribute::IS_REPEATABLE when multiple instances are valid on the same target (multiple #[Middleware] on one method). Use \Attribute::TARGET_* constants to restrict where your attribute can be applied.

Performance: Reading attributes via Reflection is slow. Attribute-heavy frameworks cache the parsed attribute data — typically as compiled PHP arrays in the OPcache. Never read attributes on every request in production; always cache the result.

Code Example

php
<?php
declare(strict_types=1);

// ===== Validation attribute system =====
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::IS_REPEATABLE)]
class Validate
{
    public function __construct(
        public readonly string $rule,
        public readonly string $message = '',
    ) {}
}

class CreatePostRequest
{
    #[Validate('required', 'Title is required')]
    #[Validate('max:200', 'Title must be under 200 characters')]
    public string $title = '';

    #[Validate('required', 'Body is required')]
    #[Validate('min:50', 'Body must be at least 50 characters')]
    public string $body = '';

    #[Validate('url', 'Must be a valid URL')]
    public string $imageUrl = '';
}

// Attribute reader / validator
class AttributeValidator
{
    public function validate(object $dto): array
    {
        $errors = [];
        $rc = new ReflectionClass($dto);

        foreach ($rc->getProperties() as $prop) {
            foreach ($prop->getAttributes(Validate::class) as $attr) {
                $validate = $attr->newInstance();
                $value    = $prop->getValue($dto);
                $error    = $this->applyRule($validate->rule, $value, $prop->getName());
                if ($error) {
                    $errors[$prop->getName()][] = $validate->message ?: $error;
                }
            }
        }
        return $errors;
    }

    private function applyRule(string $rule, mixed $value, string $field): ?string
    {
        if ($rule === 'required' && empty($value)) return "$field is required";
        if (str_starts_with($rule, 'max:')) {
            $max = (int) substr($rule, 4);
            if (strlen($value) > $max) return "$field exceeds $max chars";
        }
        if (str_starts_with($rule, 'min:')) {
            $min = (int) substr($rule, 4);
            if (strlen($value) < $min) return "$field must be at least $min chars";
        }
        return null;
    }
}

$dto = new CreatePostRequest();
$dto->title = 'Hi'; // too short: no min rule, but would fail max:200? no — it's fine
$dto->body  = ''; // will fail required

$validator = new AttributeValidator();
$errors    = $validator->validate($dto);
print_r($errors); // ['body' => ['Body is required']]