PHP 23931 ~ 7 мин.

PHP 8: Атрибуты в PHP

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:


public function doSomething(): void

alesha@alesha.com27.11.2023

откуда появился $event в конструкторе???

#[Attribute]
class ListensTo
{
public string $event;

public function __construct(string $eventClass)
{
$this->event = $event;
}
}

Сергей Мухин 28.11.2023

Спокойнее...все гораздо проще) это я с именованием аргументов перемудрил, спасибо за информирование, поправил наименование

Лысов Кирилл06.03.2023

Салам братья

Валерий13.02.2023

Сложновато для понимая. Нет в целом понятно, но когда погружаешься в детали...
Хотел повторить, погонять код на сервере.
Но не понял, что такое ReflectionClass. Его определения нигде нет в статье.
ЗЫ:
Догадываюсь откуда ноги растут у этих примеров. Из Симфони?
Но фреймворками пока не пользуюсь.

Сергей Мухин 14.02.2023

Согласен, что при первом знакомстве с Аттрибутами возникают вопросы. Но потом когда приходит осознание, пользоваться ими одно удовольствие)
ReflectionClass - набор классов, с помощью которых мы можем создавать объекты для разных типов данных в PHP, а затем можно творить с ними всё что только вздумается.
Да, верно подмечено, что первое на ум приходит Симфони с ее аннотациями, которые теперь являются аттрибутами.
Но тут скорее близкое родство с аттрибутами в C# или декораторами в Python.

Категории
  • PHP 68
  • Заметки 18
  • Безопасность 4
  • Флуд 2
  • Nginx 2
  • ИТ новости 2
  • Видео 1
  • Docker 1
  • Roadmap 1
  • Архитектура 0

Хочешь поддержать сайт?

Делаем из мухи слона

sergeymukhin.com

персональный блог о веб-разработке от Сергея Мухина. Блог был основан в 2018 году, и собирался уделять основное внимание последним тенденциям, учебным пособиям, а также советам и рекомендациям, позволяющим начинающим девелоперам встать быстрее на правильную дорогу веб разработки, но что-то пошло не так 😃

Релизы PHP 8.4

Дата Релиз
4 Июля 2024 Альфа 1
18 Июля 2024 Альфа 2
1 Августа 2024 Альфа 3
13 Августа 2024 Feature freeze
15 Августа 2024 Бета 1
29 Августа 2024 Бета 2
12 Сентября 2024 Бета 3
26 Сентября 2024 RC 1
10 Октября 2024 RC 2
24 Октября 2024 RC 3
7 Ноября 2024 RC 4
21 Ноября 2024 GA

Что нового?