注解概览
概述
注解(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
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
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
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
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
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
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
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
declare(strict_types=1);
use Attribute;
// 正确:声明为注解类
#[Attribute]
class MyAnnotation {}
// 错误:没有 #[Attribute] 标记的类不能用作注解
// class NotAnAnnotation {}
// #[NotAnAnnotation] // Error: NotAnAnnotation is not an Attribute注解不是注释
注解在运行时是可用的(通过反射),它们不是简单的注释。不要将敏感信息放在注解中。
最佳实践
- 使用命名空间:将注解类放在独立的命名空间中(如
App\Attributes)。 - 使用 readonly 属性:注解类的属性应该是
readonly的,确保不可变。 - 声明目标限制:在
#[Attribute]中指定注解可以使用的位置。 - 使用构造函数验证:在注解类的构造函数中进行参数验证。
- 缓存反射结果:注解的反射读取有一定开销,在性能敏感场景中应缓存结果。
<?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
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
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 Annotations | PHP 原生 Attributes |
|---|---|---|
| 语法验证 | 无(纯字符串) | IDE 支持,语法高亮 |
| 类型安全 | 无(需要手动解析) | 强类型,构造函数验证 |
| 性能 | 需要正则解析注释 | 原生反射 API |
| 缓存支持 | 需要框架实现 | 反射结果可缓存 |
| 可维护性 | 容易拼写错误 | IDE 重构支持 |
| PHP 版本 | 所有版本 | 8.0+ |
| 命名空间 | 不支持 | 完整支持 |
注解在现代框架中的应用
Symfony 框架
Symfony 从 5.2 开始支持 PHP 8 原生注解,6.x 版本中已成为首选方式:
<?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
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
declare(strict_types=1);
#[Route(path: '/api/users', methods: ['GET'], name: 'user_list')]
class UserController {}