反射读取注解
概述
PHP 8.0 的反射 API 为注解提供了完整的支持。通过 ReflectionClass、ReflectionMethod、ReflectionProperty、ReflectionFunction、ReflectionParameter 等反射类的 getAttributes() 方法,可以在运行时读取代码结构上的注解信息。本章将详细讲解 ReflectionAttribute 的使用方法。
核心 API
getAttributes()— 获取所有注解getAttributes(Name::class)— 获取指定类型的注解ReflectionAttribute::newInstance()— 实例化注解类ReflectionAttribute::getArguments()— 获取注解参数
基础概念
ReflectionAttribute
ReflectionAttribute 是反射 API 中表示注解的对象。它提供了注解的名称、参数和实例化方法。
getAttributes() 方法
所有反射类(ReflectionClass、ReflectionMethod、ReflectionProperty 等)都提供了 getAttributes() 方法,返回 ReflectionAttribute 数组。
newInstance() 与 getArguments()
newInstance() 创建注解类的实例(调用其构造函数)。getArguments() 返回传递给注解的原始参数数组。
语法与代码
获取类上的注解
php
<?php
declare(strict_types=1);
use Attribute;
#[Attribute]
class Table
{
public function __construct(public readonly string $name) {}
}
#[Table(name: 'users')]
class User {}
// 反射读取注解
$refClass = new \ReflectionClass(User::class);
$attrs = $refClass->getAttributes();
foreach ($attrs as $attr) {
echo "注解名称: " . $attr->getName() . "\n";
echo "注解参数: " . json_encode($attr->getArguments()) . "\n";
$instance = $attr->newInstance();
echo "表名: " . $instance->name . "\n";
}
// 注解名称: Table
// 注解参数: ["name","users"]
// 表名: users获取指定类型的注解
php
<?php
declare(strict_types=1);
use Attribute;
#[Attribute(Attribute::TARGET_METHOD)]
class Route
{
public function __construct(
public readonly string $path,
public readonly string $method = 'GET'
) {}
}
#[Attribute(Attribute::TARGET_METHOD)]
class Middleware
{
public function __construct(public readonly string $name) {}
}
class UserController
{
#[Route(path: '/users', method: 'GET')]
#[Middleware(name: 'auth')]
public function list(): array { return []; }
#[Route(path: '/users/{id}', method: 'GET')]
public function show(int $id): array { return []; }
}
// 只获取 Route 类型的注解
$refMethod = new \ReflectionMethod(UserController::class, 'list');
$routeAttrs = $refMethod->getAttributes(Route::class);
foreach ($routeAttrs as $attr) {
$route = $attr->newInstance();
echo "路由: {$route->method} {$route->path}\n";
// 路由: GET /users
}newInstanceArgs() 方法
php
<?php
declare(strict_types=1);
use Attribute;
#[Attribute]
class Config
{
public function __construct(public readonly array $values) {}
}
#[Config(values: ['host' => 'localhost', 'port' => 3306])]
class DbConfig {}
$ref = new \ReflectionClass(DbConfig::class);
$attrs = $ref->getAttributes(Config::class);
$attr = $attrs[0];
$instance = $attr->newInstance();
// $instance->values = ['host' => 'localhost', 'port' => 3306]
print_r($instance->values);获取方法上的注解
php
<?php
declare(strict_types=1);
use Attribute;
#[Attribute(Attribute::TARGET_METHOD)]
class Cache
{
public function __construct(
public readonly int $ttl = 3600,
public readonly string $key = ''
) {}
}
class ProductService
{
#[Cache(ttl: 600, key: 'products:list')]
public function listAll(): array { return []; }
#[Cache(ttl: 3600, key: 'products:detail')]
public function findById(int $id): ?array { return null; }
}
// 遍历所有方法,找出有 Cache 注解的方法
$refClass = new \ReflectionClass(ProductService::class);
foreach ($refClass->getMethods() as $method) {
$cacheAttrs = $method->getAttributes(Cache::class);
foreach ($cacheAttrs as $attr) {
$cache = $attr->newInstance();
echo "方法 {$method->getName()}: TTL={$cache->ttl}s, Key={$cache->key}\n";
}
}
// 方法 listAll: TTL=600s, Key=products:list
// 方法 findById: TTL=3600s, Key=products:detail获取属性上的注解
php
<?php
declare(strict_types=1);
use Attribute;
#[Attribute(Attribute::TARGET_PROPERTY)]
class Column
{
public function __construct(
public readonly string $type = 'string',
public readonly bool $nullable = false
) {}
}
class Article
{
#[Column(type: 'integer', nullable: false)]
public int $id;
#[Column(type: 'string', nullable: true)]
public ?string $title;
#[Column(type: 'text')]
public string $content;
}
$refClass = new \ReflectionClass(Article::class);
foreach ($refClass->getProperties() as $property) {
$colAttrs = $property->getAttributes(Column::class);
foreach ($colAttrs as $attr) {
$col = $attr->newInstance();
echo "{$property->getName()}: type={$col->type}, nullable=" . ($col->nullable ? 'true' : 'false') . "\n";
}
}详细说明
ReflectionAttribute 方法一览
| 方法 | 说明 | 返回类型 |
|---|---|---|
getName() | 获取注解类名 | string |
getArguments() | 获取注解参数(原始数组) | array |
newInstance() | 实例化注解类 | object |
newInstanceArgs(array) | 使用指定参数实例化(PHP 8.4+ 弃用) | object |
注解过滤器
getAttributes() 可以接受第二个参数(过滤器标志),如 ReflectionAttribute::IS_INSTANCEOF。
php
<?php
declare(strict_types=1);
use Attribute;
interface ValidateRule {}
#[Attribute(Attribute::TARGET_PROPERTY)]
class Required implements ValidateRule
{
public string $message = '此字段为必填项';
}
#[Attribute(Attribute::TARGET_PROPERTY)]
class MaxLength implements ValidateRule
{
public function __construct(public readonly int $value) {}
}
// getAttributes 支持接口过滤
class FormRequest
{
#[Required]
#[MaxLength(value: 100)]
public string $name;
}
$ref = new \ReflectionProperty(FormRequest::class, 'name');
$validateAttrs = $ref->getAttributes(
ValidateRule::class,
\ReflectionAttribute::IS_INSTANCEOF
);
foreach ($validateAttrs as $attr) {
echo $attr->getName() . "\n";
}
// Required
// MaxLength性能考虑
反射读取注解有一定的性能开销。在性能敏感场景中,应该缓存反射结果。
php
<?php
declare(strict_types=1);
class AttributeCache
{
/** @var array<string, array> */
private static array $cache = [];
public static function getForClass(string $className): array
{
if (isset(self::$cache[$className])) {
return self::$cache[$className];
}
$refClass = new \ReflectionClass($className);
$result = [];
foreach ($refClass->getAttributes() as $attr) {
$result[] = [
'name' => $attr->getName(),
'instance' => $attr->newInstance(),
];
}
self::$cache[$className] = $result;
return $result;
}
}实战示例
实战:注解驱动的路由解析器
php
<?php
declare(strict_types=1);
use Attribute;
#[Attribute(Attribute::TARGET_METHOD)]
class Route
{
public function __construct(
public readonly string $path,
public readonly string $method = 'GET'
) {}
}
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class Middleware
{
public function __construct(public readonly string $name) {}
}
class Router
{
/** @var array<string, array{method: string, handler: string, middleware: string[]}> */
private array $routes = [];
public function register(string $controllerClass): void
{
$refClass = new \ReflectionClass($controllerClass);
$prefix = $this->getControllerPrefix($refClass);
foreach ($refClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
$routeAttrs = $method->getAttributes(Route::class);
$middlewareAttrs = $method->getAttributes(Middleware::class);
foreach ($routeAttrs as $attr) {
$route = $attr->newInstance();
$middlewares = array_map(
fn(\ReflectionAttribute $a) => $a->newInstance()->name,
$middlewareAttrs
);
$fullPath = $prefix . $route->path;
$this->routes[$fullPath] = [
'method' => $route->method,
'handler' => "{$controllerClass}::{$method->getName()}",
'middleware' => $middlewares,
];
}
}
}
private function getControllerPrefix(\ReflectionClass $refClass): string
{
$routeAttrs = $refClass->getAttributes(Route::class);
if (!empty($routeAttrs)) {
return $routeAttrs[0]->newInstance()->path;
}
return '';
}
public function dispatch(string $method, string $path): ?array
{
foreach ($this->routes as $routePath => $info) {
if ($info['method'] === $method && $routePath === $path) {
return $info;
}
}
return null;
}
}注意事项
newInstance() 可能抛出异常
如果注解类的构造函数抛出异常,newInstance() 会传播该异常。
不要滥用反射
反射 API 有一定的性能开销。在热路径(频繁执行的代码)中,应该缓存反射结果。
最佳实践
- 使用特定类型过滤:使用
getAttributes(SpecificAttribute::class)而非getAttributes()后手动过滤。 - 缓存反射结果:在性能敏感场景中缓存注解解析结果。
- 处理缺失注解:在读取注解前检查是否存在。
- 使用接口过滤:当多个注解实现同一接口时,使用
IS_INSTANCEOF过滤。
php
<?php
declare(strict_types=1);
function getRouteInfo(string $class, string $method): ?object
{
$ref = new \ReflectionMethod($class, $method);
$attrs = $ref->getAttributes(Route::class);
if (empty($attrs)) {
return null;
}
return $attrs[0]->newInstance();
}反射读取注解的高级用法
获取类上的所有注解
php
<?php
declare(strict_types=1);
use Attribute;
use ReflectionClass;
#[Attribute]
readonly class Table { public function __construct(public string $name = '') {} }
#[Attribute(Attribute::IS_REPEATABLE)]
readonly class Index {
public function __construct(public string $column, public string $type = 'BTREE') {}
}
#[Table(name: 'users')]
#[Index(column: 'email', type: 'UNIQUE')]
#[Index(column: 'username')]
class UserEntity {}
$refClass = new ReflectionClass(UserEntity::class);
// 获取所有注解
foreach ($refClass->getAttributes() as $attr) {
echo "注解类: " . $attr->getName() . "\n";
echo "参数: " . json_encode($attr->getArguments(), JSON_UNESCAPED_UNICODE) . "\n";
echo "\n";
}getAttributes 的过滤
php
<?php
declare(strict_types=1);
// 只获取特定类的注解
$tableAttrs = $refClass->getAttributes(Table::class);
$indexAttrs = $refClass->getAttributes(Index::class);
// 使用 getAttribute (PHP 8.0+) 获取单个注解
$tableAttr = $refClass->getAttribute(Table::class);
if ($tableAttr !== null) {
$table = $tableAttr->newInstance();
echo "表名: " . $table->name . "\n";
}方法参数上的注解
php
<?php
declare(strict_types=1);
use Attribute;
use ReflectionMethod;
use ReflectionParameter;
#[Attribute(Attribute::TARGET_PARAMETER)]
readonly class Validate
{
public function __construct(
public string $rule = '',
public string $message = '',
) {}
}
class RequestHandler
{
public function handle(
#[Validate(rule: 'required|min:3', message: '用户名必填且至少3字符')]
string $username,
#[Validate(rule: 'required|email', message: '邮箱格式不正确')]
string $email,
): void {}
}
$refMethod = new ReflectionMethod(RequestHandler::class, 'handle');
foreach ($refMethod->getParameters() as $param) {
$validates = $param->getAttributes(Validate::class);
foreach ($validates as $validateAttr) {
$validate = $validateAttr->newInstance();
echo "\${$param->getName()}: {$validate->rule} ({$validate->message})\n";
}
}类常量上的注解
php
<?php
declare(strict_types=1);
use Attribute;
use ReflectionClassConstant;
#[Attribute(Attribute::TARGET_CLASS_CONSTANT)]
readonly class EnumValue
{
public function __construct(public string $label = '') {}
}
class OrderStatus
{
#[EnumValue(label: '待支付')]
public const PENDING = 'pending';
#[EnumValue(label: '已支付')]
public const PAID = 'paid';
#[EnumValue(label: '已取消')]
public const CANCELLED = 'cancelled';
}
$refClass = new ReflectionClass(OrderStatus::class);
foreach ($refClass->getReflectionConstants() as $const) {
$enumAttrs = $const->getAttributes(EnumValue::class);
if (!empty($enumAttrs)) {
$enum = $enumAttrs[0]->newInstance();
echo "{$const->getName()} = {$const->getValue()} -> {$enum->label}\n";
}
}注解缓存策略
框架中通常会在启动时缓存注解解析结果,避免每次请求都进行反射操作:
php
<?php
declare(strict_types=1);
class AttributeCache
{
/** @var array<string, array> */
private static array $cache = [];
public static function getForClass(string $className, string $attributeClass): ?object
{
if (isset(self::$cache[$className][$attributeClass])) {
return self::$cache[$className][$attributeClass];
}
$ref = new \ReflectionClass($className);
$attrs = $ref->getAttributes($attributeClass);
if (empty($attrs)) {
self::$cache[$className][$attributeClass] = null;
return null;
}
$instance = $attrs[0]->newInstance();
self::$cache[$className][$attributeClass] = $instance;
return $instance;
}
}