Skip to content

注解概览

概述

注解(Attributes,也称属性)是 PHP 8.0 引入的特性,用于在类、方法、属性、函数等代码结构上添加结构化的元数据。注解是 PHP 原生的元数据机制,用于替代传统的 doc-block annotations(如 @ORM\Column@Route 等),提供更好的类型安全性和运行时反射支持。

PHP 版本

注解从 PHP 8.0 开始可用。本文基于 PHP 8.1+ 编写,包含后续版本新增的内置注解。

基础概念

什么是注解

注解是一种在声明处(类、方法、属性、函数、参数等)添加结构化元数据的方式。它使用 #[...] 语法,比 doc-block 注释中的 @tagName 更安全、更强大。

替代 doc-block annotations

在 PHP 8.0 之前,框架通过解析 doc-block 中的 @ 注释来获取元数据(如路由、验证规则、ORM 映射等)。这种方式有几个缺点:没有类型检查、容易拼写错误、IDE 支持有限。

#[Attribute] 语法

注解使用 #[...] 语法,可以放在声明语句的上一行。

反射读取

通过 PHP 的反射 API(Reflection API),可以在运行时读取类、方法、属性上的注解信息。

语法与代码

基本注解声明

php
<?php
declare(strict_types=1);

use Attribute;

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

// 使用注解
#[Route('/users', method: 'GET')]
class UserController
{
    #[Route('/users/{id}', method: 'GET')]
    public function show(int $id): string
    {
        return "User {$id}";
    }

    #[Route('/users', method: 'POST')]
    public function create(): string
    {
        return 'User created';
    }
}

注解的命名空间

php
<?php
declare(strict_types=1);

namespace App\Attributes;

use Attribute;

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

namespace App\Models;

use App\Attributes\Table;

#[Table(name: 'users')]
class User
{
    // ...
}

读取注解(反射)

php
<?php
declare(strict_types=1);

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

// 获取类上的所有注解
$classAttributes = $refClass->getAttributes();
foreach ($classAttributes as $attr) {
    echo "类注解: " . $attr->getName() . "\n";
    $instance = $attr->newInstance();
    echo "  路径: {$instance->path}\n";
    echo "  方法: {$instance->method}\n";
}

// 获取方法上的注解
$method = $refClass->getMethod('create');
$methodAttrs = $method->getAttributes();
foreach ($methodAttrs as $attr) {
    echo "方法注解: " . $attr->getName() . "\n";
}

多个注解

php
<?php
declare(strict_types=1);

use Attribute;

#[Attribute(Attribute::TARGET_METHOD)]
class HttpGet
{
    public function __construct(public readonly string $path = '') {}
}

#[Attribute(Attribute::TARGET_METHOD)]
class AuthRequired {}

class ProductController
{
    #[HttpGet('/products')]
    #[AuthRequired]
    public function list(): array
    {
        return ['product1', 'product2'];
    }
}

注解参数

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,
        public readonly ?string $default = null
    ) {}
}

class Product
{
    #[Column(type: 'integer', nullable: false)]
    public int $id;

    #[Column(type: 'string', nullable: true)]
    public ?string $name = null;

    #[Column(type: 'decimal', default: '0.00')]
    public string $price = '0.00';
}

详细说明

注解声明位置

注解可以放置在以下声明之前:

目标说明
类(CLASS)class, interface, trait, enum
方法(METHOD)类方法
函数(FUNCTION)独立函数
属性(PROPERTY)类属性/成员变量
常量(CLASS_CONSTANT)类常量
参数(PARAMETER)函数/方法参数
枚举 case(ENUM_CASE)PHP 8.1+ 枚举成员

注解 vs Doc-Block 注释

特性注解 #[...]Doc-Block @...
类型安全是(PHP 类)否(字符串)
运行时可用是(反射读取)需要解析注释
IDE 支持完整(自动完成、导航)有限
命名空间
参数验证构造函数类型检查
可继承可配置

注解的内部表示

每个注解在底层都是一个 PHP 类的实例。#[Route('/users')] 实际上是创建了 Route 类的一个实例,参数通过构造函数传递。

实战示例

实战:简单的路由系统

php
<?php
declare(strict_types=1);

namespace App\Framework;

use Attribute;

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

class Router
{
    /** @var array<string, array{method: string, class: string, method: string}> */
    private array $routes = [];

    public function registerController(string $className): void
    {
        $refClass = new \ReflectionClass($className);

        foreach ($refClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
            $attrs = $method->getAttributes(Route::class);

            foreach ($attrs as $attr) {
                $route = $attr->newInstance();
                $this->routes[$route->path] = [
                    'method' => $route->method,
                    'class' => $className,
                    'method' => $method->getName(),
                ];
            }
        }
    }

    public function match(string $path, string $httpMethod): ?array
    {
        foreach ($this->routes as $routePath => $routeInfo) {
            if ($routePath === $path && $routeInfo['method'] === $httpMethod) {
                return $routeInfo;
            }
        }
        return null;
    }
}

实战:验证注解

php
<?php
declare(strict_types=1);

use Attribute;

#[Attribute(Attribute::TARGET_PROPERTY)]
class Validate
{
    public function __construct(
        public readonly array $rules = []
    ) {}
}

class UserRequest
{
    #[Validate(rules: ['required', 'email'])]
    public string $email = '';

    #[Validate(rules: ['required', 'min:6'])]
    public string $password = '';

    #[Validate(rules: ['integer', 'min:0', 'max:150'])]
    public int $age = 0;
}

function validate(object $request): array
{
    $errors = [];
    $refClass = new \ReflectionClass($request);

    foreach ($refClass->getProperties() as $property) {
        $attrs = $property->getAttributes(Validate::class);
        foreach ($attrs as $attr) {
            $validate = $attr->newInstance();
            $value = $property->getValue($request);

            foreach ($validate->rules as $rule) {
                if ($rule === 'required' && ($value === '' || $value === null)) {
                    $errors[$property->getName()][] = "{$property->getName()} 是必填项";
                }
                if ($rule === 'email' && !str_contains($value, '@')) {
                    $errors[$property->getName()][] = "{$property->getName()} 不是有效邮箱";
                }
            }
        }
    }

    return $errors;
}

注意事项

注解类必须有 #[Attribute] 标记

php
<?php
declare(strict_types=1);

use Attribute;

// 正确:声明为注解类
#[Attribute]
class MyAnnotation {}

// 错误:没有 #[Attribute] 标记的类不能用作注解
// class NotAnAnnotation {}
// #[NotAnAnnotation] // Error: NotAnAnnotation is not an Attribute

注解不是注释

注解在运行时是可用的(通过反射),它们不是简单的注释。不要将敏感信息放在注解中。

最佳实践

  1. 使用命名空间:将注解类放在独立的命名空间中(如 App\Attributes)。
  2. 使用 readonly 属性:注解类的属性应该是 readonly 的,确保不可变。
  3. 声明目标限制:在 #[Attribute] 中指定注解可以使用的位置。
  4. 使用构造函数验证:在注解类的构造函数中进行参数验证。
  5. 缓存反射结果:注解的反射读取有一定开销,在性能敏感场景中应缓存结果。
php
<?php
declare(strict_types=1);

namespace App\Attributes;

use Attribute;

#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
readonly class Cache
{
    public function __construct(
        public int $ttl = 3600,
        public string $prefix = ''
    ) {
        if ($this->ttl < 0) {
            throw new \InvalidArgumentException('TTL 不能为负数');
        }
    }
}

注解与 Doc-Block Annotations 的对比

传统 Doc-Block 注解方式

在 PHP 8.0 之前,框架通常通过解析注释中的特殊标签来实现注解功能:

php
<?php
declare(strict_types=1);

/**
 * @Route("/api/users", methods={"GET"})
 * @Security("is_granted('ROLE_ADMIN')")
 * @Cache(public=true, maxage=3600)
 */
class UserController extends AbstractController
{
    /**
     * @param Request $request
     * @return JsonResponse
     * @Route("/{id}", requirements={"id": "\d+"})
     */
    public function show(int $id): JsonResponse
    {
        // ...
    }
}

PHP 8.0+ 原生注解方式

php
<?php
declare(strict_types=1);

use App\Attributes\Route;
use App\Attributes\Security;
use App\Attributes\Cache;

#[Route('/api/users', methods: ['GET'])]
#[Security('is_granted(\'ROLE_ADMIN\')')]
#[Cache(public: true, maxage: 3600)]
class UserController extends AbstractController
{
    #[Route('/{id}', requirements: ['id' => '\d+'])]
    public function show(int $id): JsonResponse
    {
        // ...
    }
}

对比总结

特性Doc-Block AnnotationsPHP 原生 Attributes
语法验证无(纯字符串)IDE 支持,语法高亮
类型安全无(需要手动解析)强类型,构造函数验证
性能需要正则解析注释原生反射 API
缓存支持需要框架实现反射结果可缓存
可维护性容易拼写错误IDE 重构支持
PHP 版本所有版本8.0+
命名空间不支持完整支持

注解在现代框架中的应用

Symfony 框架

Symfony 从 5.2 开始支持 PHP 8 原生注解,6.x 版本中已成为首选方式:

php
<?php
declare(strict_types=1);

use Symfony\Component\Routing\Attribute\Route;

#[Route('/api/products', name: 'product_list', methods: ['GET'])]
class ProductController
{
    #[Route('/{id}', name: 'product_show', requirements: ['id' => '\d+'])]
    public function show(int $id): Response
    {
        // ...
    }
}

Laravel 框架

Laravel 在 9.x 中通过 spatie/laravel-permission 等包支持注解,并在后续版本中逐渐增加原生支持。

Doctrine ORM

Doctrine 2.9+ 支持 PHP 8 原生注解作为映射元数据:

php
<?php
declare(strict_types=1);

use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ORM\Table(name: 'products')]
class Product
{
    #[ORM\Id]
    #[ORM\Column(type: 'integer')]
    #[ORM\GeneratedValue(strategy: 'AUTO')]
    private ?int $id = null;

    #[ORM\Column(type: 'string', length: 255)]
    private string $name = '';
}

常见误区与 FAQ

注解会影响性能吗?

注解本身不影响运行时性能。只有在通过反射读取注解时才有开销。框架通常会在启动时缓存注解解析结果,后续请求直接读取缓存。

可以在函数/闭包上使用注解吗?

PHP 8.0+ 支持。函数、方法、闭包、类的属性、方法参数、类常量都可以使用注解。

注解可以有多个参数吗?

可以。注解类可以有多个构造函数参数,使用命名参数传递更加清晰:

php
<?php
declare(strict_types=1);

#[Route(path: '/api/users', methods: ['GET'], name: 'user_list')]
class UserController {}

参考链接