company_banner

PSR-14 — главное событие в PHP

Original author: Larry (Crell) Garfield
  • Translation
В прошлом году PHP-FIG — Группа концепций совместимости PHP, выпустила несколько новых спецификаций. Последняя из них — PSR-14, посвящена диспетчеризации событий. Как и другие PSR, это локальная спецификация, но имеет большое влияние на многие аспекты стандартизации.

От переводчика: Это перевод первой части целой серии публикаций, в которой Larry (Crell) Garfield, один из членов PHP-FIG, объясняет, что такое PSR-14, на что способен, а на что нет, и как лучше всего использовать его в своих проектах.

Цель


Диспетчеризация событий давно используется во многих языках. Если вы когда-нибудь использовали EventDispatcher в Symfony, Event system в Laravel, хуки в Drupal, Event Manager во фреймворке Zend, пакет League\Event, или что-то подобное, то понимаете о чём речь.

В общем смысле, все эти системы представляют из себя некую форму «наблюдателя-посредника». Один фрагмент кода отправляет сообщение типа — «событие», а посредник передает его другим отдельным фрагментам кода — «слушателям». Иногда сигнал направлен только в одну сторону, иногда «слушатель» может как-то передавать данные обратно вызывающей стороне. Конечно же, они все разные и не очень совместимы между собой.

Это проблема для автономных библиотек, которые хотят подключаться к различным библиотекам и приложениям. Многие библиотеки можно расширить через отправку событий в той или иной форме, чтобы другой код мог связаться с ними. Но такой посреднический слой, фактически, проприетарный. Библиотека, которая запускает Symfony EventDispatcher, затем объединяется с Symfony. Тогда использование ее где-то еще требует установки EventDispatcher и соединения с библиотеками в программе. Библиотека, которая вызывает связывающую систему от Drupal module_invoke_all(), затем связывается с Drupal. И так далее.

Цель PSR-14 — избавить библиотеки от этой зависимости. Это позволяет библиотекам расширяться через тонкий общий слой, и потом облегчит их перенос в другую среду без дополнительных усилий и затрат, например, в Symfony, Zend Framework, Laravel, TYPO3, eZ Platform или Slim. Пока у среды есть совместимость с PSR-14, всё будет работать.

Спецификация


Как уже говорил, спецификация довольно легкая. Это три интерфейса в одном методе и мета-описание, как их использовать. Все просто и удобно. Ниже код этих интерфейсов (без комментариев для экономии места).

namespace Psr\EventDispatcher;

interface EventDispatcherInterface
{
    public function dispatch(object $event);
}

interface ListenerProviderInterface
{
    public function getListenersForEvent(object $event) : iterable;
}

interface StoppableEventInterface
{
    public function isPropagationStopped() : bool;
} 

Первые два это ядро спецификации. StoppableEventInterface — это расширение, к которому вернёмся позже.

Думаю, EventDispatcher большинству из вас знаком — это всего лишь объект с методом, которому вы передаете событие — посредник, о котором уже говорили. Само событие, однако, не определено — им может быть любой PHP-объект. Подробнее об этом позже.

Большинство существующих реализаций имеют один объект или набор функций, которые работают как посредник или диспетчер и место для регистрации кода, которое получает событие (слушателей). Для PSR-14 мы сознательно разделили эти две обязанности на отдельные объекты. Диспетчер получает список слушателей от объекта поставщика, который составляет этот список.

Откуда тогда поставщик получает список слушателей? Да откуда хочет! Существует миллиард и один способ связать слушателя и событие, все они абсолютно действующие и несовместимые. Еще в начале мы решили, что стандартизация «Единого Истинного Пути» регистрации слушателей будет слишком ограничена. Однако, стандартизировав процесс подключения слушателя к диспетчеру, можно получить отличную гибкость, не заставляя пользователя делать что-то странное и непонятное.

Также в коде не указывается, что представляют из себя слушатель. Им может быть любой способный к восприятию сигнала фрагмент PHP: функция, анонимная функция, метод объекта, всё что угодно. Поскольку вызываемый объект может делать что угодно, это значит, что допустимо иметь в качестве слушателя, скажем, анонимную функцию, которая выполняет отложенную загрузку сервиса из DI-контейнера и вызывает в сервисе метод, который на самом деле и содержит слушающий код.

Вкратце, диспетчер это простой и лёгкий API для авторов библиотек. Поставщики слушателей предлагают надёжный и гибкий API для интеграторов фрэймворков, а отношения между диспетчером и провайдером объединяют их вместе.

Простой пример


В общем виде, схема объединения всех частей в целое, будет выглядеть примерно так.

class Dispatcher implements EventDispatcherInterface
{

    public function __construct(ListenerProviderInterface $provider)
    {
        $this->provider = $provider;
    }

    public function dispatch(object $event)
    {
        foreach ($this->provider->getListenersForEvent($event) as $listener) {
            $listener($event);
        }
       return $event;
    }
}

$dispatcher = new Dispatcher($provider);

$event = new SomethingHappened();
$dispatcher->dispatch($event);

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

Код


PSR-14 уже поддерживается основными фреймворками и приложениями.

  • Matthew Weier O’Phinney уже обязался ввести поддержку PSR-14 в zend-eventmanager 4.0 во фрэймворке Zend.
  • Symfony недавно анонсировали изменения в EventDispatcher для совместимости с PSR-14, что даст полную поддержку в 5.0/5.1.
  • Фрэймворк Yii объявил о намерении интегрировать PSR-14 в версии 3.0.
  • Benni Mack из TYPO3 CMS заявил, что в следующем релизе TYPO3 все существующие концепции типа «ловушка+сигнал/слот» будут поддерживать PSR-14.

Также PSR-14 имеет три полнофункциональные независимые реализации, которые вы уже можете использовать в любом приложении.

  • Tukio от Larry Garfield, автора этой статьи.
  • Phly Event Dispatcher от Matthew Weier O’Phinney.
  • Kart от Benni Mack, который работает как встраиваемый плагин.

Автор благодарит всю рабочую группу PSR: Larry Garfield, Cees-Jan Kiewiet, Benjamin Mack, Elizabeth Smith, Ryan Weaver, Matthew Weier O’Phinney. На протяжении всей работы процесс был в высшей степени продуктивным: все работали вместе, коллективно, как и должно быть. Результат радует, и хотелось бы, чтобы и все дальнейшие усилия в совместной работе над архитектурой были так же продуктивны.

Узнать больше подробностей можно или из оригинала следующей части и документации или 17 мая на PHP Russia. Второй вариант привлекателен по нескольким причинам. Например, глава Программного комитета Александр (samdark) Макаров в числе тех, кто внедрил PSR-14 в Yii. Да и в принципе состав Программного комитета и спикеров невероятно силен, вряд ли найдется хоть одна тема из сферы профессионального использования PHP, которую не удастся обсудить на этой конференции.
Конференции Олега Бунина (Онтико)
440.39
Конференции Олега Бунина
Share post

Comments 27

    +9
    А теперь о реализации, а точнее про тайпхинт «object»:

    1) Тайпхинт object доступен начиная с PHP 7.2, однако почти все фреймворки используют в минималках 7.1
    2) object не доступен для перегрузки, а значит вот такое просто невозможно:
    class Dispatcher implements EventDispatcherInterface
    {
        public function dispatch(EventInterface $event) { ... }
        // Fatal error:  Declaration of Dispatcher::dispatch(EventInterface $event) must be compatible with EventDispatcherInterface::dispatch(object $event)
    }
    

    Как следствие — куча оверхеда и минус консистентность.

    Выводы, думаю, очевидны: PSR-14 — в текущем виде печален и не удивительно, что Фабьен ушёл из PSR после принятия 14го.
      +1
      Давайте искать плюсы. Фреймворки подтянут минимальную версию php до 7.2. Это же хорошо. А по поводу object это конечно слишком общно. Но с другой стороны, а какой интерфейс должен быть о Event? И чтобы всем подошло. Я думаю это тема будущих дискуссий.
        0
        Фреймворки подтянут минимальную версию php до 7.2. Это же хорошо.

        Лишь в следующих мажорных релизах и то вряд ли. И это не плюс, а скорее минус: Мы получаем уменьшение количества поддерживаемых версий PHP из-за одного единственного интерфейса, т.к. объективно 7.2 (как и 7.3) проходные версии, которые ничего особо нового не добавляют и следующий инкремент с 7.1 оправдано делать лишь на 7.4, т.к. там огромное количество киллерфич добавляется.


        Но с другой стороны, а какой интерфейс должен быть о Event? И чтобы всем подошло. Я думаю это тема будущих дискуссий.

        С другой стороны а нужен ли вообще этот PSR-14? У разных фреймворков разные реализации. У Symfony, например, события именованные и внедрение PSR-14 ломает вообще всю обратную совместимость. У Laravel и Zend вообще события содержат обычный массив и тут ситуация ещё хуже.


        А если уж делать PSR полностью, то где вообще addListener, removeListener, и прочее?


        Хорошо, вот ещё пример проблем PSR-14: Почему вообще getListenersForEvent должен содержать объект?


        // Метод из диспатчера Symfony в качестве примера добавления листнера
        $dispatcher->addListener(SomeEvent::class, function () { ... });
        
        // Получение листнеров из Symfony
        $dispatcher->getListeners(SomeEvent::class);
        
        // А это уже PSR
        $dispatcher->getListenersForEvent(new SomeEvent()); // Нахрена тут объект?
          0
          $dispatcher->getListenersForEvent(new SomeEvent()); // Нахрена тут объект?

          Видимо решили так сделать, чтобы диспатчер и/или листенеры могли отличать экземпляры событий одного класса, по event type или event source еще как-то, хз, в зависимости уже от конкретной реализации. Я тоже удивился когда там объектный тайпхинт увидел.
            0
            Видимо решили так сделать, чтобы диспатчер и/или листенеры могли отличать экземпляры событий одного класса, по event type или event source еще как-то, хз, в зависимости уже от конкретной реализации.

            Для этого и у Zend, и у Laravel, и у Symfony и, наверняка, ещё у кучи других есть имя события. Так что можно было бы сделать запросто вот так:


            interface EventInterface
            {
                public function getName(): string;
            }
            
            interface EventDispatcherInterface
            {
                public function dispatch(EventInterface $event);
            }
            
            interface ListenerProviderInterface
            {
                public function getListeners(string $name): iterable;
            }
              0

              Вы посмотрите пример использования. ListenerProviderInterface::getListenersForEvent() вызывается из EventDispatcherInterface::dispatch(), а в этом месте у нас нет идентификатора события, только сам объект события.
              Извлечь идентификатор события из объекта события должен как раз тот, кто отвечает за ведение списка обработчиков, т.е. ListenerProvider. Как он это будет делать — оставлено за рамками стандарта.
              Например слушатель может регистрироваться по имени класса события:


              $listenerProvider->addListener(MyEvent::class, new MyEventListener());

              Ваш вариант, с $event->getName() в принципе тоже возможен, но вот только зачем это вызов делать в EventDispatcher? Если он не отвечает за формирование списка обработчиков, то ему абсолютно не важно, как называется (обозначается) событие.

                0

                Да, я вижу что в dispatch можно передать что угодно, хоть stdClass… Но если это допустимо, то почему нельзя передать строчку? Или массив? Если не делать ограничений на типы объекта, то почему нужно ограничивать объектом? Ну ладно, опустим этот момент.


                А теперь главный вопрос: Вот вы совершенно правильно говорите, что идентификатор должен детектить провайдер, который используется внутри диспетчера. А нахрена в PSR нужен интерфейс провайдера, который является частным случаем реализации диспатчера, должен использоваться только внутри него и совершенно не пригоден для использования вне? PSR как бы про публичные интерфейс, которые дёргаются внутри пользовательских приложений.

                  0
                  PSR как бы про публичные интерфейс, которые дёргаются внутри пользовательских приложений.

                  Не совсем про это они, имхо. Они, скорее, по совместимость компонентов фреймворков, а не про предоставление пользователям фреймворков (разработчикам пользовательских приложений) единого публичного API, чтобы они могли легко писать framework agnostic приложения.

                    0

                    Совместимость компонентов фреймворков? Это чтобы использовать ListenerProviderInterface из зенда и подсовывать его в EventDispatcherInterface из ларки? Именно для таких задач этот провайдер существует?


                    Ну… Это не то что странно… Это вряд ли вообще кому-то вообще нужно, скажем так.

                      0

                      Типа того. Для событий сложно сходу придумать, поскольку новый интерфейс, но, например, логгер с PSR-3: есть в симфони, есть в зенде реализации, есть monolog (сам по себе фреймворк для логгинга), есть в друпале, есть в yii работа с psr-3. Или middleware. Или response-request. Внедрённые во фреймворки стандартные интерфейсы позволяют малой кровью собирать свой фреймворк из компонентов разных вендоров, своих или, например, плавно мигрировать с одного фрейма на другой.

                        0

                        В случае psr-3 берётся LoggerInterface и без каких-либо изменений всовывается в любой фреймворк. Это как раз публичный интерфейс, который можно запросто дёргать и напрямую без всяких обвязок и подмена реализации на другую ничего не изменит. Миддлвари имеют примерно такую же цель. Про этот интерфейс разработчик знает и активно может использовать.


                        А вот EventDispatcherInterface — это внутренняя и частная реализация диспатчера, которая никуда не торчит. Что мы потеряем если вообще удалим этот интерфейс? Какую переносимость потеряем и что не сможем сделать?

        0
        object не доступен для перегрузки, а значит вот такое просто невозможно:

        А зачем нам может потребоваться реализация EventDispatcherInterface которая знает что-то об объекте-событии? Задача диспетчера просто передать событе обработчику, как оно пришло. Дальнейшим анализом обработчик будет заниматься.

          0

          С таким же успехом, перефразируя, можно спросить про то, зачем ему знать об объекте, ведь событием может быть и массив, и строка… Не правда ли? Zend и Laravel тому в пример.


          P.S. Но с другой стороны понятна причина добавления тайпхинта, т.к. есть вот такой RFC: https://wiki.php.net/rfc/covariant-returns-and-contravariant-parameters Позволяющий сузить область при наследовании и указать конкретный тип. Но… Но мы возвращаемся к первому моему примеру с EventInterface, чем он в таком случае не угодил? Пусть даже с пустой реализацией, но мы сразу же выкинем из под категории "событие" весь stdlib php и все вендорные классы, которые не являются потомками EventInterface

            0
            Не обьясняет. Для пхп нельзя обьект сузить до строки или массива.
            Интерфейс — единственное правильное решение.

            Я вообще не понимаю каким образом в стандартах ПСР могут быть не интерфейсы. И следует провоцировать разработчиков языка наконец-то создать полноценные интефейсы вместо кастрированных тайпхинтингов (object, collable, e.t.c.)
              0

              Когда буду алгебраические типы, тогда можно будет. Мои попытки написать RFC на эту тему закончились тем, что их внедрение потребует дохрена всего. Очевидно, что подобные вещи надо внедрять постепенно и ковариантные тайпхинты вполне себе первый шажок в этом направлении.

                0
                Мне кажется, это плохая идея.
                1) еще одна новая сущность — типы. Идете по губительным стопам си — понапихать побольше сущностей. Очевидно, что надо идти в обратном направлении. Уже существующее надо перебрать и упростить.

                2) Большая часть общества это не поддержит. Потому что сложно. Да, я тоже хочу алгебраические типы. Но вот найти команду для проекта в положеный срок — будет тот еще вызов.

                3) Судя по рфц — вы не можете написать патч на ядро. На сколько я слежу за разработкой ядра — никто из тех кто может хочет реализовать эту фичу. Тем более на фоне претубераций в команде за последний год.

                4) Основные училия сейчас направленны на ФФИ. Лично меня они радуют. Должы и Вас. Потому что можно будет писать свой новый ПХП по верх стандартного.
                  0
                  Основные училия сейчас направленны на ФФИ. Лично меня они радуют. Должы и Вас. Потому что можно будет писать свой новый ПХП по верх стандартного.

                  Это вы про бесподобный пост от ircmaxell?

                    0
                    да
            0

            del (задублировалось)

            0

            Тут, по-хорошему, дженерики нужны. Если бы они еще были...

              0
              2. Возможно, с этим и боролись. Т.к. здесь у пакета появляется дополнительная зависимость в виде EventInterface. А для большей переносимости лучше, чтобы аспекты реализации не хранились в интерфейсе.
              Во всяком случае их мысль я понимаю именно так.
              +2
              И как можно универсально использовать эти интерфейсы без универсального формата события? Это всё равно что стандартизировать strpos(string, string), но не указать в какой строке какая строка ищется.
                0

                а я не особо понимаю чем отличается от паттерна наблюдателя и почему не используются встроенные spl интерфейсы

                  0
                  да, мне тоже напомнило, наверное тут будет обобщенно все называться
                0
                Я не понимаю, а как же реализовывается приоритет обработчиков для одного события?
                  0

                  скорее всего в getListenersForEvent, если нужно.
                  как тут писали выше, слишком все какое-то абстрактное #comment_20125570

                    +1
                    Это одна из проблем текущих реализаций.
                    Порядок обработчиков не должен играть роли.
                    Ивенты подразумевают, что слушателей вообще может не быть.

                    зы. StoppableEventInterface лишняя сущность, как по мне. Радует хотябы, что это отдельный интерфейс

                  Only users with full accounts can post comments. Log in, please.