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

4 года назад от 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),
]

К сожалению, значением аргумента не может быть другой атрибут, т.е. атрибуты не могут быть вложенными. Например, не существует прямого способа преобразования следующей аннотации, используемой в Doctrine, в атрибуты:

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

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

Отражение атрибута

Какие атрибуты есть у отдельных элементов, можно определить с помощью отражения. Классы Reflection имеют новый метод 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 Compiler..

Последние сообщения