Начиная с PHP 8 мы сможем использовать атрибуты. Задача этих атрибутов, во многих других языках известных как аннотации, заключается в структурированном добавлении метаданных к классам, методам, переменным и всему остальному.
Сама концепция атрибутов отнюдь не нова: мы годами имитировали их поведение с помощью docblock'ов. Однако с появлением атрибутов мы получили первоклассную поддержку таких метаданных на уровне самого языка, и нам больше не нужно вручную парсить docblock'и.
Так как же они выглядят? Как создавать собственные атрибуты? Есть ли какие-то нюансы? На эти вопросы мы и ответим в этой статье. Поехали!
Краткий обзор
Первым делом посмотрим, как атрибут выглядит на практике:
use \Support\Attributes\ListensTo;
class ProductSubscriber
{
#[ListensTo(ProductCreated::class)]
public function onProductCreated(ProductCreated $event) { /* … */ }
#[ListensTo(ProductDeleted::class)]
public function onProductDeleted(ProductDeleted $event) { /* … */ }
}Дальше в статье я покажу и другие примеры, но мне кажется, что пример с подписчиками на события отлично подходит для первого знакомства с атрибутами.
И да, я знаю: синтаксис может оказаться не совсем тем, на что вы рассчитывали. Возможно, вам больше нравился @ , @: , docblock'и или что-то ещё... Но этот синтаксис с нами надолго, так что лучше научиться с ним работать. Единственное, о чём стоит упомянуть по поводу синтаксиса: обсуждались вообще все возможные варианты, и для выбора именно этого были очень веские причины. Всю дискуссию по RFC можно почитать в рассылке internals.
С этим разобрались, теперь давайте сосредоточимся на самом интересном: как же этот ListensTo работает под капотом?
Прежде всего, кастомные атрибуты — это обычные классы, которые сами помечены атрибутом #[Attribute] ; в исходном RFC этот базовый Attribute назывался PhpAttribute , но затем его изменили в следующем RFC.
Вот как это будет выглядеть:
#[Attribute]
class ListensTo
{
public string $event;
public function __construct(string $event)
{
$this->event = $event;
}
}Вот и всё — предельно просто, не так ли? Помните о назначении атрибутов: они нужны только для добавления метаданных к классам и методам, не более того. Их нельзя — и не получится — использовать, например, для валидации входных аргументов. Иными словами: из атрибутов у вас не будет доступа к параметрам, переданным в метод. В одном из предыдущих RFC такое поведение допускалось, но в этом RFC всё намеренно сделали проще.
Вернёмся к примеру с подписчиком на события: нам всё ещё нужно прочитать метаданные и где-то зарегистрировать наших подписчиков. Поскольку я пришёл из мира Laravel, для этой задачи я бы выбрал сервис-провайдер, но вы можете использовать любые другие решения.
Вот скучный шаблонный код для контекста:
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;
}Как видите, читать метаданные таким способом гораздо проще, чем парсить строки docblock'ов. Однако здесь есть две тонкости, на которые стоит обратить внимание.
Во-первых, вызов $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
);Это удобное сокращение, встроенное прямо в ядро.
Присоединяйтесь к более чем 14 тысячам подписчиков моей рассылки: я пишу о PHP, разработке и делюсь новостями этого блога. Подписаться можно, отправив письмо на brendt@stitcher.io.
Немного теории
Теперь, когда вы представляете, как атрибуты работают на практике, пришло время разобрать теорию, чтобы закрепить понимание. Прежде всего, как я уже вскользь упоминал, атрибуты можно применять в самых разных местах.
В классах, а также в анонимных классах;
#[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) { /* … */ }Их можно объявлять до или после docblock'ов;
/** @return void */
#[MethodAttribute]
public function doSomething(): void { /* … */ }И они могут принимать ноль, один или несколько аргументов, которые определяются конструктором атрибута:
#[Listens(ProductCreatedEvent::class)] #[Autowire] #[Route(Http::POST, '/products/create')]
Что касается аргументов, которые можно передавать в атрибут: вы уже видели, что разрешены константы классов, имена ::class и скалярные типы. Но тут есть важное уточнение: атрибуты принимают в качестве входных аргументов только константные выражения.
Это означает, что разрешены скалярные выражения — даже побитовые сдвиги, — а также ::class , константы, массивы и распаковка массивов, логические выражения и оператор объединения с null. Полный список того, что считается константным выражением, можно найти в исходном коде.
#[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
Это битовые флаги, поэтому их можно комбинировать с помощью побитовой операции OR.
#[Attribute(Attribute::TARGET_METHOD|Attribute::TARGET_FUNCTION)]
class ClassAttribute
{
}Ещё один флаг конфигурации отвечает за повторяемость. По умолчанию один и тот же атрибут нельзя применить дважды, если только он явно не помечен как повторяемый. Это настраивается точно так же, как и цели применения — с помощью битового флага.
#[Attribute(Attribute::IS_REPEATABLE)]
class ClassAttribute
{
}Обратите внимание, что все эти флаги проверяются только при вызове $attribute->newInstance() , не раньше.
Встроенные атрибуты
После принятия базового RFC появилась возможность добавлять встроенные атрибуты прямо в ядро. Один из таких примеров — атрибут #[Deprecated] , а также популярным примером стал атрибут #[Jit] — если не знаете, о чём речь, можете почитать мою статью о том, что такое JIT.
Уверен, в будущем мы увидим всё больше и больше встроенных атрибутов.
И напоследок для тех, кто переживает о дженериках: этот синтаксис не будет с ними конфликтовать, если их когда-нибудь всё-таки добавят в PHP, так что беспокоиться не о чем!
Комментарии (0)
Пока нет комментариев — будьте первым.