• Время чтения ~2 мин
  • 04.08.2023

Запускаете устаревшее приложение Laravel? Получите мгновенное автоматическое обновление Laravel с помощью Laravel Shift

Начиная с PHP 8, мы сможем использовать атрибуты. Цель этих атрибутов, также известных как аннотации во многих других языках, состоит в том, чтобы добавлять метаданные к классам, методам, переменным и тому подобному; в структурированном виде.

Концепция атрибутов совсем не нова, мы уже много лет используем docblocks для моделирования их поведения. Однако с добавлением атрибутов у нас теперь есть первоклассный гражданин в языке для представления такого рода метаданных, вместо того, чтобы вручную анализировать блоки документов.

Так как же они выглядят? Как мы делаем пользовательские атрибуты? Есть ли какие-то предостережения? Это вопросы, на которые будут даны ответы в этом посте. Давайте погрузимся!

# Краткое изложение

Перво-наперво, вот как атрибут будет выглядеть в дикой природе:

use \Support\Attributes\ListensTo;
class ProductSubscriber
{
    #[ListensTo(ProductCreated::class)]
    public function onProductCreated(ProductCreated $event) { /* … */ }
    #[ListensTo(ProductDeleted::class)]
    public function onProductDeleted(ProductDeleted $event) { /* … */ }
}

Я покажу другие примеры позже в этом посте, но я думаю, что пример подписчиков событий хорош, чтобы сначала объяснить использование атрибутов.

Также да, я знаю, что синтаксис может быть не таким, как вы хотели или надеялись. Возможно, вы предпочли @, или , или docblocks, или@:, ... Тем не менее, он здесь, чтобы остаться, поэтому нам лучше научиться справляться с ним. Единственное, что стоит упомянуть о синтаксисе, это то, что все варианты были обсуждены, и есть очень веские причины, по которым был выбран этот синтаксис. Вы можете прочитать всю дискуссию о RFC в списке внутренних компонентов.

При этом давайте сосредоточимся на крутых вещах: как это ListensTo будет работать под капотом?

Прежде всего, пользовательские атрибуты — это простые классы, аннотированные атрибутом#[Attribute]; эта база Attribute раньше вызывалась PhpAttribute в исходном RFC, но впоследствии была изменена другим 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
);

Это хорошее сокращение, встроенное в ядро.

Заметили тпё? Вы можете отправить PR, чтобы исправить это. Если вы хотите быть в курсе того, что происходит в этом блоге, вы можете подписаться на меня в Твиттере или подписаться на мою рассылку:

# Техническая теория

Теперь, когда у вас есть представление о том, как атрибуты работают на практике, пришло время для дополнительной теории, убедившись, что вы понимаете их досконально. Прежде всего, я вкратце упомянул об этом ранее, атрибуты могут быть добавлены в нескольких местах.

В классах, а также анонимных классах;

#[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) { /* … */ }

Они могут быть объявлены до или после docblocks;

/** @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

Это флаги битовых масок, поэтому их можно комбинировать с помощью двоичной операции ИЛИ.

#[Attribute(Attribute::TARGET_METHOD|Attribute::TARGET_FUNCTION)]
class ClassAttribute
{
}

Еще один флаг конфигурации касается повторяемости. По умолчанию один и тот же атрибут не может быть применен дважды, если он специально не помечен как повторяемый. Это делается так же, как и целевая конфигурация, с битовым флагом.

#[Attribute(Attribute::IS_REPEATABLE)]
class ClassAttribute
{
}

Обратите внимание, что все эти флаги проверяются только при вызове$attribute->newInstance(), а не ранее.

# Встроенные атрибуты После

того, как базовый RFC был принят, появились новые возможности для добавления встроенных атрибутов в ядро. Одним из таких примеров является #[Deprecated] атрибут, и популярным примером был #[Jit] атрибут — если вы не уверены, о чем этот последний, вы можете прочитать мой пост о том, что такое JIT.

Я уверен, что в будущем мы увидим все больше и больше встроенных атрибутов.

В качестве последнего замечания для тех, кто беспокоится о дженериках: синтаксис не будет конфликтовать с ними, если они когда-либо будут добавлены в PHP, так что мы в безопасности!



У меня уже есть несколько вариантов использования атрибутов, а как насчет вас? Если у вас есть какие-то мысли об этой удивительной новой функции в PHP 8, вы можете связаться со мной в Twitter или по электронной почте, или мы можем обсудить это на Reddit.

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

Про мене

Professional Fullstack Developer with extensive experience in website and desktop application development. Proficient in a wide range of tools and technologies, including Bootstrap, Tailwind, HTML5, CSS3, PUG, JavaScript, Alpine.js, jQuery, PHP, MODX, and Node.js. Skilled in website development using Symfony, MODX, and Laravel. Experience: Contributed to the development and translation of MODX3 i...

Об авторе CrazyBoy49z
WORK EXPERIENCE
Контакты
Ukraine, Lutsk
+380979856297