PHP 注解

(PHP 8)

PHP8 开始可以使用注解功能。

注解功能使得代码中的声明部分可以添加结构化、机器可读的元数据,注解作用于类、方法、函数、参数、属性、类常量。 

通过反射 API 可在运行时获取注解所定义的元数据,因此注解可以成为直接嵌入代码的配置式语言。


注解概览

通过注解的使用,在应用中实现功能、使用功能可以相互解耦。 某种程度上讲,它可以和接口(interface)与其实现(implementation)相比较。 

但接口与实现是代码相关的,注解则与声明额外信息和配置相关。 

接口可以通过类来实现,而注解也可以声明到方法、函数、参数、属性、类常量中,因此它们比接口更灵活。

注解使用的一个简单例子:将接口(interface)的可选方法改用注解实现。 

我们假设接口 ActionHandler 代表了应用的一个操作: 部分 action handler 的实现需要 setup,部分不需要。 

我们可以使用注解,而不用要求所有类必须实现 ActionHandler 接口并实现 setUp() 方法。 因此带来一个好处是可以多次使用注解。

示例:用注解实现接口的可选方法

<?php
interface ActionHandler
{
    public function execute();
}
#[Attribute]
class SetUp {}
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));
    }
}
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);

注解语法

注解语法包含以下几部分。 首先,注解声明总是以 #[ 开头,以 ] 结尾来包围。 内部则是一个或以逗号包含的多个注解。 注解的名称按 使用命名空间:基础 章节中描述,可以是非限定、限定、完全限定的名称。 注解的参数是可以选的,以常见的括号()包围。 注解的参数可以是字面值或者常量表达式。 它同时接受位置参数和命名参数两种语法。

通过反射 API 请求注解实例时,注解的名称会被解析到一个类,注解的参数则传入该类的构造器中。 因此每个注解都需要引入一个类。

示例 注解语法

<?php
// a.php
namespace MyExample;
use Attribute;
#[Attribute]
class MyAttribute
{
    const VALUE = 'value';
    private $value;
    public function __construct($value = null)
    {
        $this->value = $value;
    }
}
// b.php
namespace Another;
use MyExample\MyAttribute;
#[MyAttribute]
#[\MyExample\MyAttribute]
#[MyAttribute(1234)]
#[MyAttribute(value: 1234)]
#[MyAttribute(MyAttribute::VALUE)]
#[MyAttribute(array("key" => "value"))]
#[MyAttribute(100 + 200)]
class Thing
{
}
#[MyAttribute(1234), MyAttribute(5678)]
class AnotherThing
{
}

使用反射 API 读取注解

反射 API 提供了 getAttributes() 方法, 类、方法、函数、参数、属性、类常量的反射对象可通过它获取相应的注解。 该方法返回了 ReflectionAttribute 实例的数组, 可用于查询注解名称、参数、也可以实例化一个注解。

实例和反射注解的分离使得程序员增加了在丢失反射类、类型错误、丢失参数等情况下的处理能力,也能处理错误。 只有调用 newInstance() 后,注解类的对象才会以验证过匹配的参数来实例化。

示例 通过反射 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)
}
*/

通过传入参数:待搜索的注解类名,可返回指定的注解类, 而不需要再到反射类中迭代循环获取所有注解。

示例 使用反射 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));

声明注解类

虽然没有严格要求,推荐为每个注解创建一个实际的类。 在这个最简单的例子中,通过 use 语法从全局命名空间引入 #[Attribute] 注解所需要全空的类。

示例 目标限定使用的注解

<?php
namespace Example;
use Attribute;
#[Attribute]
class MyAttribute
{
}

要限制指定注解的声明类型,可为 #[Attribute] 注解第一个参数传入字节位掩码设置。

示例 简单的 Attribute 类

<?php
namespace Example;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION)]
class MyAttribute
{
}

在另一个类型中声明 MyAttribute 会在调用 ReflectionAttribute::newInstance() 时抛出异常。

注解在每个声明中默认情况下只能使用一次。 如果需要重复,可以在 #[Attribute] 声明中设置字节位掩码。

示例 使用 IS_REPEATABLE 允许注解在声明中出现多次

<?php
namespace Example;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION | Attribute::IS_REPEATABLE)]
class MyAttribute
{
}