PHP 8.0: Атрибути (3/4)

3 рік тому Від David Grudl  

Прямо зараз випускається версія PHP 8.0. У ній багато нового, як у жодній іншій версії. Їх представлення заслуговує на чотири окремі статті. У третій частині ми розглянемо атрибути.

Атрибути надають абсолютно новий спосіб написання структурованих метаданих для класів і всіх його членів, а також функцій, закриття і їх параметрів. Досі для цього використовувалися коментарі PhpDoc, але їх синтаксис завжди був настільки вільним і непослідовним, що їх неможливо було запустити в машинну обробку. Тому на зміну їм приходять атрибути з визначеним синтаксисом і підтримкою в класах рефлексії.

Завдяки цьому бібліотеки, які раніше отримували метадані шляхом розбору коментарів phpDoc, зможуть замінити їх на атрибути. Одним з прикладів є Nette, де в останніх версіях Application та DI вже можна використовувати атрибути Persistent, CrossOrigin та Inject замість анотацій @persistent, @crossOrigin та @inject.

Код з використанням анотацій:

/**
 * @persistent(comp1, comp2)
 */
class SomePresenter
{
	/** @persistent */
	public $page = 1;

	/** @inject */
	public Facade $facade;

	/**
	 * @crossOrigin
	 */
	public function handleSomething()
	{
	}
}

Те саме з атрибутами:

use Nette\Application\Attributes\CrossOrigin;
use Nette\Application\Attributes\Persistent;
use Nette\DI\Attributes\Inject;

#[Persistent('comp1', 'comp2')]
class SomePresenter
{
	#[Persistent]
	public int $page = 1;

	#[Inject]
	public Facade $facade;

	#[CrossOrigin]
	public function handleSomething()
	{
	}
}

PHP оцінює імена атрибутів так само, як якщо б вони були класами, в контексті просторів імен і речень use. Тож їх навіть можна було б написати, наприклад, так:

use Nette\Application\Attributes;

class SomePresenter
{
	#[Attributes\Persistent]
	public int $page = 1;

	#[\Nette\DI\Attributes\Inject]
	public Facade $facade;

Клас, що представляє атрибут, може існувати, а може і не існувати. Але краще, якщо він існує, тому що тоді редактор може підказати його під час написання, статичний аналізатор розпізнає помилки тощо.

Синтаксис

Дуже зручно, що PHP до версії 8 сприймає атрибути лише як коментарі, тому їх можна використовувати у коді, який мав би працювати у старіших версіях.

Синтаксис окремого атрибута виглядає як створення екземпляра об'єкта, якщо опустити оператор new. Отже, ім'я класу, за яким слідують необов'язкові аргументи в круглих дужках:

#[Column('string', 32, true, false)]#
protected $username;

І ось тут можна застосувати нову гарячу функцію PHP 8.0 – іменовані аргументи:

#[Column(
	type: 'string',
	length: 32,
	unique: true,
	nullable: false,
)]#
protected $username;

Кожен елемент може мати декілька атрибутів, які можна писати окремо або через кому:

#[Inject]
#[Lazy]
public Foo $foo;

#[Inject, Lazy]
public Bar $bar;

Наступний атрибут застосовується до всіх трьох властивостей:

#[Common]
private $a, $b, $c;

Прості вирази та константи, які можна обчислити під час компіляції і які використовуються як значення за замовчуванням для властивостей, можна використовувати як аргументи в атрибутах:

#[
	ScalarExpression(1 + 1),
	ClassNameAndConstants(PDO::class, PHP_VERSION_ID),
	BitShift(4 >> 1, 4 << 1),
	BitLogic(1 | 2, JSON_HEX_TAG | JSON_HEX_APOS),
]

На жаль, значенням аргументу не може бути інший атрибут, тобто атрибути не можуть бути вкладеними. Наприклад, не існує прямого способу перетворення наступної анотації, що використовується у Доктрині, на атрибути:

/**
 * @Table(name="products",uniqueConstraints={@UniqueConstraint(columns={"name", "email"})})
 */

Також не існує еквівалента атрибуту для файлу phpDoc, тобто коментаря, розташованого на початку файлу, який використовується, наприклад, Nette Tester.

Відображення атрибутів

За допомогою рефлексії можна визначити, які атрибути мають окремі елементи. У класах з рефлексією з'явився новий метод getAttributes(), який повертає масив об'єктів ReflectionAttribute.

use MyAttributes\Example;

#[Example('Hello', 123)]
class Foo
{}

$reflection = new ReflectionClass(Foo::class);

foreach ($reflection->getAttributes() as $attribute) {
	$attribute->getName();      // full attribute name, e.g. MyAttributes\Example
	$attribute->getArguments(); // ['Hello', 123]
	$attribute->newInstance();  // returns an instance: new MyAttributes\Example('Hello', 123)
}

Атрибути, що повертаються, можна відфільтрувати за допомогою параметра, наприклад, $reflection->getAttributes(Example::class) повертає тільки атрибути Example.

Класи атрибутів

Атрибутивний клас MyAttributes\Example може не існувати. Тільки виклик методу newInstance() вимагає його існування, оскільки він його інстанціює. Отже, давайте напишемо його. Це буде абсолютно звичайний клас, тільки ми повинні надати атрибут Attribute (тобто з глобального системного простору імен):

namespace MyAttributes;

use Attribute;

#[Attribute]
class Example
{
	public function __construct(string $message, int $number)
	{
		...
	}
}

Можна обмежити, для яких елементів мови буде дозволено використовувати цей атрибут. Наприклад, лише для класів та властивостей:

#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_PROPERTY)]
class Example
{
	...
}

Доступні прапори TARGET_CLASS, TARGET_FUNCTION, TARGET_METHOD, TARGET_PROPERTY, TARGET_CLASS_CONSTANT, TARGET_PARAMETER та за замовчуванням TARGET_ALL.

Але будьте обережні, перевірка правильності чи неправильності використання відбувається, на диво, лише при виклику методу newInstance(). Сам компілятор цієї перевірки не виконує.

Майбутнє з атрибутами

Завдяки атрибутам і новим типам, коментарі до документації PHP вперше в своїй історії стануть дійсно тільки документальними коментарями. PhpStorm вже поставляється з кастомними атрибутами, які можуть замінити, наприклад, анотацію @deprecated. І можна припустити, що колись цей атрибут буде в PHP за замовчуванням. Аналогічно будуть замінені й інші анотації, такі як @throws тощо.

Хоча Nette використовує анотації з найпершої версії для позначення постійних параметрів і компонентів, вони не використовувалися більш масово, оскільки не були конструкцією рідної мови, тому редактори не пропонували їх, і було легко помилитися. Хоча це вже вирішується за допомогою плагінів для редакторів, справді рідний спосіб, який приносять атрибути, відкриває абсолютно нові можливості.

До речі, атрибути отримали виняток у стандарті кодування Nette, який вимагає, щоб ім'я класу, окрім специфіки (наприклад, Product, InvalidValue), містило ще й узагальнення (наприклад, ProductPresenter, InvalidValueException). Інакше при використанні в коді буде незрозуміло, що саме представляє клас. Для атрибутів це не бажано, тому клас називається Inject замість InjectAttribute.

*В останній частині ми розглянемо, які нові функції і класи з'явилися в PHP і познайомимося з компілятором Just in Time.