[译注] 原文中用了 Attribute 而不是常见的 Annotation 来表示注解特性,不知他们出于何种原因。Attribute 这个单词我们在计算机领域可能更多的等同于 Property了,比如 xml 中元素的属性。不过查了一下词典,也许可以认为他们想表达“定语”。语法格式似乎学的C#。
概述
通过在代码中做相应声明,注解提供了为代码添加结构化的机器可理解的元数据信息的能力,支持范围:类、方法、函数、参数、属性及类常量。后续在代码运行时阶段,这些定义的元数据可用Reflection API进行理解。因此,注解可被认为是一种直接嵌入代码的配置语言。
类比于接口及其实现,注解也能让应用内某个功能特性的通用实现与其具体运行时状态进行解耦,不同的是接口用于代码的解耦,注解则是注释了代码运行过程中实际所需的额外信息与配置。
注解的一种简单用法是将将某个带有某些可选方法的接口换成用注解实现。
实例 #1 使用注解实现可选方法接口的场景
<?php
interface ActionHandler
{
public function execute();
}
#[Attribute]
class SetUp {}
/**
* 必须实现 ActionHandler 的 execute 方法
*/
class CopyFile implements ActionHandler
{
public string $fileName;
public string $targetDirectory;
#[SetUp]
public function fileExists()
{
if (!file_exists($this->fileName)) {
throw new RuntimeException("File does not exist");
}
}
#[SetUp]
public function targetDirectoryExists()
{
mkdir($this->targetDirectory);
}
public function execute()
{
copy($this->fileName, $this->targetDirectory . '/' . basename($this->fileName));
}
}
<?php
function executeAction(ActionHandler $actionHandler)
{
$reflection = new ReflectionObject($actionHandler);
// 解析注解数据,执行复制文件的前置操作
foreach ($reflection->getMethods() as $method) {
$attributes = $method->getAttributes(SetUp::class);
if (count($attributes) > 0) {
$methodName = $method->getName();
$actionHandler->$methodName();
}
}
// 执行 复制文件的最终操作
$actionHandler->execute();
}
$copyAction = new CopyFile();
$copyAction->fileName = "/tmp/foo.jpg";
$copyAction->targetDirectory = "/home/user";
executeAction($copyAction);
语法
- 注解声明总是以 #[ 开头,且以 ] 结束;
- 里面可以有一个或多个注解,以逗号分隔;
- 注解名会被解析为类,参数则被传给类的构造函数;
- 注解可以带参数(非必须),用括号包裹起来;
- 注解参数只能是字面量或常量;传统的参数形式及新的具名参数语法都支持
实例 #2 注解语法
<?php
// a.php
namespace MyExample;
use Attribute;
#[Attribute]
class MyAttribute
{
const VALUE = 'value';
private $value;
public function __construct($value = null)
{
$this->value = $value;
}
}
<?php
// b.php
namespace Another;
use MyExampleMyAttribute;
#[MyAttribute]
#[MyExampleMyAttribute]
#[MyAttribute(1234)]
#[MyAttribute(value: 1234)]
#[MyAttribute(MyAttribute::VALUE)]
#[MyAttribute(array("key" => "value"))]
#[MyAttribute(100 + 200)]
class Thing
{
}
#[MyAttribute(1234), MyAttribute(5678)]
class AnotherThing
{
}
声明注解类
尽管从语法上并不严格要求,但还是建议为每个注解创建一个实际的类,以便被全局命名空间导入。最简单的形式是声明一个空类,并带上 #[Attribute] 注解。
实例 #3 限制注解能使用的场景
<?php
namespace Example;
use Attribute;
#[Attribute]
class MyAttribute
{
}
可以限制注解被指定的类型。
实例 #4 简单注解类
<?php
namespace Example;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION)]
class MyAttribute
{
}
如果将上述 MyAttribute 声明到另一种类型的场景,会导致在调用 ReflectionAttribute::newInstance() 方法是抛出异常。
默认情况下,一个注解处,同一个注解类只能被用一次。如果确实需要重复,可以声明为允许重复。
实例 #5 使用 IS_REPEATABLE
<?php
namespace Example;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION | Attribute::IS_REPEATABLE)]
class MyAttribute
{
}
通过Reflection API解析注解内容
Reflection API 提供了 getAttributes() 方法用于访问注解,该方法返回由 ReflectionAttribute 实例构成的数组,可以通过这些实例查询注解名、参数,并将注解所代表的类进行实例化。
调用 newInstance() 方法后,这些注解类才会被实例化为对象,因此可以在此之前进行参数的校验,或进行相应错误处理。
实例 #6 用Reflection API解析注解
<?php
#[Attribute]
class MyAttribute
{
public $value;
public function __construct($value)
{
$this->value = $value;
}
}
#[MyAttribute(value: 1234)]
class Thing
{
}
function dumpAttributeData($reflection) {
$attributes = $reflection->getAttributes();
foreach ($attributes as $attribute) {
var_dump($attribute->getName());
var_dump($attribute->getArguments());
var_dump($attribute->newInstance());
}
}
dumpAttributeData(new ReflectionClass(Thing::class));
/*
string(11) "MyAttribute"
array(1) {
["value"]=>
int(1234)
}
object(MyAttribute)#3 (1) {
["value"]=>
int(1234)
}
*/
可以通过指定注解类来避免遍历所有注解。
实例 #7 用Reflection API 解析特定注解
<?php
function dumpMyAttributeData($reflection) {
$attributes = $reflection->getAttributes(MyAttribute::class);
foreach ($attributes as $attribute) {
var_dump($attribute->getName());
var_dump($attribute->getArguments());
var_dump($attribute->newInstance());
}
}
dumpAttributeData(new ReflectionClass(Thing::class));