PHP Attributes have evolved from a curious addition in PHP 8.0 to a powerful metaprogramming tool that's reshaping how we write modern PHP applications. With PHP 8.3's enhancements, attributes now offer capabilities that go far beyond simple annotations, enabling elegant solutions for cross-cutting concerns, dependency injection, and declarative programming patterns.
Table Of Contents
- The Evolution of PHP Attributes
- Advanced Attribute Architecture
- Building an Attribute-Driven Framework
- Real-World Attribute Patterns
- Performance Optimization with Attributes
- Testing Attributes
- Advanced Patterns and Best Practices
- Conclusion
The Evolution of PHP Attributes
Remember the days of parsing docblock annotations? PHP Attributes eliminated that overhead, providing native support for metadata directly in the language. PHP 8.3 takes this further with improved performance, better tooling support, and new patterns that make attributes indispensable for modern PHP development.
Understanding attributes means understanding their true power: they're not just replacements for annotations – they're first-class citizens that can carry logic, validate data, and transform how your application behaves at runtime.
Advanced Attribute Architecture
Let's explore sophisticated attribute patterns that go beyond basic usage:
<?php
namespace App\Attributes;
use Attribute;
use ReflectionClass;
use ReflectionMethod;
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class Cached
{
public function __construct(
private int $ttl = 3600,
private ?string $key = null,
private array $tags = [],
private bool $perUser = false
) {}
public function getCacheKey(object $instance, string $method, array $args): string
{
if ($this->key) {
return $this->interpolateKey($this->key, $instance, $args);
}
$key = get_class($instance) . ':' . $method;
if ($this->perUser && $user = auth()->user()) {
$key .= ':user:' . $user->id;
}
if (!empty($args)) {
$key .= ':' . md5(serialize($args));
}
return $key;
}
private function interpolateKey(string $key, object $instance, array $args): string
{
return preg_replace_callback('/{(\w+)}/', function ($matches) use ($instance, $args) {
$param = $matches[1];
// Check instance properties
if (property_exists($instance, $param)) {
return $instance->$param;
}
// Check method arguments
if (isset($args[$param])) {
return $args[$param];
}
return $matches[0];
}, $key);
}
public function getTtl(): int
{
return $this->ttl;
}
public function getTags(): array
{
return $this->tags;
}
}
Attribute Composition and Inheritance
Create powerful attribute combinations:
<?php
namespace App\Attributes;
#[Attribute(Attribute::TARGET_CLASS)]
class Repository
{
public function __construct(
private string $model,
private array $scopes = [],
private bool $cacheable = true
) {}
}
#[Attribute(Attribute::TARGET_METHOD)]
class Transaction
{
public function __construct(
private int $attempts = 1,
private int $delay = 0,
private ?string $connection = null
) {}
}
#[Attribute(Attribute::TARGET_METHOD)]
class Authorized
{
public function __construct(
private string|array $permissions,
private ?string $guard = null,
private bool $requireAll = true
) {}
}
// Composite attribute
#[Attribute(Attribute::TARGET_METHOD)]
class SecureTransaction
{
public function __construct(
private string|array $permissions,
private int $attempts = 3
) {}
public function getAttributes(): array
{
return [
new Authorized($this->permissions),
new Transaction($this->attempts),
new Logged('security.transaction')
];
}
}
Building an Attribute-Driven Framework
Let's create a mini-framework that processes attributes:
<?php
namespace App\Core;
use ReflectionClass;
use ReflectionMethod;
use ReflectionAttribute;
class AttributeProcessor
{
private array $handlers = [];
private array $cache = [];
public function registerHandler(string $attributeClass, callable $handler): void
{
$this->handlers[$attributeClass] = $handler;
}
public function process(object $instance, string $method, array $arguments): mixed
{
$reflection = new ReflectionMethod($instance, $method);
$attributes = $this->getMethodAttributes($reflection);
// Pre-processing
$context = new ExecutionContext($instance, $method, $arguments);
foreach ($attributes as $attribute) {
if ($handler = $this->getHandler($attribute)) {
$result = $handler($attribute, $context, 'before');
if ($result instanceof Response) {
return $result;
}
}
}
// Execute method
try {
$result = $reflection->invoke($instance, ...$arguments);
$context->setResult($result);
} catch (\Throwable $e) {
$context->setException($e);
// Exception handling
foreach ($attributes as $attribute) {
if ($handler = $this->getHandler($attribute)) {
$handled = $handler($attribute, $context, 'exception');
if ($handled instanceof Response) {
return $handled;
}
}
}
throw $e;
}
// Post-processing
foreach (array_reverse($attributes) as $attribute) {
if ($handler = $this->getHandler($attribute)) {
$result = $handler($attribute, $context, 'after');
if ($result !== null) {
$context->setResult($result);
}
}
}
return $context->getResult();
}
private function getMethodAttributes(ReflectionMethod $method): array
{
$cacheKey = $method->class . '::' . $method->name;
if (isset($this->cache[$cacheKey])) {
return $this->cache[$cacheKey];
}
$attributes = [];
// Get method attributes
foreach ($method->getAttributes() as $attribute) {
$attributes[] = $attribute->newInstance();
}
// Get class attributes that should apply to methods
$class = $method->getDeclaringClass();
foreach ($class->getAttributes() as $attribute) {
$instance = $attribute->newInstance();
if ($instance instanceof MethodApplicable) {
$attributes[] = $instance;
}
}
// Handle composite attributes
$expanded = [];
foreach ($attributes as $attribute) {
if (method_exists($attribute, 'getAttributes')) {
$expanded = array_merge($expanded, $attribute->getAttributes());
} else {
$expanded[] = $attribute;
}
}
return $this->cache[$cacheKey] = $expanded;
}
private function getHandler(object $attribute): ?callable
{
$class = get_class($attribute);
return $this->handlers[$class] ?? null;
}
}
Real-World Attribute Patterns
Validation Attributes
Create expressive validation using attributes:
<?php
namespace App\Attributes\Validation;
use Attribute;
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]
class Valid
{
public function __construct(
private array $rules = []
) {}
public function validate(mixed $value): bool
{
$validator = validator(['value' => $value], ['value' => $this->rules]);
return !$validator->fails();
}
public function getErrors(mixed $value): array
{
$validator = validator(['value' => $value], ['value' => $this->rules]);
if ($validator->fails()) {
return $validator->errors()->get('value');
}
return [];
}
}
#[Attribute(Attribute::TARGET_PROPERTY)]
class Range
{
public function __construct(
private int|float $min,
private int|float $max
) {}
public function validate(mixed $value): bool
{
return is_numeric($value) && $value >= $this->min && $value <= $this->max;
}
}
// Usage in a DTO
class CreateProductDTO
{
#[Valid(['required', 'string', 'max:255'])]
public string $name;
#[Valid(['required', 'numeric'])]
#[Range(0, 999999.99)]
public float $price;
#[Valid(['required', 'integer', 'min:0'])]
public int $stock;
#[Valid(['nullable', 'string'])]
public ?string $description;
public function __construct(array $data)
{
$this->hydrate($data);
$this->validate();
}
private function validate(): void
{
$reflection = new ReflectionClass($this);
$errors = [];
foreach ($reflection->getProperties() as $property) {
$attributes = $property->getAttributes();
foreach ($attributes as $attribute) {
$instance = $attribute->newInstance();
if (method_exists($instance, 'validate')) {
$value = $property->getValue($this);
if (!$instance->validate($value)) {
$errors[$property->getName()][] = $instance->getErrors($value);
}
}
}
}
if (!empty($errors)) {
throw new ValidationException($errors);
}
}
}
Dependency Injection Attributes
Implement sophisticated DI patterns:
<?php
namespace App\Attributes\DI;
use Attribute;
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]
class Inject
{
public function __construct(
private ?string $service = null,
private array $parameters = []
) {}
public function resolve(ReflectionProperty|ReflectionParameter $reflection): mixed
{
$type = $reflection->getType();
if (!$type || $type->isBuiltin()) {
throw new \RuntimeException('Cannot inject into untyped or built-in type');
}
$service = $this->service ?? $type->getName();
if (!empty($this->parameters)) {
return app($service, $this->parameters);
}
return app($service);
}
}
#[Attribute(Attribute::TARGET_PROPERTY)]
class Config
{
public function __construct(
private string $key,
private mixed $default = null
) {}
public function resolve(): mixed
{
return config($this->key, $this->default);
}
}
#[Attribute(Attribute::TARGET_PROPERTY)]
class Environment
{
public function __construct(
private string $key,
private mixed $default = null
) {}
public function resolve(): mixed
{
return env($this->key, $this->default);
}
}
// Auto-injection trait
trait AutoInject
{
public function __construct()
{
$this->injectDependencies();
}
private function injectDependencies(): void
{
$reflection = new ReflectionClass($this);
foreach ($reflection->getProperties() as $property) {
foreach ($property->getAttributes() as $attribute) {
$instance = $attribute->newInstance();
if (method_exists($instance, 'resolve')) {
$property->setAccessible(true);
$property->setValue($this, $instance->resolve($property));
}
}
}
}
}
// Usage
class PaymentService
{
use AutoInject;
#[Inject]
private PaymentGateway $gateway;
#[Inject(StripeClient::class, ['api_key' => 'env:STRIPE_KEY'])]
private StripeClient $stripe;
#[Config('payment.timeout', 30)]
private int $timeout;
#[Environment('PAYMENT_MODE', 'test')]
private string $mode;
}
Event-Driven Attributes
Create an event system using attributes:
<?php
namespace App\Attributes\Events;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class Listen
{
public function __construct(
private string $event,
private int $priority = 0
) {}
public function getEvent(): string
{
return $this->event;
}
public function getPriority(): int
{
return $this->priority;
}
}
#[Attribute(Attribute::TARGET_METHOD)]
class Emit
{
public function __construct(
private string $event,
private bool $async = false
) {}
}
// Event dispatcher with attribute support
class AttributeEventDispatcher
{
private array $listeners = [];
public function scanForListeners(object $instance): void
{
$reflection = new ReflectionClass($instance);
foreach ($reflection->getMethods() as $method) {
foreach ($method->getAttributes(Listen::class) as $attribute) {
$listen = $attribute->newInstance();
$this->listeners[$listen->getEvent()][] = [
'instance' => $instance,
'method' => $method->getName(),
'priority' => $listen->getPriority()
];
}
}
// Sort by priority
foreach ($this->listeners as $event => &$listeners) {
usort($listeners, fn($a, $b) => $b['priority'] <=> $a['priority']);
}
}
public function dispatch(string $event, array $payload = []): void
{
if (!isset($this->listeners[$event])) {
return;
}
foreach ($this->listeners[$event] as $listener) {
$listener['instance']->{$listener['method']}(...$payload);
}
}
}
// Usage
class OrderService
{
#[Emit('order.created', async: true)]
public function createOrder(array $data): Order
{
$order = Order::create($data);
// Emission handled by attribute processor
return $order;
}
#[Listen('order.created', priority: 10)]
public function sendOrderConfirmation(Order $order): void
{
Mail::to($order->customer)->send(new OrderConfirmation($order));
}
#[Listen('order.created', priority: 5)]
public function updateInventory(Order $order): void
{
foreach ($order->items as $item) {
Inventory::decrement($item->product_id, $item->quantity);
}
}
}
Performance Optimization with Attributes
Leverage attributes for performance optimization:
<?php
namespace App\Attributes\Performance;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD)]
class Memoize
{
private static array $cache = [];
public function __construct(
private ?int $maxEntries = 100
) {}
public function getCachedResult(string $key): mixed
{
if (isset(self::$cache[$key])) {
return self::$cache[$key]['value'];
}
return null;
}
public function setCachedResult(string $key, mixed $value): void
{
if (count(self::$cache) >= $this->maxEntries) {
array_shift(self::$cache);
}
self::$cache[$key] = [
'value' => $value,
'hits' => 0
];
}
public function generateKey(object $instance, string $method, array $args): string
{
return sprintf(
'%s::%s(%s)',
get_class($instance),
$method,
md5(serialize($args))
);
}
}
#[Attribute(Attribute::TARGET_CLASS)]
class LazyLoad
{
private array $initialized = [];
public function __construct(
private array $properties = []
) {}
public function initialize(object $instance, string $property): void
{
$key = spl_object_hash($instance) . '::' . $property;
if (isset($this->initialized[$key])) {
return;
}
$reflection = new ReflectionProperty($instance, $property);
$reflection->setAccessible(true);
$initMethod = 'init' . ucfirst($property);
if (method_exists($instance, $initMethod)) {
$reflection->setValue($instance, $instance->$initMethod());
}
$this->initialized[$key] = true;
}
}
// Usage
#[LazyLoad(['heavyData', 'expensiveCalculation'])]
class AnalyticsService
{
use AutoProxy;
private ?array $heavyData = null;
private ?float $expensiveCalculation = null;
#[Memoize(maxEntries: 50)]
public function calculateMetrics(array $data): array
{
// Complex calculation that benefits from memoization
return array_map(fn($item) => $this->processItem($item), $data);
}
protected function initHeavyData(): array
{
// Load data only when accessed
return DB::table('analytics')
->where('date', '>=', now()->subDays(30))
->get()
->toArray();
}
protected function initExpensiveCalculation(): float
{
// Perform expensive calculation only when needed
return $this->performComplexCalculation($this->heavyData);
}
}
Testing Attributes
Create testable attribute-driven code:
<?php
namespace Tests\Attributes;
use PHPUnit\Framework\TestCase;
use App\Attributes\Cached;
use App\Core\AttributeProcessor;
class AttributeTest extends TestCase
{
public function testCachedAttribute()
{
$processor = new AttributeProcessor();
$cacheHandler = $this->createMock(CacheHandler::class);
$processor->registerHandler(Cached::class, function ($attribute, $context, $phase) use ($cacheHandler) {
if ($phase === 'before') {
$key = $attribute->getCacheKey(
$context->getInstance(),
$context->getMethod(),
$context->getArguments()
);
$cached = $cacheHandler->get($key);
if ($cached !== null) {
return $cached;
}
} elseif ($phase === 'after') {
$key = $attribute->getCacheKey(
$context->getInstance(),
$context->getMethod(),
$context->getArguments()
);
$cacheHandler->set($key, $context->getResult(), $attribute->getTtl());
}
});
$service = new class {
#[Cached(ttl: 60, key: 'test_{id}')]
public function getData(int $id): array
{
return ['id' => $id, 'data' => 'test'];
}
};
$cacheHandler->expects($this->once())
->method('get')
->with('test_123')
->willReturn(null);
$cacheHandler->expects($this->once())
->method('set')
->with('test_123', ['id' => 123, 'data' => 'test'], 60);
$result = $processor->process($service, 'getData', [123]);
$this->assertEquals(['id' => 123, 'data' => 'test'], $result);
}
}
Advanced Patterns and Best Practices
Attribute Metadata and Reflection
<?php
namespace App\Attributes\Metadata;
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
class Documentation
{
public function __construct(
private string $description,
private array $examples = [],
private array $tags = []
) {}
public static function getDocumentation(string $class, ?string $method = null): ?self
{
$reflection = $method
? new ReflectionMethod($class, $method)
: new ReflectionClass($class);
$attributes = $reflection->getAttributes(self::class);
return !empty($attributes) ? $attributes[0]->newInstance() : null;
}
public function toArray(): array
{
return [
'description' => $this->description,
'examples' => $this->examples,
'tags' => $this->tags
];
}
}
// Generate API documentation from attributes
class ApiDocGenerator
{
public function generate(array $controllers): array
{
$documentation = [];
foreach ($controllers as $controller) {
$reflection = new ReflectionClass($controller);
$classDoc = Documentation::getDocumentation($controller);
$endpoints = [];
foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
if ($method->isConstructor()) continue;
$methodDoc = Documentation::getDocumentation($controller, $method->getName());
$route = $this->getRoute($method);
if ($route && $methodDoc) {
$endpoints[] = [
'method' => $route['method'],
'path' => $route['path'],
'documentation' => $methodDoc->toArray()
];
}
}
if (!empty($endpoints)) {
$documentation[$controller] = [
'class' => $classDoc?->toArray(),
'endpoints' => $endpoints
];
}
}
return $documentation;
}
}
Conclusion
PHP 8.3 Attributes have matured into a powerful feature that enables elegant, declarative programming patterns. By moving beyond simple annotations to sophisticated metaprogramming techniques, attributes allow us to write cleaner, more maintainable code that clearly expresses intent.
The examples we've explored – from caching and validation to dependency injection and event handling – demonstrate that attributes are more than syntactic sugar. They're a fundamental tool for building modern PHP applications that are both powerful and expressive. As the PHP ecosystem continues to embrace attributes, mastering these patterns becomes essential for any serious PHP developer.
Add Comment
No comments yet. Be the first to comment!