Skip to content

声明注解类

概述

声明注解类是使用注解机制的核心步骤。通过 #[Attribute] 标记一个类为注解类,可以控制该注解的使用范围(目标限制)、是否可重复等行为。本章将详细讲解如何声明注解类,包括 Attribute 类的使用、IS_REPEATABLE 标志、TARGET_* 常量,以及注解的继承行为。

关键步骤

  1. 创建类并添加 #[Attribute] 标记
  2. 定义构造函数接收参数
  3. 指定目标和可重复性

基础概念

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) — 可重复

注解类的继承

注解本身不支持继承,但注解类可以是其他类的子类(不过这种用法不常见)。

注解类的约束

  1. 注解类必须是可以实例化的(不能是抽象类或接口)
  2. 注解类通常应该使用 readonly 属性(PHP 8.2+)
  3. 注解类的构造函数参数应该是 public readonly
  4. 注解类不应该有复杂的方法(保持简单)

实战示例

实战:完整的 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 {}

最佳实践

  1. 使用 readonly class(PHP 8.2+):将注解类声明为 readonly class,确保完全不可变。
  2. 命名空间分离:注解类放在 App\Attributes 命名空间中。
  3. 构造函数参数验证:在构造函数中验证参数的合法性。
  4. 单一职责:每个注解类只负责一种元数据。
  5. 使用命名参数:推荐使用命名参数调用注解,提高可读性。
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 读取到。

参考链接