PHP 8.0: Атрибути (3/4)
Вийшла версія 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.
Щоб залишити коментар, будь ласка, увійдіть до системи