PHP8之属性注解(Attributes)

骑驴找蚂蚁 · 2021年03月15日 · 阅读 35

距离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/