В прошлом году 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, которую не удастся обсудить на этой конференции.

Комментарии (14)


  1. SerafimArts
    07.05.2019 15:01
    +3

    А теперь о реализации, а точнее про тайпхинт «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. komandakycto
      07.05.2019 15:34

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


      1. SerafimArts
        07.05.2019 16:35

        Фреймворки подтянут минимальную версию php до 7.2. Это же хорошо.

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


        Но с другой стороны, а какой интерфейс должен быть о 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()); // Нахрена тут объект?


        1. vdem
          07.05.2019 18:35

          $dispatcher->getListenersForEvent(new SomeEvent()); // Нахрена тут объект?

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


          1. SerafimArts
            07.05.2019 18:41

            Видимо решили так сделать, чтобы диспатчер и/или листенеры могли отличать экземпляры событий одного класса, по 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;
            }


            1. alsii
              07.05.2019 19:54

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


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

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


              1. SerafimArts
                07.05.2019 20:37

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


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


    1. alsii
      07.05.2019 19:59

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

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


      1. SerafimArts
        07.05.2019 20:40

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


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


      1. SerafimArts
        07.05.2019 20:47

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


    1. symbix
      08.05.2019 00:39

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


    1. shm-vadim
      08.05.2019 09:59

      2. Возможно, с этим и боролись. Т.к. здесь у пакета появляется дополнительная зависимость в виде EventInterface. А для большей переносимости лучше, чтобы аспекты реализации не хранились в интерфейсе.
      Во всяком случае их мысль я понимаю именно так.


  1. MIKEk8
    07.05.2019 15:43

    И как можно универсально использовать эти интерфейсы без универсального формата события? Это всё равно что стандартизировать strpos(string, string), но не указать в какой строке какая строка ищется.


    1. serginhold
      07.05.2019 16:48

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