PHP 8.0 引入了原生 Attributes(以前称为注解),彻底改变了我们编写声明式代码的方式。Attributes 实现了优雅的元数据驱动编程,用结构化、类型安全的声明替代了 docblock 注解。本综合指南将探讨自定义 Attributes、Reflection API 集成、基于 Attribute 的路由以及验证系统。
PHP Attributes 是用 #[Attribute] 标记的特殊类,可以附加到类、方法、属性、参数和常量上。与注释不同,它们是 AST(抽象语法树)的一部分,可通过 Reflection 访问。
让我们为企业应用构建一个全面的日志 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,支持复杂规则:
<?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();
}
}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;
}
}这是使用 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(),
]);
}
}<?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');<?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
}
}<?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-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
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 组:
<?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'
),
];
}
}通过缓存 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 = [];
}
}<?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();
}
}| 特性 | Attributes | Docblock 注解 | 配置文件 |
|---|---|---|---|
| 类型安全 | ✅ 编译时验证 | ❌ 字符串解析 | ⚠️ 部分支持 |
| IDE 支持 | ✅ 完整支持 | ⚠️ 有限支持 | ⚠️ 有限支持 |
| 性能 | ✅ OPcache 缓存 | ⚠️ 需要解析 | ✅ 可缓存 |
| 可读性 | ✅ 声明式 | ⚠️ 注释混杂 | ❌ 分离代码 |
| 灵活性 | ✅ 高度灵活 | ⚠️ 有限 | ✅ 灵活 |
PHP Attributes 代表了元数据编程的范式转变。它们提供:
通过掌握自定义 Attributes、Reflection API、基于 Attribute 的路由和验证模式,你可以解锁强大的架构模式,使 PHP 应用更易维护、可测试且优雅。
本文中的示例展示了可用于生产的实现,你可以根据具体需求进行调整。从验证 Attributes 开始,然后逐步采用路由以及日志和缓存等横切关注点。
记住:Attributes 是元数据——它们描述你的代码,但不能替代良好的设计。使用它们来减少样板代码并增强表达力,而不是作为清晰架构的替代品。