PHP 8: Атрибуты в PHP
В PHP 8 мы сможем использовать атрибуты.
Подобные концепции существуют в других языках, которые называются аннотации, или декораторы в Python, Javascript. Цель этих атрибутов заключается в добавлении структурированных, синтаксических метаданных классам, методам, переменным. Концепция атрибутов совсем не нова, мы уже давно используем докблоки для моделирования их поведения. Однако с добавлением атрибутов у нас появляется возможность определять директивы конфигурации, непосредственно внедренные с объявлением этого кода.
Введение
Для начала, вот как атрибут будет выглядеть в коде:
use \Support\Attributes\ListensTo;
class ProductSubscriber
{
#[ListensTo(ProductCreated::class)]
public function onProductCreated(ProductCreated $event) { /* … */ }
#[ListensTo(ProductDeleted::class)]
public function onProductDeleted(ProductDeleted $event) { /* … */ }
}
Я думаю, что events subscriber - хороший пример, чтобы объяснить использование атрибутов.
Кроме того, да, я знаю, синтаксис может быть не таким, как мы этого хотели или надеялись. Возможно, лучше всего было использовать @, или @:, или докблоки, как вариант ... Но атрибуты завершены, они с нами останутся такими, поэтому придется справляться с ними в таком виде. Единственное, что стоит упомянуть о синтаксисе, - это то, что все варианты были обсуждены, и есть очень веские причины, по которым был выбран именно этот синтаксис. Вы можете прочитать краткое резюме об этом в RFC, или прочитать всю дискуссию о RFC в internals list.
Прежде всего, пользовательские атрибуты - это простые классы, аннотируемые самим атрибутом #[Attribute]. Изначально предлагалось заключать в <<>>, в исходном RFC, но впоследствии была изменена другим RFC.
Вот как это будет выглядеть:
#[Attribute]
class ListensTo
{
public string $event;
public function __construct(string $event)
{
$this->event = $event;
}
}
Вот и все - довольно просто, верно? Имейте в виду атрибуты предназначены только для добавления метаданных к классам и методам, не более того. Они не должны и не могут использоваться, например, для проверки ввода аргументов. Другими словами: у вас не будет доступа к параметрам, переданным методу в его атрибутах.
Был RFC, который позволял такое поведение, но этот RFC определенно сделал подход к атрибутам более простыми.
Вернемся к нашему примеру подписчика: нам все еще нужно прочитать метаданные и зарегистрировать наших подписчиков где-нибудь. Исходя из опыта Laravel, я бы использовал service provider для этого, но не стесняйтесь придумывать другие решения.
Вот скучная шаблонная настройка, просто чтобы обеспечить небольшой контекст:
class EventServiceProvider extends ServiceProvider
{
// In real life scenarios,
// we'd automatically resolve and cache all subscribers
// instead of using a manual array.
private array $subscribers = [
ProductSubscriber::class,
];
public function register(): void
{
// The event dispatcher is resolved from the container
$eventDispatcher = $this->app->make(EventDispatcher::class);
foreach ($this->subscribers as $subscriber) {
// We'll resolve all listeners registered
// in the subscriber class,
// and add them to the dispatcher.
foreach (
$this->resolveListeners($subscriber)
as [$event, $listener]
) {
$eventDispatcher->listen($event, $listener);
}
}
}
}
Обратите внимание, что [$event, $listener] это просто деструктуризация массива. Теперь давайте поближе посмотрим resolveListeners, где и происходит волшебство:
private function resolveListeners(string $subscriberClass): array
{
$reflectionClass = new ReflectionClass($subscriberClass);
$listeners = [];
foreach ($reflectionClass->getMethods() as $method) {
$attributes = $method->getAttributes(ListensTo::class);
foreach ($attributes as $attribute) {
$listener = $attribute->newInstance();
$listeners[] = [
// The event that's configured on the attribute
$listener->event,
// The listener for this event
[$subscriberClass, $method->getName()],
];
}
}
return $listeners;
}
Вы можете видеть, что мета-данные теперь проще читать по сравнению с анализом докблока. Есть две тонкости, которые стоит рассмотреть.
Сначала производится вызов $attribute->newInstance(). Здесь создается наш класс пользовательских атрибутов. Он будет принимать параметры, перечисленные в определении атрибута в нашем классе подписчика, и передавать их конструктору.
Конечно технически, вам даже не нужно создавать пользовательский атрибут. Вы можете вызвать $attribute->getArguments() напрямую. Тем не менее, вам все равно нужен пользовательский класс, иначе возникнет ошибка. Более того, создание экземпляра класса означает, что вы получаете гибкость конструктора для ввода разбора любым удобным для вас способом. В целом я бы сказал, что было бы хорошо всегда создавать экземпляр атрибута с помощью newInstance().
Второе, что стоит упомянуть, это использование функции ReflectionMethod::getAttributes(), которая возвращает все атрибуты для метода. Вы можете передать ему два аргумента, чтобы отфильтровать его вывод.
Чтобы понять эту фильтрацию, нужно еще кое-что узнать об атрибутах. Возможно, это было очевидно для вас, но я все равно хотел бы упомянуть об этом: возможно добавить несколько атрибутов к одному и тому же методу, классу, свойству или константе.
Вы могли бы, например, сделать это:
#[
Route(Http::POST(), '/products/create')
Autowire
]
class ProductsCreateController
{
public function __invoke() { /* … */ }
}
Имея это в виду, понятно, почему от Reflection*::getAttributes() возвращается массив, поэтому давайте посмотрим, как его выходные данные могут быть отфильтрованы.
Допустим, вы анализируете маршруты контроллера, вас интересует только Route атрибут. Вы можете легко передать этот класс в качестве фильтра:
$attributes = $reflectionClass->getAttributes(Route::class);
Второй параметр изменяет способ фильтрации. Вы можете передать ReflectionAttribute::IS_INSTANCEOF, что вернет все атрибуты, реализующие данный интерфейс.
Например, если вы анализируете определения контейнеров, основанные на нескольких атрибутах, вы можете сделать что-то вроде этого:
$attributes = $reflectionClass->getAttributes(
ContainerAttribute::class,
ReflectionAttribute::IS_INSTANCEOF
);
Техническая теория
Теперь, когда у вас есть представление о том, как атрибуты работают на практике, пришло время еще немного теории, чтобы убедиться, что вы понимаете их полностью. Как я уже упомянул, атрибуты могут быть добавлены в разные места.
В классы, и анонимные классы;
#[ClassAttribute]
class MyClass { /* … */ }
$object = new #[ObjectAttribute] class () { /* … */ };
Свойства и константы;
#[PropertyAttribute]
public int $foo;
#[ConstAttribute]
public const BAR = 1;
Методы и функции;
#[MethodAttribute]
public function doSomething(): void { /* … */ }
#[FunctionAttribute]
function foo() { /* … */ }
Замыкания;
$closure = #[ClosureAttribute] fn() => /* … */;
И параметры метода и функции;
function foo(#[ArgumentAttribute] $bar) { /* … */ }
Они могут быть объявлены до или после докблока;
/** @return void */
#[MethodAttribute]
public function doSomething(): void { /* … */ }
И могут принимать no одному или несколько аргументов, которые определены конструктором атрибута:
#[Listens(ProductCreatedEvent::class)]
#[Autowire]
#[Route(Http::get(), '/products/create')]
Что касается разрешенных параметров, которые вы можете передать атрибуту, вы уже видели, что разрешены константы классов, ::class имена и скалярные типы. еще можно уточнить то, что атрибуты принимают только постоянные выражения в качестве входных аргументов.
Это означает, что разрешены скалярные выражения - даже битовые сдвиги - а также ::class константы, распаковки массива и массивов, логические выражения и нуллевой оператор объединения. Список всего, что разрешено в качестве константного выражения, можно найти в исходном коде.
#[AttributeWithScalarExpression(1+1)]
#[AttributeWithClassNameAndConstants(PDO::class, PHP_VERSION_ID)]
#[AttributeWithClassConstant(Http::POST)]
#[AttributeWithBitShift(4 ] 1, 4 #[ 1)]
Конфигурация атрибутов
По умолчанию атрибуты могут быть добавлены в нескольких местах, как уже было сказано выше. Однако можно настроить их так, чтобы они могли использоваться только в определенных местах. Например, вы можете сделать так, чтобы ClassAttribute можно было использовать только на классах, и больше нигде. Включение этого поведения осуществляется путем передачи определенного флага Attribute в классе атрибута.
Это выглядит так:
#[Attribute(Attribute::TARGET_CLASS)]
class ClassAttribute
{
}
Доступны следующие флаги:
Attribute::TARGET_CLASS
Attribute::TARGET_FUNCTION
Attribute::TARGET_METHOD
Attribute::TARGET_PROPERTY
Attribute::TARGET_CLASS_CONSTANT
Attribute::TARGET_PARAMETER
Attribute::TARGET_ALL
Это флаги битовой маски, поэтому вы можете комбинировать их, используя двоичную операцию ИЛИ.
#[Attribute(Attribute::TARGET_METHOD|Attribute::TARGET_FUNCTION)]
class ClassAttribute
{
}
Другой флаг конфигурации о повторяемости. По умолчанию один и тот же атрибут не может быть применен дважды, если он не помечен как повторяемый. Это делается так же, как целевая конфигурация, с битовым флагом.
#[Attribute(Attribute::IS_REPEATABLE)]
class ClassAttribute
{
}
Обратите внимание, что все эти флаги проверяются только при вызове $attribute->newInstance(), а не ранее.
Встроенные атрибуты
Одним из основных вариантов использования атрибутов будет ядро и расширения PHP. Одним из таких примеров является атрибут #[Deprecated],
// an idea, not part of the RFC
use Php\Attributes\Deprecated;
#[Deprecated("Use bar() instead")]
function foo() {}
а популярным примером является атрибут #[Jit] - если вы не знаете, что это такое, то можете прочитать мой пост о том, что такое JIT.
use Opcache\Jit;
#[Jit]
function foo() {}
Я уверен, что мы увидим все больше и больше встроенных атрибутов в будущем. В заключение, для тех, кто беспокоится о дженериках: синтаксис не будет конфликтовать с ними, если они когда-либо будут добавлены в PHP, так что мы в безопасности!
Александр09.01.2024
/** @return void */
Я так понимаю на атрибуты такое не заменить? Почему?
Сергей Мухин 09.01.2024
Нет) это просто был пример, чтобы показать что Атрибуты можно использовать как до так и после аннотаций.
Тем более что тут явно указан тип возвращаемого значения void: