理解 PHP 8 中的 Attributes (注解)

说明

从 PHP 8 开始,我们将能够开始使用 ‘注解’。

这些注解(在许多其他语言中也称为注释)的目标是以结构化的方式将元数据添加到类、方法、变量等中。

注解的概念并不新鲜,我们使用文档块来模拟它们的行为已经有很多年了。

不过,通过添加注解,我们现在有了语言中的语法来表示这种元数据,而不是必须手动解析文档块。

  • 那么它们看起来是什么样子呢?
  • 我们如何创建自定义注解?
  • 有什么注意事项吗?

这些都是本帖将要回答的问题。

分析

首先,下面是原生注解的样子:

use \Support\Attributes\ListensTo;

class ProductSubscriber
{
    <<ListensTo(ProductCreated::class)>>
    public function onProductCreated(ProductCreated $event) { /* … */ }

    <<ListensTo(ProductDeleted::class)>>
    public function onProductDeleted(ProductDeleted $event) { /* … */ }
}

我将在这篇文章的后面展示其他示例,但我认为事件订阅者的示例是一个很好的例子,可以首先解释注解的用法。

另外,我知道,这种语法可能不是您想要或看到的。您可能首选 @、或 @:、或注释或…。不过,这种语法已经被合并了,所以我们最好学会处理和使用它。关于语法,唯一值得一提的是讨论了所有能够实现注解的语法方案,选择此语法有很好的理由。您可以在 RFC 中阅读有关它的简短摘要,也可以在内部列表中阅读关于 RFC 的完整讨论。

首先,自定义注解是简单的类,用 <<attribute>> 属性对自身进行注释;这个基本属性在最初的 RFC 中被称为 PhpAttribute ,但后来在另一个 RFC 中进行了更改。

下面是它看起来的样子:

<<Attribute>>
class ListensTo
{
    public string $event;

    public function __construct(string $event)
    {
        $this->event = $event;
    }
}

就是这样,很简单,对吧?记住注解的目标:它们的目的是将元数据添加到类和方法中,仅此而已。
例如,它们不应该也不能用于参数输入验证。换句话说:您不能访问传递给注解中的方法的参数。以前有一个 RFC 允许这种行为,但这个 RFC 特别让事情变得更简单。

回到事件订阅者示例:我们仍然需要读取元数据,并在某个地方注册我们的订阅者。我有使用 Laravel 的背景,我会使用服务提供商来做这件事,但也可以自由地想出其他解决方案。

class EventServiceProvider extends ServiceProvider
{
    // 在现实场景中,
    // 我们会自动解析并缓存所有订阅者。
    // 而不是使用手动数组。
    private array $subscribers = [
        ProductSubscriber::class,
    ];

    public function register(): void
    {
        // 事件调度器从容器中解析
        $eventDispatcher = $this->app->make(EventDispatcher::class);

        foreach ($this->subscribers as $subscriber) {
            // 我们将解析所有注册的监听器。
            // 在 `Subscriber` 类中,
            // 并将其添加到调度器。
            foreach (
                $this->resolveListeners($subscriber) 
                as [$event, $listener]
            ) {
                $eventDispatcher->listen($event, $listener);
            }       
        }       
    }
}

请注意,如果您不熟悉 [$event,$listener]语法,您可以在我关于数组析构的帖子中快速了解它。

现在让我们看一看 ResolutionveListeners,这就是魔术触发的地方。

private function resolveListeners(string $subscriberClass): array
{
    $reflectionClass = new ReflectionClass($subscriberClass);

    $listeners = [];

    foreach ($reflectionClass->getMethods() as $method) {
        $attributes = $method->getAttributes(ListensTo::class);

        foreach ($attributes as $attribute) {
            $listener = $attribute->newInstance();

            $listeners[] = [
                // 在注解上配置的事件
                $listener->event,

                // 此事件的监听器
                [$subscriberClass, $method->getName()],
            ];
        }
    }

    return $listeners;
}

您可以看到,与解析注释字符串相比,这样更容易读取元数据。不过,有两个错综复杂的问题值得研究。

首先是 $attribute->newInstance() 的调用。这实际上是我们的自定义属性类被实例化的地方。它将接受订阅服务器类的属性定义中列出的参数,并将它们传递给构造函数。

这意味着,从技术上讲,您甚至不需要构造自定义注解。您可以直接调用 $attribute->getArguments()。此外,实例化类意味着您可以灵活地使用构造函数以任何您喜欢的方式进行解析输入。总而言之,我想说总是使用newInstance()实例化属性会很好。

值得一提的是ReflectionMethod::getAttributes()的使用,该函数返回方法的所有属性。
您可以向其传递两个参数,以过滤其输出。

但是,为了理解这种过滤,您首先需要了解关于注解的另一件事。
这对您来说可能是显而易见的,但我还是想快速地提一下:可以向同一个方法、类、属性或常量添加几个属性。

<<Route(Http::POST, '/products/create')>>
<<Autowire>>
class ProductsCreateController
{
    public function __invoke() { /* … */ }
}

记住这一点,就很清楚为什么Reflect*::getAttributes()返回一个数组,那么让我们看看如何过滤它的输出。

假设您正在解析控制器路由,您只对Route注解感兴趣。您可以轻松地将该类作为筛选器传递:

$attributes = $reflectionClass->getAttributes(Route::class);

第二个参数更改过滤的方式。
您可以传入ReflectionAttribute::IS_INSTANCEOF,它将返回实现给定接口的所有注解。

例如,假设您正在解析容器定义,这依赖于几个属性,您可以这样做:

$attributes = $reflectionClass->getAttributes(
    ContainerAttribute::class, 
    ReflectionAttribute::IS_INSTANCEOF
);

技术理论

现在您已经了解了注解在实践中是如何工作的,是时候进行更多的理论了,确保您彻底理解它们。
首先,我在前面简单地提到了这一点,可以在几个地方添加注解。

在类中,以及匿名类中;

<<ClassAttribute>>
class MyClass { /* … */ }

$object = new <<ObjectAttribute>> class () { /* … */ };

属性和常量;

<<PropertyAttribute>>
public int $foo;

<<ConstAttribute>>
public const BAR = 1;

方法和功能;

<<MethodAttribute>>
public function doSomething(): void { /* … */ }

<<FunctionAttribute>>
function foo() { /* … */ }

以及闭包;

$closure = <<ClosureAttribute>> fn() => /* … */;

方法和功能的参数;

function foo(<<ArgumentAttribute>> $bar) { /* … */ }

它们可以在注释之前或之后声明;

/** @return void */
<<MethodAttribute>>
public function doSomething(): void { /* … */ }

并且可以接受无参数、一个参数或多个参数,这些参数由属性的构造函数定义:

<<Listens(ProductCreatedEvent::class)>>
<<Autowire>>
<<Route(Http::POST, '/products/create')>>

至于可以传递给注解的允许参数,您已经看到允许使用类常量、::class和标量类型。不过,关于这一点还有更多要说的:注解只接受常量表达式作为输入参数。

这意味着允许标量表达式-偶数位移位-以及::class、常量、数组和数组解包、布尔表达式和 NULL 合并运算符。可以在源代码中找到允许作为常量表达式的所有内容的列表。

<<AttributeWithScalarExpression(1+1)>>
<<AttributeWithClassNameAndConstants(PDO::class, PHP_VERSION_ID)>>
<<AttributeWithClassConstant(Http::POST)>>
<<AttributeWithBitShift(4 >> 1, 4 << 1)>>

注解配置

默认情况下,可以在多个位置添加注解,如上所列。
但是,可以对它们进行配置,使其只能在特定位置使用。
例如,您可以将其设置为ClassAttribute只能在类上使用,而不能在其他地方使用。
选择加入此行为是通过将标志传递给注解类上的注解属性来完成的。

<<Attribute(Attribute::TARGET_CLASS)>>
class ClassAttribute
{
}

以下标志可用:

Attribute::TARGET_CLASS
Attribute::TARGET_FUNCTION
Attribute::TARGET_METHOD
Attribute::TARGET_PROPERTY
Attribute::TARGET_CLASS_CONSTANT
Attribute::TARGET_PARAMETER
Attribute::TARGET_ALL

这些是位掩码标志,因此您可以使用二进制或操作符将它们组合在一起。

<<Attribute(Attribute::TARGET_METHOD|Attribute::TARGET_FUNCTION)>>
class ClassAttribute
{
}

另一个配置标志是关于重复性的。默认情况下,同一注解不能应用两次,除非它特别标记为可重复。这与使用位标志的目标配置相同。

<<Attribute(Attribute::IS_REPEATABLE)>>
class ClassAttribute
{
}

请注意,所有这些标志只在调用$attribute->newInstance()时验证,而不是更早。

内置注解

一旦基本RFC被接受,就出现了向核心添加内置注解的新机会。
<<deposated>>注解就是这样的一个示例,而<<JIT>>注解就是一个很流行的示例-如果您不确定最后一个注解是关于什么的,您可以阅读我关于JIT是什么的帖子。

我相信将来我们会看到越来越多的内置注解。

最后要注意的是,对于那些担心泛型的人来说:语法不会与它们冲突,如果它们被添加到 PHP 中,那么我们是安全的!

我已经想好了一些注解的用例,你呢?

php
本作品采用《CC 协议》,转载必须注明作者和本文链接
By: Laravel-China 宁泽林 Blog: nizer.in
讨论数量: 4

能说明下这种语法吗?我没看到你文章中提到的帖子

file

2个月前 评论

我裂开了,今天面试让我讲注解,我一脸懵逼,问什么是注解 :joy:

2个月前 评论

@zccccc 没想到啊,有些公司技术跟进还是蛮快的

2个月前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!