声明注解类
概述
声明注解类是使用注解机制的核心步骤。通过 #[Attribute] 标记一个类为注解类,可以控制该注解的使用范围(目标限制)、是否可重复等行为。本章将详细讲解如何声明注解类,包括 Attribute 类的使用、IS_REPEATABLE 标志、TARGET_* 常量,以及注解的继承行为。
关键步骤
- 创建类并添加
#[Attribute]标记 - 定义构造函数接收参数
- 指定目标和可重复性
基础概念
Attribute 类
Attribute 是 PHP 内置的类,用作"注解类的标记"。只有被 #[Attribute] 标记的类才能用作注解。
Attribute::IS_REPEATABLE
IS_REPEATABLE 标志允许同一个注解在同一目标上多次使用。
Attribute::TARGET_*
TARGET_* 常量用于限制注解可以使用的声明位置。
语法与代码
基本注解类声明
php
<?php
declare(strict_types=1);
use Attribute;
// 最基本的注解类
#[Attribute]
class MyAnnotation
{
public function __construct(
public readonly string $value = ''
) {}
}指定目标限制
php
<?php
declare(strict_types=1);
use Attribute;
// 仅用于方法
#[Attribute(Attribute::TARGET_METHOD)]
class Route
{
public function __construct(public readonly string $path = '/') {}
}
// 仅用于类
#[Attribute(Attribute::TARGET_CLASS)]
class Entity
{
public function __construct(public readonly string $table = '') {}
}
// 用于类和方法
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
class Cacheable
{
public function __construct(public readonly int $ttl = 3600) {}
}可重复注解
php
<?php
declare(strict_types=1);
use Attribute;
// 声明可重复注解
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class Role
{
public function __construct(public readonly string $name) {}
}
// 使用可重复注解
class PermissionController
{
#[Role(name: 'admin')]
#[Role(name: 'editor')]
#[Role(name: 'viewer')]
public function manageUsers(): void {}
}使用枚举值作为注解参数
php
<?php
declare(strict_types=1);
use Attribute;
enum HttpMethod: string
{
case Get = 'GET';
case Post = 'POST';
case Put = 'PUT';
case Delete = 'DELETE';
}
#[Attribute(Attribute::TARGET_METHOD)]
class HttpRoute
{
public function __construct(
public readonly string $path,
public readonly HttpMethod $method = HttpMethod::Get
) {}
}
#[HttpRoute(path: '/users', method: HttpMethod::Get)]
class UserController
{
public function list(): array { return []; }
}构造函数中的验证
php
<?php
declare(strict_types=1);
use Attribute;
#[Attribute(Attribute::TARGET_PROPERTY)]
class Column
{
public function __construct(
public readonly string $type = 'string',
public readonly int $length = 255
) {
$validTypes = ['string', 'integer', 'float', 'boolean', 'text', 'datetime'];
if (!in_array($type, $validTypes, true)) {
throw new \InvalidArgumentException("无效的列类型: {$type}");
}
if ($length < 1) {
throw new \InvalidArgumentException("长度必须大于 0");
}
}
}
class Product
{
#[Column(type: 'string', length: 100)]
public string $name;
#[Column(type: 'text')]
public string $description;
}详细说明
Attribute 标志组合
#[Attribute] 可以接受以下标志的组合(使用位或 |):
Attribute::TARGET_CLASS (1) — 类、接口、trait、枚举
Attribute::TARGET_FUNCTION (2) — 函数
Attribute::TARGET_METHOD (4) — 方法
Attribute::TARGET_PROPERTY (8) — 属性
Attribute::TARGET_CLASS_CONSTANT (16) — 类常量
Attribute::TARGET_PARAMETER (32) — 参数
Attribute::TARGET_ALL (63) — 所有位置
Attribute::IS_REPEATABLE (64) — 可重复注解类的继承
注解本身不支持继承,但注解类可以是其他类的子类(不过这种用法不常见)。
注解类的约束
- 注解类必须是可以实例化的(不能是抽象类或接口)
- 注解类通常应该使用
readonly属性(PHP 8.2+) - 注解类的构造函数参数应该是
public readonly的 - 注解类不应该有复杂的方法(保持简单)
实战示例
实战:完整的 ORM 注解系统
php
<?php
declare(strict_types=1);
namespace App\ORM\Attributes;
use Attribute;
#[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 $name = null,
public readonly string $type = 'string',
public readonly bool $primary = false,
public readonly bool $autoIncrement = false,
public readonly bool $nullable = false
) {}
}
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
class Index
{
public function __construct(
public readonly string $name,
public readonly array $columns = []
) {}
}
// 使用
namespace App\Models;
use App\ORM\Attributes\Table;
use App\ORM\Attributes\Column;
use App\ORM\Attributes\Index;
#[Table(name: 'users')]
class User
{
#[Column(name: 'id', type: 'integer', primary: true, autoIncrement: true)]
public int $id;
#[Column(name: 'username', type: 'string', nullable: false)]
public string $username;
#[Column(name: 'email', type: 'string', nullable: false)]
public string $email;
#[Column(name: 'created_at', type: 'datetime', nullable: true)]
public ?string $createdAt = null;
#[Index(name: 'idx_email', columns: ['email'])]
public string $emailIndexed;
}实战:验证注解系统
php
<?php
declare(strict_types=1);
namespace App\Validation\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_PROPERTY)]
class Required {}
#[Attribute(Attribute::TARGET_PROPERTY)]
class Min
{
public function __construct(public readonly int $value) {}
}
#[Attribute(Attribute::TARGET_PROPERTY)]
class Max
{
public function __construct(public readonly int $value) {}
}
#[Attribute(Attribute::TARGET_PROPERTY)]
class Email {}
#[Attribute(Attribute::TARGET_PROPERTY)]
class Pattern
{
public function __construct(public readonly string $regex) {}
}
// 使用
namespace App\DTO;
use App\Validation\Attributes\{Required, Min, Max, Email, Pattern};
class RegistrationRequest
{
#[Required]
#[Min(value: 3)]
#[Max(value: 50)]
public string $username;
#[Required]
#[Email]
public string $email;
#[Required]
#[Min(value: 8)]
#[Pattern(regex: '/[A-Z]/')]
#[Pattern(regex: '/[0-9]/')]
public string $password;
#[Required]
#[Min(value: 18)]
#[Max(value: 120)]
public int $age;
}注意事项
错误目标上的注解
php
<?php
declare(strict_types=1);
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
class OnlyForClass {}
// #[OnlyForClass] // Error: 注解 OnlyForClass 只能用于类
// function test() {}非注解类作为注解
php
<?php
declare(strict_types=1);
class NotAnAttribute {}
// #[NotAnAttribute] // Error: NotAnAttribute 没有被 #[Attribute] 标记
class MyClass {}最佳实践
- 使用 readonly class(PHP 8.2+):将注解类声明为
readonly class,确保完全不可变。 - 命名空间分离:注解类放在
App\Attributes命名空间中。 - 构造函数参数验证:在构造函数中验证参数的合法性。
- 单一职责:每个注解类只负责一种元数据。
- 使用命名参数:推荐使用命名参数调用注解,提高可读性。
php
<?php
declare(strict_types=1);
namespace App\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD)]
readonly class Route
{
public function __construct(
public readonly string $path,
public readonly string $method = 'GET',
public readonly string $name = ''
) {}
}高级注解类设计
带验证的注解类
注解类可以有构造函数验证逻辑,确保参数的有效性:
php
<?php
declare(strict_types=1);
namespace App\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
readonly class CacheConfig
{
public function __construct(
public int $ttl = 3600,
public string $prefix = 'cache_',
public bool $public = false,
) {
if ($this->ttl < 0) {
throw new \InvalidArgumentException('TTL 不能为负数');
}
if ($this->ttl > 86400) {
throw new \InvalidArgumentException('TTL 不能超过 86400 秒');
}
if (!str_starts_with($this->prefix, 'cache_')) {
throw new \InvalidArgumentException('缓存前缀必须以 cache_ 开头');
}
}
}
// 使用
#[CacheConfig(ttl: 7200, prefix: 'cache_api_', public: true)]
class ApiService {}readonly 类
PHP 8.2+ 的 readonly class 非常适合注解类,确保注解一旦创建就不会被修改。
注解类的继承
注解类本身可以继承,子注解类继承父类的配置:
php
<?php
declare(strict_types=1);
namespace App\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD)]
class HttpEndpoint
{
public function __construct(
public string $path,
public array $methods = ['GET'],
) {}
}
#[Attribute(Attribute::TARGET_METHOD)]
class GetEndpoint extends HttpEndpoint
{
public function __construct(string $path)
{
parent::__construct($path, ['GET']);
}
}
#[Attribute(Attribute::TARGET_METHOD)]
class PostEndpoint extends HttpEndpoint
{
public function __construct(string $path)
{
parent::__construct($path, ['POST']);
}
}
// 使用
class ApiController
{
#[GetEndpoint('/users')]
public function listUsers(): array { return []; }
#[PostEndpoint('/users')]
public function createUser(): array { return []; }
}组合注解模式
通过注解类组合,实现更复杂的元数据描述:
php
<?php
declare(strict_types=1);
namespace App\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
readonly class Throttle
{
public function __construct(
public int $maxRequests = 60,
public int $perSeconds = 60,
) {}
}
#[Attribute(Attribute::TARGET_METHOD)]
readonly class ApiAction
{
public function __construct(
public string $version = 'v1',
public ?Throttle $throttle = null,
) {
$this->throttle ??= new Throttle();
}
}
// 使用
class OrderController
{
#[ApiAction(version: 'v2', throttle: new Throttle(maxRequests: 100, perSeconds: 60))]
public function list(): array { return []; }
}注解的反射读取实践
从注解中提取配置信息
php
<?php
declare(strict_types=1);
namespace App\Attributes;
use Attribute;
use ReflectionClass;
#[Attribute(Attribute::TARGET_CLASS)]
readonly class ServiceConfig
{
public function __construct(
public bool $singleton = true,
public string $alias = '',
) {}
}
#[ServiceConfig(singleton: false, alias: 'cache.service')]
class RedisCacheService {}
// 读取注解
function extractServiceConfig(string $className): array
{
$ref = new ReflectionClass($className);
$attrs = $ref->getAttributes(ServiceConfig::class);
if (empty($attrs)) {
return ['singleton' => true, 'alias' => ''];
}
$config = $attrs[0]->newInstance();
return [
'singleton' => $config->singleton,
'alias' => $config->alias,
];
}
print_r(extractServiceConfig(RedisCacheService::class));常见误区与 FAQ
注解类的构造函数可以有什么参数?
注解类的构造函数参数就是使用注解时可以传递的参数。所有参数都应该有默认值,或者在使用时提供。
IS_REPEATABLE 什么时候需要使用?
当一个目标(类、方法、属性等)上需要使用多个相同类型的注解时,必须设置 IS_REPEATABLE。否则 getAttributes() 只返回最后一个实例。
注解可以用于接口吗?
可以。只要接口不是 TARGET_* 约束排除的目标即可。接口方法上的注解可以通过子类的 ReflectionMethod 读取到。