从PHP 8开始,我们将能够使用Attributes。Attributes(在许多其他语言中也称为annotations)的目标是将元数据添加到类,方法,变量等中;以结构化的方式。

attributes的概念根本不是什么新概念,多年来,我们一直在使用docblock来模拟其行为。但是,通过添加attributes,我们现在有了该语言的一等公民来表示这种元数据,而不必手动解析docblock。

那它们长什么样呢?我们如何制作自定义attributes?有什么警告吗?这些是本文将要回答的问题。让我们深入!

纲要

重要的先说,这就是attributes看起来的样式:

use \Support\Attributes\ListensTo;

class ProductSubscriber
{
    #[ListensTo(ProductCreated::class)]
    public function onProductCreated(ProductCreated $event) { /* … */ }

    #[ListensTo(ProductDeleted::class)]
    public function onProductDeleted(ProductDeleted $event) { /* … */ }
}

我将在本文的后面显示其他示例,但我认为event subscribers示例首先是一个很好的解释attributes使用的示例。

另外,是的,我知道,语法可能不是您所希望的。您可能更喜欢使用@,或@:,或docblock或…,但是它仍然存在,因此我们最好学会处理它。关于语法,唯一值得一提的是讨论了所有选项,并且选择此语法有很好的理由。您可以在内部结构列表中阅读有关RFC的全部讨论。

话虽这么说,让我们专注于很酷的东西:ListensTo将如何在后台工作?

首先,自定义attributes是简单的类,并使用#[Attribute] attribute对其进行了注释。这个基础 Attribute 曾经在原始RFC中被称为PhpAttribute,但后来又被另一个RFC所改变。

如下所示:

#[Attribute]
class ListensTo
{
    public string $event;

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

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

回到event subscriber 示例:我们仍然需要读取元数据并在某个地方注册我们的订阅者。来自Laravel,我将使用服务提供商作为这样做的地方,但随时可以提出其他解决方案。

这是无聊的样板设置,只是为了提供一些上下文:

class EventServiceProvider extends ServiceProvider
{
    // In real life scenarios, 
    //  we'd automatically resolve and cache all subscribers
    //  instead of using a manual array.
    private array $subscribers = [
        ProductSubscriber::class,
    ];

    public function register(): void
    {
        // The event dispatcher is resolved from the container
        $eventDispatcher = $this->app->make(EventDispatcher::class);

        foreach ($this->subscribers as $subscriber) {
            // We'll resolve all listeners registered 
            //  in the subscriber class,
            //  and add them to the dispatcher.
            foreach (
                $this->resolveListeners($subscriber) 
                as [$event, $listener]
            ) {
                $eventDispatcher->listen($event, $listener);
            }       
        }       
    }
}

请注意,如果[$event, $listener]语法你不熟悉,则可以在我有关数组解构的文章中快速掌握它。

现在,让我们看一下resolveListeners神奇的地方。

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[] = [
                // The event that's configured on the attribute
                $listener->event,
    
                // The listener for this event 
                [$subscriberClass, $method->getName()],
            ];
        }
    }

    return $listeners;
}

您会发现,与解析docblock字符串相比,以这种方式读取元数据更容易。虽然有两个复杂的地方值得研究。

首先是 $attribute->newInstance() 调用。这实际上是实例化我们的自定义attribute类的地方。它将采用subscriber类的attribute定义中列出的参数,并将它们传递给构造函数。

从技术上讲,这意味着您甚至不需要构造自定义attribute。您可以直接调用 $attribute->getArguments() 。此外,实例化该类意味着您可以随意构造函数来解析输入。总而言之,我总是建议使用newInstance()实例化attribute。

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

为了理解这种过滤,首先需要了解有关attributes的另一件事。这对您来说可能很明显,但是无论如何我都想快速提一下:可以在同一个方法,类,属性或常量中添加多个attributes。

例如,您可以这样做:

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

考虑到这一点,很清楚为什么 Reflection*::getAttributes() 返回数组,因此让我们看一下如何过滤其输出。

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

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

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

例如,假设您要解析依赖于几个attributes的容器定义,则可以执行以下操作:

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

这是一个不错的捷径,内置在内核中。

技术理论

既然您已经了解了attributes在实际中的工作方式,那么该是谈更多理论的时候了,请确保您对它们有充分的了解。首先,我之前简短地提到了这一点,可以在多个位置添加attributes。

在类以及匿名类中;

#[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) { /* … */ }

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

/** @return void */
#[MethodAttribute]
public function doSomething(): void { /* … */ }

并且能使用由attribute的构造函数定义的0、一个或多个参数::

#[Listens(ProductCreatedEvent::class)]
#[Autowire]
#[Route(Http::POST, '/products/create')]

至于可以传递给attribute的允许参数,您已经看到允许使用类常量, ::class 名称和标量类型。但是,还有更多要说的:attributes仅接受常量表达式作为输入参数。

这意味着允许使用标量表达式(甚至是位移),以及::class,常量,数组和数组拆包,布尔表达式以及null合并运算符。可以在源代码中找到作为常量表达式允许的所有内容的列表。

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

Attribute配置

默认情况下,可以在上面几个位置添加attributes。但是,可以对其进行配置,以便仅在特定的地方使用它们。例如,您可以使ClassAttribute只能在类上使用,而不能在其他任何地方使用。通过Attribute向attribute类上的attribute传递标志来选择加入此行为。

看起来像这样:

#[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

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

#[Attribute(Attribute::TARGET_METHOD|Attribute::TARGET_FUNCTION)]
class ClassAttribute
{
}

另一个配置标志是关于可重复性。默认情况下,除非明确将其标记为可重复使用,否则该attribute不能被应用两次。这与目标配置相同,只是带有位标志。

#[Attribute(Attribute::IS_REPEATABLE)]
class ClassAttribute
{
}

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

内置attributes

一旦基本RFC被接受,就出现了向核心添加内置attributes的新机会。一个这样的例子就是 #[Deprecated] attribute,而一个流行的例子就是 #[Jit] attribute -如果不确定最后一个是什么,可以阅读我关于JIT是什么的文章。

我敢肯定,将来我们会看到越来越多的内置attributes。

最后一点,对于那些担心泛型的人:语法不会与它们冲突,如果曾经在PHP中添加它们,那么我们很安全!


原创翻译,转载请注明来自lenix的博客,地址https://blog.p2hp.com/archives/7424

原文:https://stitcher.io/blog/attributes-in-php-8

PHP 8的 Attributes新特性介绍
标签: