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.

Получение атрибутов

Какие атрибуты имеют отдельные элементы, мы узнаем с помощью рефлексии. Рефлексивные классы имеют новый метод getAttributes(), который возвращает массив объектов ReflectionAttribute.

use MyAttributes\Example;

#[Example('Привет', 123)]
class Foo
{}

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

foreach ($reflection->getAttributes() as $attribute) {
	$attribute->getName();      // полное имя атрибута, например MyAttributes\Example
	$attribute->getArguments(); // ['Привет', 123]
	$attribute->newInstance();  // возвращает экземпляр new MyAttributes\Example('Привет', 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 Coding Standard, который требует, чтобы имя класса, помимо специфичности (например, Product, InvalidValue), содержало и общность (то есть ProductPresenter, InvalidValueException). Иначе при использовании в коде не было бы очевидно, что именно представляет класс. У атрибутов это, наоборот, нежелательно, поэтому класс называется Inject вместо InjectAttribute.

В последней части мы рассмотрим, какие новые функции и классы появились в PHP, и представим Just in Time Compiler.