Skip to content

ReflectionAttribute

概述

ReflectionAttribute 是 PHP 8.0 引入的反射类,用于在运行时读取代码结构(类、方法、属性、函数、参数等)上的注解(Attributes)。通过 ReflectionAttribute,你可以获取注解的名称、参数,并实例化为真正的注解对象。

核心方法

getAttributes() — 获取所有注解,newInstance() — 实例化注解对象,getArguments() — 获取原始参数。

基础概念

获取注解反射对象

通过 getAttributes() 方法从 ReflectionClassReflectionMethodReflectionProperty 等获取 ReflectionAttribute 实例。

注解实例化

newInstance() 调用注解类的构造函数,创建注解对象实例。

参数映射

getArguments() 返回注解声明时的参数数组,与构造函数参数一一对应。

语法与代码

获取所有注解

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'
    ) {}
}

class PostController
{
    #[Route(path: '/posts', method: 'GET')]
    public function index(): array { return []; }

    #[Route(path: '/posts', method: 'POST')]
    public function store(): array { return []; }
}

$refClass = new \ReflectionClass(PostController::class);

foreach ($refClass->getMethods() as $method) {
    $attrs = $method->getAttributes();
    foreach ($attrs as $attr) {
        echo "方法 {$method->getName()} -> 注解: " . $attr->getName() . "\n";
        echo "  参数: " . json_encode($attr->getArguments()) . "\n";
    }
}

获取指定类型的注解

php
<?php
declare(strict_types=1);

$refMethod = new \ReflectionMethod(PostController::class, 'index');
$routeAttrs = $refMethod->getAttributes(Route::class);

foreach ($routeAttrs as $attr) {
    $route = $attr->newInstance();
    echo "路径: {$route->path}, 方法: {$route->method}\n";
}
// 路径: /posts, 方法: GET

getArguments 与 newInstance 对比

php
<?php
declare(strict_types=1);

$refMethod = new \ReflectionMethod(PostController::class, 'store');
$attrs = $refMethod->getAttributes(Route::class);
$attr = $attrs[0];

// getArguments() — 返回原始参数数组
$args = $attr->getArguments();
print_r($args);
// Array ( [path] => /posts [method] => POST )

// newInstance() — 创建注解实例
$instance = $attr->newInstance();
echo $instance->path;   // /posts
echo $instance->method; // POST

接口过滤

php
<?php
declare(strict_types=1);

interface ValidationRule {}

#[Attribute(Attribute::TARGET_PROPERTY)]
class Required implements ValidationRule
{
    public string $message = '此字段为必填项';
}

#[Attribute(Attribute::TARGET_PROPERTY)]
class MaxLength implements ValidationRule
{
    public function __construct(public readonly int $value) {}
}

class UserForm
{
    #[Required]
    #[MaxLength(value: 50)]
    public string $username;
}

$refProp = new \ReflectionProperty(UserForm::class, 'username');

// 使用 IS_INSTANCEOF 获取所有实现 ValidationRule 的注解
$rules = $refProp->getAttributes(
    ValidationRule::class,
    \ReflectionAttribute::IS_INSTANCEOF
);

foreach ($rules as $rule) {
    echo $rule->getName() . "\n";
    // Required
    // MaxLength
}

获取类、方法、属性上的注解

php
<?php
declare(strict_types=1);

#[Attribute(Attribute::TARGET_CLASS)]
class Table { public function __construct(public readonly string $name) {} }

#[Attribute(Attribute::TARGET_PROPERTY)]
class Column { public function __construct(public readonly string $type) {} }

#[Table(name: 'products')]
class Product
{
    #[Column(type: 'string')]
    public string $name;

    #[Column(type: 'integer')]
    public int $price;
}

// 类注解
$classAttrs = (new \ReflectionClass(Product::class))->getAttributes(Table::class);
$table = $classAttrs[0]->newInstance();
echo "表名: {$table->name}\n"; // products

// 属性注解
foreach ((new \ReflectionClass(Product::class))->getProperties() as $prop) {
    $colAttrs = $prop->getAttributes(Column::class);
    foreach ($colAttrs as $colAttr) {
        $col = $colAttr->newInstance();
        echo "{$prop->getName()}: {$col->type}\n";
    }
}

详细说明

ReflectionAttribute 方法

方法说明返回值
getName()获取注解类名string
getArguments()获取注解参数array
newInstance()实例化注解object
newInstanceArgs($args)使用自定义参数实例化object

getAttributes 的过滤器

php
<?php
declare(strict_types=1);

// 获取所有注解
$allAttrs = $ref->getAttributes();

// 获取指定类名的注解
$specificAttrs = $ref->getAttributes(Route::class);

// 获取实现指定接口的所有注解
$interfaceAttrs = $ref->getAttributes(
    SomeInterface::class,
    \ReflectionAttribute::IS_INSTANCEOF
);

实战示例

实战:注解驱动的验证框架

php
<?php
declare(strict_types=1);

use Attribute;

#[Attribute(Attribute::TARGET_PROPERTY)]
class Required {}

#[Attribute(Attribute::TARGET_PROPERTY)]
class Length
{
    public function __construct(
        public readonly int $min = 0,
        public readonly int $max = PHP_INT_MAX
    ) {}
}

#[Attribute(Attribute::TARGET_PROPERTY)]
class Email {}

class Validator
{
    public function validate(object $data): array
    {
        $errors = [];
        $refClass = new \ReflectionClass($data);

        foreach ($refClass->getProperties() as $prop) {
            $value = $prop->getValue($data);
            $propName = $prop->getName();

            foreach ($prop->getAttributes() as $attr) {
                $rule = $attr->newInstance();

                if ($rule instanceof Required && ($value === null || $value === '')) {
                    $errors[$propName][] = "{$propName} 是必填项";
                }

                if ($rule instanceof Length) {
                    $len = is_string($value) ? strlen($value) : 0;
                    if ($len < $rule->min) {
                        $errors[$propName][] = "{$propName} 至少需要 {$rule->min} 个字符";
                    }
                    if ($len > $rule->max) {
                        $errors[$propName][] = "{$propName} 最多 {$rule->max} 个字符";
                    }
                }

                if ($rule instanceof Email && !filter_var($value, FILTER_VALIDATE_EMAIL)) {
                    $errors[$propName][] = "{$propName} 不是有效的邮箱地址";
                }
            }
        }

        return $errors;
    }
}

注意事项

newInstance() 的异常传播

如果注解类的构造函数抛出异常,newInstance() 会传播该异常。

性能优化

反射读取注解有性能开销。建议在应用启动时一次性解析并缓存。

最佳实践

  1. 使用 getAttributes(Type::class):指定具体类型而非获取所有注解。
  2. 缓存注解实例:注解解析结果通常可以缓存。
  3. 异常处理:处理 newInstance() 可能抛出的异常。
php
<?php
declare(strict_types=1);

function getRouteInfo(string $class, string $method): ?object
{
    try {
        $ref = new \ReflectionMethod($class, $method);
        $attrs = $ref->getAttributes(Route::class);
        if (empty($attrs)) {
            return null;
        }
        return $attrs[0]->newInstance();
    } catch (\Throwable $e) {
        return null;
    }
}

ReflectionAttribute 高级用法

获取所有目标的注解

php
<?php
declare(strict_types=1);

use Attribute;

#[Attribute(Attribute::TARGET_ALL | Attribute::IS_REPEATABLE)]
readonly class Log
{
    public function __construct(
        public string $level = 'info',
        public string $message = '',
    ) {}
}

#[Log(level: 'info', message: 'Application started')]
#[Log(level: 'debug', message: 'Initializing components')]
class ApplicationService
{
    #[Log(level: 'info', message: 'Processing request')]
    public function handle(array $data): array
    {
        return $data;
    }

    #[Log(level: 'warning', message: 'Validation may fail')]
    #[Log(level: 'error', message: 'Exception possible')]
    public function validate(array $data): bool
    {
        return true;
    }
}

// 获取类上的所有注解
$refClass = new \ReflectionClass(ApplicationService::class);
$classAttrs = $refClass->getAttributes(Log::class);

echo "类注解数量: " . count($classAttrs) . "\n";
foreach ($classAttrs as $attr) {
    $log = $attr->newInstance();
    echo "  [{$log->level}] {$log->message}\n";
}

// 获取方法上的所有注解
foreach ($refClass->getMethods() as $method) {
    $methodAttrs = $method->getAttributes(Log::class);
    if (!empty($methodAttrs)) {
        echo "方法 {$method->getName()} 的注解:\n";
        foreach ($methodAttrs as $attr) {
            $log = $attr->newInstance();
            echo "  [{$log->level}] {$log->message}\n";
        }
    }
}

getArguments 和 newInstanceArgs

getArguments() 返回原始参数数组,newInstanceArgs() 允许传递额外参数:

php
<?php
declare(strict_types=1);

use Attribute;

#[Attribute(Attribute::TARGET_METHOD)]
class RateLimit
{
    public function __construct(
        public int $maxRequests = 60,
        public int $perSeconds = 60,
    ) {}
}

class ApiController
{
    #[RateLimit(maxRequests: 100, perSeconds: 60)]
    public function list(): array { return []; }
}

$method = new \ReflectionMethod(ApiController::class, 'list');
$attrs = $method->getAttributes(RateLimit::class);
$attr = $attrs[0];

// 获取原始参数
$args = $attr->getArguments();
print_r($args);
// ['maxRequests' => 100, 'perSeconds' => 60]

// 创建实例(与 newInstance 相同)
$instance = $attr->newInstance();
echo "最大请求: {$instance->maxRequests}\n";
echo "每秒: {$instance->perSeconds}\n";

注解反射在框架路由中的应用

php
<?php
declare(strict_types=1);

use Attribute;

#[Attribute(Attribute::TARGET_METHOD)]
readonly class Route
{
    public function __construct(
        public string $path,
        public string $method = 'GET',
        public string $name = '',
    ) {}
}

class UserController
{
    #[Route(path: '/users', method: 'GET', name: 'user.list')]
    public function list(): array { return []; }

    #[Route(path: '/users/{id}', method: 'GET', name: 'user.show')]
    public function show(int $id): array { return []; }

    #[Route(path: '/users', method: 'POST', name: 'user.create')]
    public function create(): array { return []; }

    #[Route(path: '/users/{id}', method: 'DELETE', name: 'user.delete')]
    public function delete(int $id): void {}
}

// 路由收集器
function collectRoutes(string $controllerClass): array
{
    $routes = [];
    $refClass = new \ReflectionClass($controllerClass);

    foreach ($refClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
        $attrs = $method->getAttributes(Route::class);
        foreach ($attrs as $attr) {
            $route = $attr->newInstance();
            $routes[] = [
                'name' => $route->name,
                'path' => $route->path,
                'method' => $route->method,
                'handler' => "{$controllerClass}::{$method->getName()}",
                'parameters' => array_map(
                    fn(\ReflectionParameter $p) => $p->getName(),
                    $method->getParameters()
                ),
            ];
        }
    }

    return $routes;
}

print_r(collectRoutes(UserController::class));

常见误区与 FAQ

getAttributes 可以不传类名参数吗?

可以。不传类名时返回所有注解。传入类名时只返回匹配的注解。传入多个类名可以通过数组形式传入。

newInstance 和 newInstanceArgs 有什么区别?

  • newInstance() 使用注解的原始参数创建实例
  • newInstanceArgs($args) 使用传入的参数创建实例

通常使用 newInstance() 即可。

如何获取注解的声明位置?

ReflectionAttribute::getTarget() 返回注解的目标类型(类、方法、属性、参数等)。

参考链接