Table Of Contents
Problem
You need to add metadata to code elements for frameworks, validation, caching, or documentation, but docblock comments are not structured or type-safe.
Solution
// Basic attribute definition
#[Attribute]
class Route {
public function __construct(
public string $path,
public string $method = 'GET',
public array $middleware = []
) {}
}
// Using attributes on methods
class UserController {
#[Route('/users', 'GET')]
public function index(): array {
return ['users' => []];
}
#[Route('/users/{id}', 'GET')]
public function show(int $id): array {
return ['user' => ['id' => $id]];
}
#[Route('/users', 'POST', ['auth', 'validation'])]
public function store(): array {
return ['created' => true];
}
}
// Reading attributes with reflection
function getRoutes(string $controllerClass): array {
$reflection = new ReflectionClass($controllerClass);
$routes = [];
foreach ($reflection->getMethods() as $method) {
$attributes = $method->getAttributes(Route::class);
foreach ($attributes as $attribute) {
$route = $attribute->newInstance();
$routes[] = [
'path' => $route->path,
'method' => $route->method,
'controller' => $controllerClass,
'action' => $method->getName(),
'middleware' => $route->middleware
];
}
}
return $routes;
}
$routes = getRoutes(UserController::class);
// Validation attributes
#[Attribute(Attribute::TARGET_PROPERTY)]
class Required {
public function __construct(public string $message = 'This field is required') {}
}
#[Attribute(Attribute::TARGET_PROPERTY)]
class Email {
public function __construct(public string $message = 'Invalid email format') {}
}
#[Attribute(Attribute::TARGET_PROPERTY)]
class MinLength {
public function __construct(
public int $length,
public string $message = 'Too short'
) {}
}
#[Attribute(Attribute::TARGET_PROPERTY)]
class MaxLength {
public function __construct(
public int $length,
public string $message = 'Too long'
) {}
}
// Model with validation attributes
class User {
#[Required]
#[MinLength(2, 'Name must be at least 2 characters')]
#[MaxLength(50, 'Name cannot exceed 50 characters')]
public string $name;
#[Required]
#[Email]
public string $email;
#[MinLength(8, 'Password must be at least 8 characters')]
public string $password;
public ?int $age = null;
}
// Validator using attributes
class AttributeValidator {
public function validate(object $object): array {
$reflection = new ReflectionClass($object);
$errors = [];
foreach ($reflection->getProperties() as $property) {
$property->setAccessible(true);
$value = $property->getValue($object);
// Check Required attribute
$requiredAttrs = $property->getAttributes(Required::class);
if (!empty($requiredAttrs) && empty($value)) {
$required = $requiredAttrs[0]->newInstance();
$errors[$property->getName()][] = $required->message;
continue;
}
// Skip other validations if value is empty
if (empty($value)) {
continue;
}
// Check Email attribute
$emailAttrs = $property->getAttributes(Email::class);
if (!empty($emailAttrs) && !filter_var($value, FILTER_VALIDATE_EMAIL)) {
$email = $emailAttrs[0]->newInstance();
$errors[$property->getName()][] = $email->message;
}
// Check MinLength attribute
$minLengthAttrs = $property->getAttributes(MinLength::class);
if (!empty($minLengthAttrs) && strlen($value) < $minLengthAttrs[0]->newInstance()->length) {
$minLength = $minLengthAttrs[0]->newInstance();
$errors[$property->getName()][] = $minLength->message;
}
// Check MaxLength attribute
$maxLengthAttrs = $property->getAttributes(MaxLength::class);
if (!empty($maxLengthAttrs) && strlen($value) > $maxLengthAttrs[0]->newInstance()->length) {
$maxLength = $maxLengthAttrs[0]->newInstance();
$errors[$property->getName()][] = $maxLength->message;
}
}
return $errors;
}
}
$user = new User();
$user->name = 'J';
$user->email = 'invalid-email';
$user->password = '123';
$validator = new AttributeValidator();
$errors = $validator->validate($user);
// Caching attributes
#[Attribute(Attribute::TARGET_METHOD)]
class Cache {
public function __construct(
public int $ttl = 3600,
public string $key = '',
public array $tags = []
) {}
}
#[Attribute(Attribute::TARGET_METHOD)]
class ClearCache {
public function __construct(public array $tags = []) {}
}
class ProductService {
#[Cache(ttl: 1800, tags: ['products'])]
public function getProducts(): array {
// Expensive database operation
return ['products' => []];
}
#[Cache(key: 'product_{id}', ttl: 3600)]
public function getProduct(int $id): array {
return ['product' => ['id' => $id]];
}
#[ClearCache(tags: ['products'])]
public function createProduct(array $data): array {
// Create product and clear cache
return ['created' => true];
}
}
// API documentation attributes
#[Attribute(Attribute::TARGET_METHOD)]
class ApiEndpoint {
public function __construct(
public string $summary,
public string $description = '',
public array $parameters = [],
public array $responses = []
) {}
}
#[Attribute(Attribute::TARGET_PARAMETER)]
class ApiParameter {
public function __construct(
public string $description,
public string $type = 'string',
public bool $required = false
) {}
}
class ApiController {
#[ApiEndpoint(
summary: 'Get user by ID',
description: 'Retrieves a single user by their unique identifier',
responses: ['200' => 'User found', '404' => 'User not found']
)]
public function getUser(
#[ApiParameter('User ID', 'integer', true)] int $id
): array {
return ['user' => ['id' => $id]];
}
}
// ORM attributes
#[Attribute(Attribute::TARGET_CLASS)]
class Table {
public function __construct(public string $name) {}
}
#[Attribute(Attribute::TARGET_PROPERTY)]
class Column {
public function __construct(
public string $name = '',
public string $type = 'string',
public bool $nullable = false,
public bool $unique = false
) {}
}
#[Attribute(Attribute::TARGET_PROPERTY)]
class PrimaryKey {}
#[Attribute(Attribute::TARGET_PROPERTY)]
class ForeignKey {
public function __construct(
public string $table,
public string $column = 'id'
) {}
}
#[Table('users')]
class UserEntity {
#[PrimaryKey]
#[Column('id', 'integer')]
public int $id;
#[Column('username', 'string', unique: true)]
public string $username;
#[Column('email', 'string', unique: true)]
public string $email;
#[Column('profile_id', 'integer', nullable: true)]
#[ForeignKey('profiles')]
public ?int $profileId = null;
}
// Event handling attributes
#[Attribute(Attribute::TARGET_METHOD)]
class EventListener {
public function __construct(
public string $event,
public int $priority = 0
) {}
}
class UserEventHandler {
#[EventListener('user.created', 10)]
public function onUserCreated(array $userData): void {
// Send welcome email
echo "Welcome email sent to " . $userData['email'] . "\n";
}
#[EventListener('user.created', 5)]
public function onUserCreatedAnalytics(array $userData): void {
// Track user registration
echo "User registration tracked\n";
}
#[EventListener('user.deleted')]
public function onUserDeleted(array $userData): void {
// Cleanup user data
echo "User data cleaned up\n";
}
}
// Authorization attributes
#[Attribute(Attribute::TARGET_METHOD)]
class RequirePermission {
public function __construct(public string $permission) {}
}
#[Attribute(Attribute::TARGET_METHOD)]
class RequireRole {
public function __construct(public string $role) {}
}
class AdminController {
#[RequireRole('admin')]
#[RequirePermission('users.manage')]
public function manageUsers(): array {
return ['users' => []];
}
#[RequirePermission('system.configure')]
public function systemSettings(): array {
return ['settings' => []];
}
}
// Testing attributes
#[Attribute(Attribute::TARGET_METHOD)]
class Test {
public function __construct(
public string $description = '',
public array $dataProvider = []
) {}
}
#[Attribute(Attribute::TARGET_METHOD)]
class DataProvider {}
class UserTest {
#[DataProvider]
public function userDataProvider(): array {
return [
['John', 'john@example.com'],
['Jane', 'jane@example.com']
];
}
#[Test(description: 'Test user creation', dataProvider: ['userDataProvider'])]
public function testUserCreation(string $name, string $email): void {
$user = new User();
$user->name = $name;
$user->email = $email;
// Test assertions
assert($user->name === $name);
assert($user->email === $email);
}
}
// Multiple attributes on single element
#[Route('/api/users/{id}', 'PUT')]
#[RequireRole('admin')]
#[Cache(ttl: 0)]
#[ApiEndpoint(summary: 'Update user')]
function updateUser(int $id, array $data): array {
return ['updated' => true];
}
// Attribute inheritance and targeting
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
class Deprecated {
public function __construct(
public string $reason = '',
public string $alternative = ''
) {}
}
#[Deprecated(reason: 'Use UserEntity instead', alternative: 'UserEntity')]
class OldUser {
#[Deprecated(reason: 'Use getFullName() instead')]
public function getName(): string {
return 'Old method';
}
}
// Utility function to get all attributes
function getAllAttributes(ReflectionClass|ReflectionMethod|ReflectionProperty $reflection): array {
$attributes = [];
foreach ($reflection->getAttributes() as $attribute) {
$attributes[] = [
'name' => $attribute->getName(),
'arguments' => $attribute->getArguments(),
'instance' => $attribute->newInstance()
];
}
return $attributes;
}
Explanation
Attributes provide structured metadata that can be read via reflection. Use #[AttributeName]
syntax to apply them to classes, methods, properties, or parameters.
Define attributes with the #[Attribute]
attribute and specify valid targets. Use reflection to read attributes at runtime and build functionality around the metadata they provide.
Share this article
Add Comment
No comments yet. Be the first to comment!