掌握 PHP Attributes 从自定义创建到生产实现

引言:PHP 元数据编程的现代时代

PHP 8.0 引入了原生 Attributes(以前称为注解),彻底改变了我们编写声明式代码的方式。Attributes 实现了优雅的元数据驱动编程,用结构化、类型安全的声明替代了 docblock 注解。本综合指南将探讨自定义 Attributes、Reflection API 集成、基于 Attribute 的路由以及验证系统。

原文链接 掌握 PHP Attributes 从自定义创建到生产实现

第一部分:自定义 Attributes——构建你自己的元数据层

理解 PHP Attributes 基础

PHP Attributes 是用 #[Attribute] 标记的特殊类,可以附加到类、方法、属性、参数和常量上。与注释不同,它们是 AST(抽象语法树)的一部分,可通过 Reflection 访问。

创建生产级自定义 Attribute

让我们为企业应用构建一个全面的日志 Attribute 系统:

<?php

#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
class AuditLog
{
    public function __construct(
        public string $operation,
        public LogLevel $level = LogLevel::INFO,
        public bool $includeParameters = true,
        public bool $includeReturnValue = false,
        public array $sensitiveParameters = []
    ) {}
}

enum LogLevel: string
{
    case TRACE = 'trace';
    case DEBUG = 'debug';
    case INFO = 'info';
    case WARNING = 'warning';
    case ERROR = 'error';
    case CRITICAL = 'critical';
}

进阶用法:属性级验证 Attribute

这是一个用于属性级验证的自定义 Attribute,支持复杂规则:

<?php

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
class BusinessRule
{
    public function __construct(
        public string $ruleName,
        public string $errorMessage = '',
        public int $priority = 0
    ) {
        if (empty($this->errorMessage)) {
            $this->errorMessage = "Business rule '{$ruleName}' validation failed";
        }
    }
}

#[Attribute(Attribute::TARGET_PROPERTY)]
class CreditCardValidation extends BusinessRule
{
    public function __construct(
        public array $acceptedCardTypes = ['Visa', 'MasterCard', 'Amex'],
        public bool $requireCVV = true,
        string $errorMessage = 'Invalid credit card'
    ) {
        parent::__construct('CreditCardValidation', $errorMessage);
    }
}

#[Attribute(Attribute::TARGET_PROPERTY)]
class EmailValidation
{
    public function __construct(
        public bool $checkDNS = false,
        public array $allowedDomains = [],
        public string $errorMessage = 'Invalid email address'
    ) {}
}

实际应用示例

<?php

#[Attribute(Attribute::TARGET_METHOD)]
class Transaction
{
    public function __construct(
        public string $isolationLevel = 'SERIALIZABLE'
    ) {}
}

#[Attribute(Attribute::TARGET_METHOD)]
class RetryPolicy
{
    public function __construct(
        public int $maxAttempts = 3,
        public int $delayMilliseconds = 1000,
        public array $retryOnExceptions = []
    ) {}
}

class PaymentProcessor
{
    #[AuditLog(
        operation: 'ProcessPayment',
        level: LogLevel::CRITICAL,
        sensitiveParameters: ['creditCardNumber', 'cvv']
    )]
    #[Transaction(isolationLevel: 'SERIALIZABLE')]
    #[RetryPolicy(maxAttempts: 3, delayMilliseconds: 1000)]
    public function processPayment(
        float $amount,
        string $creditCardNumber,
        string $cvv
    ): PaymentResult {
        // Implementation
        return new PaymentResult();
    }
}

第二部分:Reflection API——在运行时读取和处理 Attributes

基础:Reflection 与 Attribute 发现

PHP 的 Reflection API 提供了强大的工具来在运行时检查和操作 Attributes。这是一个全面的 Attribute 处理器:

<?php

class AttributeProcessor
{
    /**
     * Get all attributes of a specific type from a class
     */
    public static function getClassAttributes(string $className, string $attributeClass): array
    {
        $reflection = new ReflectionClass($className);
        $attributes = $reflection->getAttributes($attributeClass, ReflectionAttribute::IS_INSTANCEOF);
        
        return array_map(fn($attr) => $attr->newInstance(), $attributes);
    }
    
    /**
     * Get attributes from all methods in a class
     */
    public static function getMethodAttributes(string $className, string $attributeClass): array
    {
        $reflection = new ReflectionClass($className);
        $result = [];
        
        foreach ($reflection->getMethods() as $method) {
            $attributes = $method->getAttributes($attributeClass, ReflectionAttribute::IS_INSTANCEOF);
            
            if (!empty($attributes)) {
                $result[$method->getName()] = array_map(
                    fn($attr) => $attr->newInstance(),
                    $attributes
                );
            }
        }
        
        return $result;
    }
    
    /**
     * Get attributes from properties
     */
    public static function getPropertyAttributes(string $className, string $attributeClass): array
    {
        $reflection = new ReflectionClass($className);
        $result = [];
        
        foreach ($reflection->getProperties() as $property) {
            $attributes = $property->getAttributes($attributeClass, ReflectionAttribute::IS_INSTANCEOF);
            
            if (!empty($attributes)) {
                $result[$property->getName()] = array_map(
                    fn($attr) => $attr->newInstance(),
                    $attributes
                );
            }
        }
        
        return $result;
    }
}

构建 Attribute 驱动的拦截器

这是使用 Reflection 实现审计日志拦截器的实际示例:

<?php

class AuditLogInterceptor
{
    private LoggerInterface $logger;
    
    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }
    
    public function intercept(object $target, string $method, array $arguments): mixed
    {
        $reflection = new ReflectionMethod($target, $method);
        $attributes = $reflection->getAttributes(AuditLog::class);
        
        if (empty($attributes)) {
            return $target->$method(...$arguments);
        }
        
        foreach ($attributes as $attribute) {
            $auditLog = $attribute->newInstance();
            $this->logBefore($auditLog, $method, $arguments, $reflection);
        }
        
        $startTime = microtime(true);
        
        try {
            $result = $target->$method(...$arguments);
            $executionTime = microtime(true) - $startTime;
            
            foreach ($attributes as $attribute) {
                $auditLog = $attribute->newInstance();
                $this->logAfter($auditLog, $method, $result, $executionTime);
            }
            
            return $result;
        } catch (Throwable $e) {
            foreach ($attributes as $attribute) {
                $auditLog = $attribute->newInstance();
                $this->logError($auditLog, $method, $e);
            }
            throw $e;
        }
    }
    
    private function logBefore(AuditLog $audit, string $method, array $arguments, ReflectionMethod $reflection): void
    {
        $logData = [
            'operation' => $audit->operation,
            'method' => $method,
            'timestamp' => date('Y-m-d H:i:s'),
        ];
        
        if ($audit->includeParameters) {
            $params = $reflection->getParameters();
            $sanitizedArgs = [];
            
            foreach ($params as $index => $param) {
                $paramName = $param->getName();
                $value = $arguments[$index] ?? null;
                
                if (in_array($paramName, $audit->sensitiveParameters)) {
                    $sanitizedArgs[$paramName] = '***REDACTED***';
                } else {
                    $sanitizedArgs[$paramName] = $value;
                }
            }
            
            $logData['parameters'] = $sanitizedArgs;
        }
        
        $this->logger->log($audit->level->value, "Executing: {$audit->operation}", $logData);
    }
    
    private function logAfter(AuditLog $audit, string $method, mixed $result, float $executionTime): void
    {
        $logData = [
            'operation' => $audit->operation,
            'method' => $method,
            'execution_time' => round($executionTime * 1000, 2) . 'ms',
        ];
        
        if ($audit->includeReturnValue) {
            $logData['return_value'] = $result;
        }
        
        $this->logger->log($audit->level->value, "Completed: {$audit->operation}", $logData);
    }
    
    private function logError(AuditLog $audit, string $method, Throwable $e): void
    {
        $this->logger->log(LogLevel::ERROR->value, "Failed: {$audit->operation}", [
            'operation' => $audit->operation,
            'method' => $method,
            'error' => $e->getMessage(),
            'trace' => $e->getTraceAsString(),
        ]);
    }
}

使用 Attributes 的动态代理模式

<?php

class AttributeProxy
{
    private object $target;
    private AuditLogInterceptor $interceptor;
    
    public function __construct(object $target, AuditLogInterceptor $interceptor)
    {
        $this->target = $target;
        $this->interceptor = $interceptor;
    }
    
    public function __call(string $method, array $arguments): mixed
    {
        $reflection = new ReflectionClass($this->target);
        
        if (!$reflection->hasMethod($method)) {
            throw new BadMethodCallException("Method {$method} does not exist");
        }
        
        $methodReflection = $reflection->getMethod($method);
        $attributes = $methodReflection->getAttributes(AuditLog::class);
        
        if (!empty($attributes)) {
            return $this->interceptor->intercept($this->target, $method, $arguments);
        }
        
        return $this->target->$method(...$arguments);
    }
}

// Usage
$processor = new PaymentProcessor();
$logger = new Logger();
$interceptor = new AuditLogInterceptor($logger);
$proxy = new AttributeProxy($processor, $interceptor);

// This call will be intercepted and logged
$result = $proxy->processPayment(100.00, '4111111111111111', '123');

第三部分:Attribute 驱动的路由系统

路由 Attributes 定义

<?php

#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)]
class Route
{
    public function __construct(
        public string $path,
        public string $method = 'GET',
        public array $middleware = [],
        public ?string $name = null
    ) {}
}

#[Attribute(Attribute::TARGET_CLASS)]
class RoutePrefix
{
    public function __construct(
        public string $prefix,
        public array $middleware = []
    ) {}
}

#[Attribute(Attribute::TARGET_PARAMETER)]
class FromBody
{
    public function __construct(
        public ?string $validator = null
    ) {}
}

#[Attribute(Attribute::TARGET_PARAMETER)]
class FromQuery
{
    public function __construct(
        public ?string $name = null,
        public mixed $default = null
    ) {}
}

#[Attribute(Attribute::TARGET_PARAMETER)]
class FromRoute
{
    public function __construct(
        public string $parameter
    ) {}
}

路由注册器实现

<?php

class RouteRegistry
{
    private array $routes = [];
    
    public function register(string $controllerClass): void
    {
        $reflection = new ReflectionClass($controllerClass);
        
        // Get class-level prefix and middleware
        $prefixAttributes = $reflection->getAttributes(RoutePrefix::class);
        $prefix = '';
        $classMiddleware = [];
        
        if (!empty($prefixAttributes)) {
            $routePrefix = $prefixAttributes[0]->newInstance();
            $prefix = $routePrefix->prefix;
            $classMiddleware = $routePrefix->middleware;
        }
        
        // Process each method
        foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
            $routeAttributes = $method->getAttributes(Route::class);
            
            foreach ($routeAttributes as $attribute) {
                $route = $attribute->newInstance();
                
                $fullPath = $prefix . $route->path;
                $middleware = array_merge($classMiddleware, $route->middleware);
                
                $this->routes[] = [
                    'method' => strtoupper($route->method),
                    'path' => $fullPath,
                    'controller' => $controllerClass,
                    'action' => $method->getName(),
                    'middleware' => $middleware,
                    'name' => $route->name,
                    'parameters' => $this->extractParameters($method),
                ];
            }
        }
    }
    
    private function extractParameters(ReflectionMethod $method): array
    {
        $parameters = [];
        
        foreach ($method->getParameters() as $param) {
            $paramInfo = [
                'name' => $param->getName(),
                'type' => $param->getType()?->getName(),
                'source' => 'auto',
            ];
            
            // Check for parameter attributes
            $fromBodyAttrs = $param->getAttributes(FromBody::class);
            $fromQueryAttrs = $param->getAttributes(FromQuery::class);
            $fromRouteAttrs = $param->getAttributes(FromRoute::class);
            
            if (!empty($fromBodyAttrs)) {
                $paramInfo['source'] = 'body';
                $paramInfo['validator'] = $fromBodyAttrs[0]->newInstance()->validator;
            } elseif (!empty($fromQueryAttrs)) {
                $fromQuery = $fromQueryAttrs[0]->newInstance();
                $paramInfo['source'] = 'query';
                $paramInfo['queryName'] = $fromQuery->name ?? $param->getName();
                $paramInfo['default'] = $fromQuery->default;
            } elseif (!empty($fromRouteAttrs)) {
                $fromRoute = $fromRouteAttrs[0]->newInstance();
                $paramInfo['source'] = 'route';
                $paramInfo['routeParam'] = $fromRoute->parameter;
            }
            
            $parameters[] = $paramInfo;
        }
        
        return $parameters;
    }
    
    public function getRoutes(): array
    {
        return $this->routes;
    }
    
    public function match(string $method, string $path): ?array
    {
        foreach ($this->routes as $route) {
            if ($route['method'] !== strtoupper($method)) {
                continue;
            }
            
            $pattern = $this->pathToRegex($route['path']);
            
            if (preg_match($pattern, $path, $matches)) {
                return [
                    'route' => $route,
                    'params' => array_filter($matches, 'is_string', ARRAY_FILTER_USE_KEY),
                ];
            }
        }
        
        return null;
    }
    
    private function pathToRegex(string $path): string
    {
        $pattern = preg_replace('/{([a-zA-Z0-9_]+)}/', '(?P<$1>[^/]+)', $path);
        return '#^' . $pattern . '$#';
    }
}

控制器示例

<?php

#[RoutePrefix('/api/users', middleware: ['auth', 'api'])]
class UserController
{
    #[Route('/', method: 'GET', name: 'users.list')]
    public function index(
        #[FromQuery('page', default: 1)] int $page,
        #[FromQuery('limit', default: 20)] int $limit
    ): array {
        return [
            'users' => [],
            'page' => $page,
            'limit' => $limit,
        ];
    }
    
    #[Route('/{id}', method: 'GET', name: 'users.show')]
    public function show(#[FromRoute('id')] int $id): array
    {
        return ['user' => ['id' => $id]];
    }
    
    #[Route('/', method: 'POST', name: 'users.create')]
    public function create(
        #[FromBody(validator: UserValidator::class)] UserCreateRequest $request
    ): array {
        return ['user' => ['id' => 1]];
    }
    
    #[Route('/{id}', method: 'PUT', name: 'users.update')]
    public function update(
        #[FromRoute('id')] int $id,
        #[FromBody] UserUpdateRequest $request
    ): array {
        return ['user' => ['id' => $id]];
    }
    
    #[Route('/{id}', method: 'DELETE', name: 'users.delete')]
    public function delete(#[FromRoute('id')] int $id): array
    {
        return ['success' => true];
    }
}

路由调度器

<?php

class Router
{
    private RouteRegistry $registry;
    private Container $container;
    
    public function __construct(RouteRegistry $registry, Container $container)
    {
        $this->registry = $registry;
        $this->container = $container;
    }
    
    public function dispatch(string $method, string $path): mixed
    {
        $match = $this->registry->match($method, $path);
        
        if ($match === null) {
            http_response_code(404);
            return ['error' => 'Route not found'];
        }
        
        $route = $match['route'];
        $routeParams = $match['params'];
        
        // Run middleware
        foreach ($route['middleware'] as $middleware) {
            $this->runMiddleware($middleware);
        }
        
        // Resolve controller
        $controller = $this->container->resolve($route['controller']);
        
        // Prepare method arguments
        $arguments = $this->prepareArguments($route['parameters'], $routeParams);
        
        // Call controller method
        return $controller->{$route['action']}(...$arguments);
    }
    
    private function prepareArguments(array $parameters, array $routeParams): array
    {
        $arguments = [];
        
        foreach ($parameters as $param) {
            $value = match($param['source']) {
                'body' => $this->getBodyParameter($param),
                'query' => $this->getQueryParameter($param),
                'route' => $routeParams[$param['routeParam']] ?? null,
                default => null,
            };
            
            $arguments[] = $value;
        }
        
        return $arguments;
    }
    
    private function getBodyParameter(array $param): mixed
    {
        $body = json_decode(file_get_contents('php://input'), true);
        
        if ($param['type'] && class_exists($param['type'])) {
            $instance = new $param['type']();
            
            foreach ($body as $key => $value) {
                if (property_exists($instance, $key)) {
                    $instance->$key = $value;
                }
            }
            
            return $instance;
        }
        
        return $body;
    }
    
    private function getQueryParameter(array $param): mixed
    {
        $queryName = $param['queryName'] ?? $param['name'];
        return $_GET[$queryName] ?? $param['default'] ?? null;
    }
    
    private function runMiddleware(string $middleware): void
    {
        // Middleware implementation
    }
}

第四部分:Attribute 驱动的验证系统

验证 Attributes

<?php

#[Attribute(Attribute::TARGET_PROPERTY)]
class Required
{
    public function __construct(
        public string $message = 'This field is required'
    ) {}
}

#[Attribute(Attribute::TARGET_PROPERTY)]
class Length
{
    public function __construct(
        public ?int $min = null,
        public ?int $max = null,
        public string $message = ''
    ) {
        if (empty($this->message)) {
            if ($this->min && $this->max) {
                $this->message = "Length must be between {$this->min} and {$this->max}";
            } elseif ($this->min) {
                $this->message = "Minimum length is {$this->min}";
            } elseif ($this->max) {
                $this->message = "Maximum length is {$this->max}";
            }
        }
    }
}

#[Attribute(Attribute::TARGET_PROPERTY)]
class Email
{
    public function __construct(
        public bool $checkDNS = false,
        public string $message = 'Invalid email address'
    ) {}
}

#[Attribute(Attribute::TARGET_PROPERTY)]
class Pattern
{
    public function __construct(
        public string $regex,
        public string $message = 'Invalid format'
    ) {}
}

#[Attribute(Attribute::TARGET_PROPERTY)]
class Range
{
    public function __construct(
        public ?float $min = null,
        public ?float $max = null,
        public string $message = ''
    ) {
        if (empty($this->message)) {
            if ($this->min !== null && $this->max !== null) {
                $this->message = "Value must be between {$this->min} and {$this->max}";
            } elseif ($this->min !== null) {
                $this->message = "Minimum value is {$this->min}";
            } elseif ($this->max !== null) {
                $this->message = "Maximum value is {$this->max}";
            }
        }
    }
}

#[Attribute(Attribute::TARGET_PROPERTY)]
class InArray
{
    public function __construct(
        public array $allowedValues,
        public string $message = 'Invalid value'
    ) {}
}

#[Attribute(Attribute::TARGET_PROPERTY)]
class Url
{
    public function __construct(
        public array $allowedSchemes = ['http', 'https'],
        public string $message = 'Invalid URL'
    ) {}
}

#[Attribute(Attribute::TARGET_PROPERTY)]
class Date
{
    public function __construct(
        public string $format = 'Y-m-d',
        public string $message = 'Invalid date format'
    ) {}
}

#[Attribute(Attribute::TARGET_PROPERTY)]
class Unique
{
    public function __construct(
        public string $table,
        public string $column,
        public ?int $ignoreId = null,
        public string $message = 'This value already exists'
    ) {}
}

#[Attribute(Attribute::TARGET_PROPERTY)]
class CreditCard
{
    public function __construct(
        public array $types = ['visa', 'mastercard', 'amex', 'discover'],
        public string $message = 'Invalid credit card number'
    ) {}
}

#[Attribute(Attribute::TARGET_CLASS)]
class CompareFields
{
    public function __construct(
        public string $field1,
        public string $field2,
        public string $operator = '===',
        public string $message = 'Fields do not match'
    ) {}
}

增强验证器实现

<?php

class EnhancedValidator
{
    private array $errors = [];
    
    public function validate(object $object): bool
    {
        $this->errors = [];
        $reflection = new ReflectionClass($object);
        
        // Validate properties
        foreach ($reflection->getProperties() as $property) {
            $this->validateProperty($object, $property);
        }
        
        // Validate class-level rules
        $this->validateClassRules($object, $reflection);
        
        return empty($this->errors);
    }
    
    private function validateProperty(object $object, ReflectionProperty $property): void
    {
        $property->setAccessible(true);
        $value = $property->getValue($object);
        $propertyName = $property->getName();
        
        foreach ($property->getAttributes() as $attribute) {
            $validator = $attribute->newInstance();
            
            $isValid = match($attribute->getName()) {
                Required::class => $this->validateRequired($value, $validator),
                Length::class => $this->validateLength($value, $validator),
                Email::class => $this->validateEmail($value, $validator),
                Pattern::class => $this->validatePattern($value, $validator),
                Range::class => $this->validateRange($value, $validator),
                InArray::class => $this->validateInArray($value, $validator),
                Url::class => $this->validateUrl($value, $validator),
                Date::class => $this->validateDate($value, $validator),
                CreditCard::class => $this->validateCreditCard($value, $validator),
                default => true,
            };
            
            if (!$isValid) {
                $this->errors[$propertyName][] = $validator->message;
            }
        }
    }
    
    private function validateRequired(mixed $value, Required $rule): bool
    {
        if (is_string($value)) {
            return trim($value) !== '';
        }
        return $value !== null && $value !== '';
    }
    
    private function validateLength(mixed $value, Length $rule): bool
    {
        if ($value === null || $value === '') {
            return true;
        }
        
        $length = mb_strlen((string)$value);
        
        if ($rule->min !== null && $length < $rule->min) {
            return false;
        }
        
        if ($rule->max !== null && $length > $rule->max) {
            return false;
        }
        
        return true;
    }
    
    private function validateEmail(mixed $value, Email $rule): bool
    {
        if ($value === null || $value === '') {
            return true;
        }
        
        if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
            return false;
        }
        
        if ($rule->checkDNS) {
            $domain = substr(strrchr($value, "@"), 1);
            return checkdnsrr($domain, 'MX');
        }
        
        return true;
    }
    
    private function validatePattern(mixed $value, Pattern $rule): bool
    {
        if ($value === null || $value === '') {
            return true;
        }
        
        return preg_match($rule->regex, (string)$value) === 1;
    }
    
    private function validateRange(mixed $value, Range $rule): bool
    {
        if ($value === null || $value === '') {
            return true;
        }
        
        $numValue = (float)$value;
        
        if ($rule->min !== null && $numValue < $rule->min) {
            return false;
        }
        
        if ($rule->max !== null && $numValue > $rule->max) {
            return false;
        }
        
        return true;
    }
    
    private function validateInArray(mixed $value, InArray $rule): bool
    {
        if ($value === null || $value === '') {
            return true;
        }
        
        return in_array($value, $rule->allowedValues, true);
    }
    
    private function validateUrl(mixed $value, Url $rule): bool
    {
        if ($value === null || $value === '') {
            return true;
        }
        
        if (!filter_var($value, FILTER_VALIDATE_URL)) {
            return false;
        }
        
        $scheme = parse_url($value, PHP_URL_SCHEME);
        return in_array($scheme, $rule->allowedSchemes);
    }
    
    private function validateDate(mixed $value, Date $rule): bool
    {
        if ($value === null || $value === '') {
            return true;
        }
        
        $date = DateTime::createFromFormat($rule->format, (string)$value);
        return $date && $date->format($rule->format) === $value;
    }
    
    private function validateCreditCard(mixed $value, CreditCard $rule): bool
    {
        if ($value === null || $value === '') {
            return true;
        }
        
        // Luhn algorithm
        $number = preg_replace('/D/', '', (string)$value);
        $sum = 0;
        $length = strlen($number);
        
        for ($i = 0; $i < $length; $i++) {
            $digit = (int)$number[$length - $i - 1];
            
            if ($i % 2 === 1) {
                $digit *= 2;
                if ($digit > 9) {
                    $digit -= 9;
                }
            }
            
            $sum += $digit;
        }
        
        return $sum % 10 === 0;
    }
    
    private function validateClassRules(object $object, ReflectionClass $reflection): void
    {
        $attributes = $reflection->getAttributes(
            CompareFields::class,
            ReflectionAttribute::IS_INSTANCEOF
        );
        
        foreach ($attributes as $attribute) {
            $rule = $attribute->newInstance();
            
            $prop1 = $reflection->getProperty($rule->field1);
            $prop2 = $reflection->getProperty($rule->field2);
            
            $prop1->setAccessible(true);
            $prop2->setAccessible(true);
            
            $value1 = $prop1->getValue($object);
            $value2 = $prop2->getValue($object);
            
            $isValid = match($rule->operator) {
                '===' => $value1 === $value2,
                '==' => $value1 == $value2,
                '!==' => $value1 !== $value2,
                '!=' => $value1 != $value2,
                '>' => $value1 > $value2,
                '>=' => $value1 >= $value2,
                '<' => $value1 < $value2,
                '<=' => $value1 <= $value2,
                default => false,
            };
            
            if (!$isValid) {
                $this->errors['_class'][] = $rule->message;
            }
        }
    }
    
    public function getErrors(): array
    {
        return $this->errors;
    }
}

实际模型示例

<?php

#[CompareFields('password', 'confirmPassword', message: 'Passwords do not match')]
class UserRegistrationRequest
{
    #[Required]
    #[Length(min: 3, max: 50)]
    #[Pattern(
        regex: '/^[a-zA-Z0-9_]+$/',
        message: 'Username can only contain letters, numbers, and underscores'
    )]
    public string $username;
    
    #[Required]
    #[Email(checkDNS: true)]
    #[Unique(table: 'users', column: 'email')]
    public string $email;
    
    #[Required]
    #[Length(min: 8, max: 100)]
    #[Pattern(
        regex: '/^(?=.*[a-z])(?=.*[A-Z])(?=.*d)(?=.*[@$!%*?&])[A-Za-zd@$!%*?&]/',
        message: 'Password must contain uppercase, lowercase, number, and special character'
    )]
    public string $password;
    
    #[Required]
    public string $confirmPassword;
    
    #[Required]
    #[Length(min: 2, max: 100)]
    public string $firstName;
    
    #[Required]
    #[Length(min: 2, max: 100)]
    public string $lastName;
    
    #[Date(format: 'Y-m-d')]
    public ?string $birthDate = null;
    
    #[Url(allowedSchemes: ['https'])]
    public ?string $website = null;
    
    #[Pattern(regex: '/^+?[1-9]d{1,14}$/', message: 'Invalid phone number')]
    public ?string $phone = null;
    
    #[InArray(allowedValues: ['male', 'female', 'other', 'prefer_not_to_say'])]
    public ?string $gender = null;
    
    #[Range(min: 18, max: 120)]
    public ?int $age = null;
}

class PaymentRequest
{
    #[Required]
    #[Range(min: 0.01, max: 999999.99)]
    public float $amount;
    
    #[Required]
    #[InArray(allowedValues: ['USD', 'EUR', 'GBP', 'JPY'])]
    public string $currency;
    
    #[Required]
    #[CreditCard(types: ['visa', 'mastercard'])]
    public string $cardNumber;
    
    #[Required]
    #[Pattern(regex: '/^d{3,4}$/', message: 'Invalid CVV')]
    public string $cvv;
    
    #[Required]
    #[Pattern(regex: '/^(0[1-9]|1[0-2])/d{2}$/', message: 'Invalid expiry date (MM/YY)')]
    public string $expiryDate;
    
    #[Required]
    #[Length(min: 2, max: 100)]
    public string $cardholderName;
    
    #[Email]
    public ?string $receiptEmail = null;
}

在控制器中使用验证

<?php

class RegistrationController
{
    private EnhancedValidator $validator;
    
    public function __construct()
    {
        $this->validator = new EnhancedValidator();
    }
    
    #[Route('/register', method: 'POST')]
    public function register(#[FromBody] UserRegistrationRequest $request): array
    {
        if (!$this->validator->validate($request)) {
            http_response_code(422);
            return [
                'success' => false,
                'errors' => $this->validator->getErrors(),
                'message' => 'Validation failed'
            ];
        }
        
        // Process registration
        $user = $this->createUser($request);
        
        return [
            'success' => true,
            'user' => $user,
            'message' => 'Registration successful'
        ];
    }
    
    private function createUser(UserRegistrationRequest $request): array
    {
        // Implementation
        return [
            'id' => 1,
            'username' => $request->username,
            'email' => $request->email,
        ];
    }
}

进阶模式和最佳实践

Attribute 组合

创建可重用的 Attribute 组:

<?php

#[Attribute(Attribute::TARGET_PROPERTY)]
class StrongPassword
{
    public static function getRules(): array
    {
        return [
            new Required('Password is required'),
            new Length(min: 8, max: 100),
            new Pattern(
                regex: '/^(?=.*[a-z])(?=.*[A-Z])(?=.*d)(?=.*[@$!%*?&])/',
                message: 'Password must meet complexity requirements'
            ),
        ];
    }
}

#[Attribute(Attribute::TARGET_PROPERTY)]
class PersonName
{
    public static function getRules(): array
    {
        return [
            new Required(),
            new Length(min: 2, max: 100),
            new Pattern(
                regex: '/^[a-zA-Zs'-]+$/',
                message: 'Name contains invalid characters'
            ),
        ];
    }
}

缓存 Reflection 数据

通过缓存 Attribute 元数据来提升性能:

<?php

class AttributeCache
{
    private static array $cache = [];
    
    public static function getAttributes(string $className, string $attributeType): array
    {
        $key = "{$className}::{$attributeType}";
        
        if (!isset(self::$cache[$key])) {
            self::$cache[$key] = AttributeProcessor::getClassAttributes(
                $className,
                $attributeType
            );
        }
        
        return self::$cache[$key];
    }
    
    public static function getMethodAttributes(
        string $className,
        string $method,
        string $attributeType
    ): array {
        $key = "{$className}::{$method}::{$attributeType}";
        
        if (!isset(self::$cache[$key])) {
            $reflection = new ReflectionMethod($className, $method);
            self::$cache[$key] = array_map(
                fn($attr) => $attr->newInstance(),
                $reflection->getAttributes($attributeType, ReflectionAttribute::IS_INSTANCEOF)
            );
        }
        
        return self::$cache[$key];
    }
    
    public static function clear(): void
    {
        self::$cache = [];
    }
}

Attribute 驱动的依赖注入

<?php

#[Attribute(Attribute::TARGET_PROPERTY)]
class Inject
{
    public function __construct(
        public ?string $service = null
    ) {}
}

class Container
{
    private array $services = [];
    
    public function register(string $name, callable $factory): void
    {
        $this->services[$name] = $factory;
    }
    
    public function resolve(string $className): object
    {
        $reflection = new ReflectionClass($className);
        $instance = $reflection->newInstanceWithoutConstructor();
        
        foreach ($reflection->getProperties() as $property) {
            $attributes = $property->getAttributes(Inject::class);
            
            if (!empty($attributes)) {
                $inject = $attributes[0]->newInstance();
                $serviceName = $inject->service ?? $property->getType()->getName();
                
                if (isset($this->services[$serviceName])) {
                    $service = $this->services[$serviceName]($this);
                    $property->setAccessible(true);
                    $property->setValue($instance, $service);
                }
            }
        }
        
        return $instance;
    }
}

// Usage
class OrderService
{
    #[Inject]
    private DatabaseConnection $db;
    
    #[Inject(service: 'mailer')]
    private EmailService $emailService;
    
    #[Inject]
    private LoggerInterface $logger;
    
    public function createOrder(array $data): Order
    {
        $this->logger->info('Creating order');
        // Implementation
        return new Order();
    }
}

性能优化技巧

  1. 缓存 Reflection 结果:Reflection 开销较大;缓存 Attribute 元数据
  2. 使用 IS_INSTANCEOF:搜索 Attributes 时使用继承标志
  3. 延迟加载:仅在需要时处理 Attributes
  4. 编译路由:在生产环境中预编译路由表
  5. 避免深度嵌套:保持 Attribute 处理浅层化

Attributes 与替代方案对比

特性AttributesDocblock 注解配置文件
类型安全 编译时验证 字符串解析️ 部分支持
IDE 支持 完整支持️ 有限支持️ 有限支持
性能 OPcache 缓存️ 需要解析 可缓存
可读性 声明式️ 注释混杂 分离代码
灵活性 高度灵活️ 有限 灵活

结语

PHP Attributes 代表了元数据编程的范式转变。它们提供:

  • 类型安全:编译时验证 Attribute 使用
  • 简洁语法:声明式、可读的代码
  • IDE 支持:完整的自动补全和重构
  • 性能:解析一次,由 opcache 缓存
  • 灵活性:适用于类、方法、属性、参数

通过掌握自定义 Attributes、Reflection API、基于 Attribute 的路由和验证模式,你可以解锁强大的架构模式,使 PHP 应用更易维护、可测试且优雅。

本文中的示例展示了可用于生产的实现,你可以根据具体需求进行调整。从验证 Attributes 开始,然后逐步采用路由以及日志和缓存等横切关注点。

记住:Attributes 是元数据——它们描述你的代码,但不能替代良好的设计。使用它们来减少样板代码并增强表达力,而不是作为清晰架构的替代品。

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:alixiixcom@163.com