• Час читання ~2 хв
  • 04.08.2023

Використовуєте застарілий додаток Laravel? Отримайте миттєві, автоматизовані оновлення Laravel за допомогою Laravel Shift

Починаючи з PHP 8, ми зможемо використовувати атрибути. Метою цих атрибутів, також відомих як анотації в багатьох інших мовах, є додавання метаданих до класів, методів, змінних та іншого; структуровано.

Концепція атрибутів зовсім не нова, ми вже багато років використовуємо docblocks для моделювання їхньої поведінки. Однак, з додаванням атрибутів, тепер ми маємо першокласного громадянина мовою для представлення такого роду метаданих, замість того, щоб вручну аналізувати 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
);

Це гарне скорочення, вбудоване в ядро.

Помітили tpyo? Ви можете подати PR, щоб виправити це. Якщо ви хочете бути в курсі того, що відбувається в цьому блозі, ви можете стежити за мною в Twitter або підписатися на мій інформаційний бюлетень:

# Технічна теорія

Тепер, коли у вас є уявлення про те, як атрибути працюють на практиці, настав час для додаткової теорії, переконавшись, що ви їх глибоко розумієте. Перш за все, я коротко згадував про це раніше, атрибути можна додавати в декількох місцях.

На заняттях, а також анонімних заняттях;

#[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, константи, масиви та розпакування масивів, логічні вирази та оператор нульового об'єднання. Список усього, що допускається як постійний вираз, можна знайти у вихідному коді.

#[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, тому ми в безпеці!



Я вже маю на увазі кілька варіантів використання атрибутів, а як щодо вас? Якщо у вас є деякі думки, щоб поділитися цією чудовою новою функцією в 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