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.
Чтобы оставить комментарий, пожалуйста, войдите в систему