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
之前,比如Drupal
,Symfony
和Doctrine
之类的框架使用注释的方式来组织为某些类提供辅助信息。最典型的就是注释路由:
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]
选择此语法时,官方进行了很好的讨论,建议的一些替代模式是:
@@ Attribute
[[Attribute]]
@: 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注释。
Drupal
和Symfony
对控制器,插件,渲染块等使用Doctrine Annotation
。在适当的时候,它们都可以升级为Attributes
。
本文大部分内容是通过此文章翻译过来。