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

掌握 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 复制代码
<?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 复制代码
<?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 复制代码
<?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 复制代码
<?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 复制代码
<?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 复制代码
<?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 复制代码
<?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 复制代码
<?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 复制代码
<?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 复制代码
<?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 复制代码
<?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 复制代码
<?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 复制代码
<?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-z\d@$!%*?&]/',
        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 复制代码
<?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 复制代码
<?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-Z\s\'-]+$/',
                message: 'Name contains invalid characters'
            ),
        ];
    }
}

缓存 Reflection 数据

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

php 复制代码
<?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 复制代码
<?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 与替代方案对比

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

结语

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

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

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

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

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

相关推荐
上进小菜猪9 小时前
面向课堂与自习场景的智能坐姿识别系统——从行为感知到可视化部署的完整工程【YOLOv8】
后端
BestAns10 小时前
一文带你吃透 Java 反射机制
java·后端
2501_9167665410 小时前
【Springboot】数据层开发-数据源自动管理
java·spring boot·后端
半夏知半秋10 小时前
docker常用指令整理
运维·笔记·后端·学习·docker·容器
程序员码歌10 小时前
短思考第263天,每天复盘10分钟,胜过盲目努力一整年
android·前端·后端
软件管理系统10 小时前
基于Spring Boot的便民维修管理系统
java·spring boot·后端
小波小波轩然大波11 小时前
openstack总结
windows·php·openstack
源代码•宸11 小时前
Leetcode—620. 有趣的电影&&Q3. 有趣的电影【简单】
数据库·后端·mysql·算法·leetcode·职场和发展
百***787511 小时前
Step-Audio-2 轻量化接入全流程详解
android·java·gpt·php·llama