骑驴找蚂蚁

全干工程师

PHP8之属性注解(Attributes)

距离PHP 8发布已经过了好几个月了,本文将针对一个非常重要的功能特性进行讲解。它就是万众期待的注解(Attributes)也叫做属性。在没有Attributes之前我们也可以通过注释来实现它的一些功能,也有很多的注释解析库如: Doctrine Annotations, phpdocumentor/reflection-docblock等 。PHP 8之后我们得到了原生的支持。

什么是Attributes

Attributes是为PHP类,函数,闭包,类属性,类方法,常量甚至在匿名类上添加的元数据。 PHP DocBlock注释可能是最熟悉的示例。

/**  
* @param string $message  
*/  
function foo(string $message) {}  

这些注释在某种程度上由@param“注释”构成。 这些注释块不会被执行,但是PHP提供了一个称为"Reflection API"的API,可以方便地读取这些注释。就像刚刚说得那几种注释库都是通过这个API来解析数据。

这种注释的方法有点缺陷,因为它很容易打错字,直到将这些注释从代码中的其他地方读出来您才会注意到。PHP 8中添加的新方法是Attributes。 属性提供了一种更为实用的方法来声明和获取这些信息。

在没有Attributes之前,比如DrupalSymfonyDoctrine之类的框架使用注释的方式来组织为某些类提供辅助信息。最典型的就是注释路由:

class AboutPage extends AbstractController {  
/**  
* @Route("/about")  
*/  
public function page() {}  
}  

带有DocBlock注释,此注释提供了关于AboutPage类的有用信息。 在框架中,可以将其转换为路由表中的一条路由,以将“/about”路径路由到AboutPage::page方法。

PHP 8中的Attributes更优于注释,它为注释带来了一种结构化且经过引擎验证的方法。

class AboutPage extends AbstractController {  
#[Route('/about')]  
public function page() {}  
}  

属性不是以XML架构或JSON架构的形式编写单独的定义,而是提供一种轻松且可管理的方式来组织此元数据。

其他语言的Attributes

许多语言具有与PHP属性类似的功能特性。

  • Java可能是最受欢迎的Java,它的Annotations语法类似于@Route(name="/about")。
  • Rust的Attributes语法类似于#[route(name="/about")],与PHP的实现完全相同。
  • Python注释称为装饰器,并遵循类似的语法:@route("/about")。

PHP现有的注释形式被广泛使用,但是PHP 8中的属性使用#[]大括号语法。 社区对此和用户进行了辩论,并将其从最初的<< Attr >>实现更改为@@ Attr,再更改为最终的#[Attr]语法。

属性与注释

属性和注释提供相同的功能。 “注释”已在PHP库和框架中广泛使用,因此名称Attributes有助于最大程度地减少与注释的混淆。使用属性我们不必去解析注释,使用最新的PhpStorm IDE也支持了Attributes功能。

以前的尝试

以前有两次尝试将此功能引入PHP。 第一个大约在8年前,提出了一个名为“注释”的提案。 2016年,Dmitry Stogov提出了第一个Attributes RFC。 这些尝试都没有取得丰硕的成果。 实际上,第一个Attribute RFC提出了与PHP 8相同的语法,但是第二个RFC简化了PHP 8,Benjamin Eberlei做出了巨大的工作来解决次要细节并与之进行健康的讨论。 社区同意其语法和功能。

属性语法和功能

属性语法只是用#[]来完成约定。

#[Attribute]  

选择此语法时,官方进行了很好的讨论,建议的一些替代模式是:

从最初的<>语法后来被RFC更改为@@,随后又是另一个RFC更改为"#[]",这也带来了某种形式的向后兼容性。

设计目标

PHP 8属性提供了对信息的便捷访问。 语法和实现的目的是使语法非常熟悉用户已经熟悉的内容:

  • 属性可以解析为类名。
  • 属性可以命名空间。
  • 可以使用use语句导入属性类名称。
  • 属性可以具有零个或多个参数。
  • 一个声明可以有多个属性。
  • 可以从Reflection API中检索属性实例。

本文的其余部分将通过详尽的示例来说明所有这些功能。

使用命名空间并将它们与类名称相关联,可以更轻松地重用和组织属性。 可以扩展它们或实现接口,在轮询属性时,Reflection API会提供方便的过滤器功能。

属性可以解析为类名

尽管不是必需的,PHP 8提供了将属性名称解析为类名称的功能。 您可以使用use语句来清理代码。 将遵循类名称解析的标准规则。 将属性名称与类名称匹配是可选的。 如果属性未映射到类名,则允许重复该属性,并且不允许从Reflection API实例化该属性。

属性可以具有参数

每个属性可以具有零个或多个参数。 如果尝试获取属性的实例化对象,则将它们传递给Attribute类构造函数。 参数可以是简单的标量类型,数组,甚至可以是简单的表达式,例如数学表达式,PHP常数,类常数(包括魔术常数)。 任何可以用作类常量的表达式都可以用作Attribute参数。

允许多个属性

接收属性的每个类目可以具有零个或多个属性,每个属性都在其自己的[]括号内,或用逗号分隔。 每个属性都可以由空格(换行或空格)分隔。 请注意,如果一个属性映射到一个类名,则不允许该属性多次被赋予属性。 可以将该属性显式声明为可重复,以允许此操作。

#[Attr]  
#[FooAttr]  
function foo(){}  
  
#[Attr, FooAttr]  
function bar(){}  

DocBlock之前和之后的注释

属性可以出现在DocBlock注释之前和之后。 对于代码样式,没有标准的建议,但是肯定会在以后的PSR代码样式建议中消除这一建议。我比较喜欢注释之后。

属性示例

可以将属性添加到各种声明中。

函数

#[Attr('foo')]  
function example(){}  

#[Attr('foo')]  
class Example {}  

函数方法参数

function example(#[Attr('foo')] string $foo) {}  

类属性

class Foo {  
#[Attr('foo')]  
private string $foo;  
}  

类常量

class Foo {  
#[Attr('foo')]  
private const FOO = 'foo';  
}  

闭包

$fn = #[Attr('foo')] fn() => 1 > 2;  
  
$fn = #[Attr('foo')] function() {  
return 1 > 2;  
}  

匿名类

$instance = new #[Attr('foo')] class {};  

使用DocBlocks

可以在DocBlock注释之前和/或之后放置属性:

#[AttributeBefore('foo')]  
#[AttributeBefore2('foo')]  
#[AttrCommas('foo'), AttrCommas('foo')]  
/**  
* Foo  
*/  
#[AttributeAfter('foo')]  
function example() {}  

完整的例子

#[FooAttribute]  
function foo_func(#[FooParamAttrib('Foo1')] $foo) {}  
  
#[FooAttribute('hello')]  
#[BarClassAttrib(42)]  
class Foo {  
#[ConstAttr]  
#[FooAttribute(null)]  
private const FOO_CONST = 28;  
private const BAR_CONST = 28;  
  
#[PropAttr(Foo::BAR_CONST, 'string')]  
private string $foo;  
  
#[SomeoneElse\FooMethodAttrib]  
public function getFoo(#[FooClassAttrib(28)] $a): string{}  
}  
  
// Declare Attributes  
  
/*  
* Attributes are declared with `#[Attribute]`.  
*/  
  
#[Attribute]  
class FooAttribute {  
public function __construct(?string $param1 = null) {}  
}  
  
#[Attribute]  
class ClassAttrib {  
public function __construct(int $index) {}  
}  

声明属性

属性本身可以声明为类。 仅当获取属性时才对此进行验证,而在解析代码时不立即进行验证。 PHP是标准的PHP类,使用#[Attribute]属性声明。

#[Attribute]  
class FooAttribute{  
  
}  

默认情况下,可以在任何接受属性的类目上使用声明的属性。 这包括类,类方法,闭包,函数,参数和类属性。

声明属性时,可以声明必须使用该属性的目标。

#[Attribute(Attribute::TARGET_CLASS)]  
class Foo {}  

当该属性带有其支持的目标时,PHP不允许将该属性用于任何其他目标。 它接受位掩码以允许该属性出现在一个或多个目标中。

#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]  
class Foo {}  

它允许以下目标:

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

TARGET_ALL是所有其他目标.

属性类声明为final

Attribute类被声明为final,这阻止了它的扩展。

可重复属性

默认情况下,不允许在同一目标上多次使用同一属性。 该属性必须明确允许它:

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

属性类简介

#[Attribute(Attribute::TARGET_CLASS)]  
final class Attribute {  
public int $flags;  
/**  
* Marks that attribute declaration is allowed only in classes.  
*/  
const TARGET_CLASS = 1;  
  
/**  
* Marks that attribute declaration is allowed only in functions.  
*/  
const TARGET_FUNCTION = 1 << 1;  
  
/**  
* Marks that attribute declaration is allowed only in class methods.  
*/  
const TARGET_METHOD = 1 << 2;  
  
/**  
* Marks that attribute declaration is allowed only in class properties.  
*/  
const TARGET_PROPERTY = 1 << 3;  
  
/**  
* Marks that attribute declaration is allowed only in class constants.  
*/  
const TARGET_CLASS_CONSTANT = 1 << 4;  
  
/**  
* Marks that attribute declaration is allowed only in function or method parameters.  
*/  
const TARGET_PARAMETER = 1 << 5;  
  
/**  
* Marks that attribute declaration is allowed anywhere.  
*/  
const TARGET_ALL = (1 << 6) - 1;  
  
/**  
* Notes that an attribute declaration in the same place is  
* allowed multiple times.  
*/  
const IS_REPEATABLE = 1 << 10;  
  
/**  
* @param int $flags A value in the form of a bitmask indicating the places  
* where attributes can be defined.  
*/  
public function __construct(int $flags = self::TARGET_ALL)  
{  
}  
}  

反射API

使用反射API检索属性。 当PHP引擎解析包含属性的代码时,它们会存储在内部结构中以备将来使用。 包含Opcache支持。 除非请求Attribute的实例,否则它不会执行任何代码或调用属性的构造函数(请参见下面的示例)。
使用Reflection API,可以以包含属性名称(已解析类名)及其可选参数的字符串的形式检索属性。 Reflection API还可以实例化Attribute类的实例,其中解析了类名,自动加载了该类名,并将可选参数传递给了类构造函数。
未能实例化该类将抛出\ Error异常,可以在调用方级别捕获该异常。

getAttributes

$reflector = new \ReflectionClass(Foo::class);  
$reflector->getAttributes();  

所有Reflection类都有一个新方法getAttributes方法,该方法返回ReflectionAttribute对象的数组。 此新方法的摘要类似于以下内容:

/**  
* @param string $name Name of the class to filter the return list  
* @param int $flags Flags to pass for the filtering process.  
* @return array ReflectionAttribute[]  
*/  
public function getAttributes(?string $name = null, int $flags = 0): array {}  

ReflectionAttribute

final class ReflectionAttribute {  
/**  
* @return string The name of the attribute, with class names resolved.  
*/  
public function getName(): string {}  
  
/**  
* @return array Arguments passed to the attribute when it is declared.  
*/  
public function getArguments(): array {}  
  
/**  
* @return object An instantiated class object of the name, with arguments passed to the constructor.  
*/  
public function newInstance(): object {}  
}  

属性过滤

getAttributes可选地接受一个类名字符串,该字符串可用于通过某个属性名来过滤属性的返回数组。

$attrs = $reflector->getAttributes(FooAttribute::class);  

$attrs数组现在仅是ReflectionAttribute对象或FooAttribute属性名称。第二个可选参数接受整数以进一步微调返回数组。

$attrs = $reflector->getAttributes(BaseAttribute::class, \ReflectionAttribute::IS_INSTANCEOF);  

目前,仅\ReflectionAttribute::IS_INSTANCEOF可用。 如果传递了\ReflectionAttribute::IS_INSTANCEOF,则返回数组将包含具有相同类名或扩展或实现所提供名称的类的Attribute(即所有满足instanceOf $name的类)。

属性对象实例

ReflectionAttribute::newInstance方法返回Attribute类的实例,并将任何参数传递给Attribute对象类的构造函数。

完整的反射示例

#[exampleAttribute('Hello world', 42)]  
class Foo {}  
  
#[Attribute]  
class ExampleAttribute {  
private string $message;  
private int $answer;  
public function __construct(string $message, int $answer) {  
$this->message = $message;  
$this->answer = $answer;  
}  
}  
  
$reflector = new \ReflectionClass(Foo::class);  
$attrs = $reflector->getAttributes();  
  
foreach ($attrs as $attribute) {  
$attribute->getName(); // "My\Attributes\ExampleAttribute"  
$attribute->getArguments(); // ["Hello world", 42]  
$attribute->newInstance();  
// object(ExampleAttribute)#1 (2) {  
// ["message":"Foo":private]=> string(11) "Hello World"  
// ["answer":"Foo":private]=> int(42)  
// }  
}  

实际用例

Attributes功能非常强大,因为它们可以直接与类名关联,并且类名解析是内置的,静态分析器和IDE可以轻松添加对Attributes的支持。 像PHPStorm这样的IDE已经支持Attributes,它甚至还提供了一些自己的内置属性,例如#[Deprecated]

从注释迁移到属性

当您的项目有能力使用PHP 8作为最低版本时,可以将具有Doctrine风格的注释升级为一流的PHP属性。

- /** @ORM\Entity */  
+ #[ORM\Entity]  
class Book {  
- /**  
- * @ORM\Id  
- * @ORM\Column(type="string")  
- * @ORM\GeneratedValue  
- */  
+ #[ORM\Id]  
+ #[ORM\Column("string")]  
+ #[ORM\GeneratedValue]  
private string $isbn;  
}  

如果你的项目使用得Symfony 5.2以上的版本,可以直接将注释路由直接升级到属性路由。

// AFTER: annotations defined with PHP 8 attributes  
use Symfony\Component\Routing\Annotation\Route;  
  
class SomeController  
{  
#[Route('/path', name: 'action')]  
public function someAction()  
{  
// ...  
}  
}  

Laravel框架三方包也提供了属性路由。

本网站的后台菜单也是通过属性生成。

未来的属性

属性可能是许多PHP功能的基石,而这些功能在理想情况下并未用接口“标记”。

在关于属性的建议中,它提到了使用属性来标记声明与JIT兼容/不兼容。 另一个用例是#[Deprecated]属性,该属性可用于声明引擎和用户范围的类/函数或其他任何不推荐使用的类/函数。 最终可以撤消@deprecated DocBlock注释。

DrupalSymfony对控制器,插件,渲染块等使用Doctrine Annotation。在适当的时候,它们都可以升级为Attributes

本文大部分内容是通过此文章翻译过来。

推荐阅读

  1. https://php.watch/articles/php-attributes
  2. https://www.php.net/manual/en/language.attributes.overview.php
  3. https://stitcher.io/blog/attributes-in-php-8
  4. https://platform.sh/blog/2020/php-8-0-feature-focus-attributes/

留言