PHP 8.0 — Attributes (#[Route], #[Inject], etc.)
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
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']]