Skip to content

反射读取注解

概述

PHP 8.0 的反射 API 为注解提供了完整的支持。通过 ReflectionClassReflectionMethodReflectionPropertyReflectionFunctionReflectionParameter 等反射类的 getAttributes() 方法,可以在运行时读取代码结构上的注解信息。本章将详细讲解 ReflectionAttribute 的使用方法。

核心 API

  • getAttributes() — 获取所有注解
  • getAttributes(Name::class) — 获取指定类型的注解
  • ReflectionAttribute::newInstance() — 实例化注解类
  • ReflectionAttribute::getArguments() — 获取注解参数

基础概念

ReflectionAttribute

ReflectionAttribute 是反射 API 中表示注解的对象。它提供了注解的名称、参数和实例化方法。

getAttributes() 方法

所有反射类(ReflectionClassReflectionMethodReflectionProperty 等)都提供了 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 有一定的性能开销。在热路径(频繁执行的代码)中,应该缓存反射结果。

最佳实践

  1. 使用特定类型过滤:使用 getAttributes(SpecificAttribute::class) 而非 getAttributes() 后手动过滤。
  2. 缓存反射结果:在性能敏感场景中缓存注解解析结果。
  3. 处理缺失注解:在读取注解前检查是否存在。
  4. 使用接口过滤:当多个注解实现同一接口时,使用 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;
    }
}

参考链接