从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()
返回数组,因此让我们看一下如何过滤其输出。
假设您正在解析控制器路由,则只对Route
attribute感兴趣。您可以轻松地将该类作为过滤器传递:
$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