Skip to content

Мета-документ диспетчера событий

1. Резюме

Цель данного документа — описать обоснование и логику, лежащие в основе спецификации Диспетчера событий.

2. Зачем это нужно?

Многие библиотеки, компоненты и фреймворки уже давно поддерживают механизмы, позволяющие произвольному стороннему коду взаимодействовать с ними. Большинство из них являются вариациями классического паттерна «Наблюдатель» (Observer), зачастую реализованного через промежуточный объект или сервис. Другие придерживаются подхода аспектно-ориентированного программирования (AOP). Тем не менее все они опираются на одну и ту же базовую концепцию: прервать выполнение программы в фиксированной точке, чтобы предоставить произвольным сторонним библиотекам информацию о выполняемом действии и позволить им либо реагировать на него, либо влиять на поведение программы.

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

3. Область применения

3.1 Цели

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

3.2 Цели, находящиеся вне области применения

  • Асинхронные системы часто имеют понятие «цикла событий» (event loop) для управления чередованием корутин. Это не связанная с данной спецификацией тема и явно выходит за её рамки.
  • Системы хранения данных, реализующие паттерн «Источник событий» (Event Source), также оперируют понятием «события». Это не связано с событиями, рассматриваемыми здесь, и явно выходит за рамки данной спецификации.
  • Строгая обратная совместимость с существующими системами событий не является приоритетом и не предполагается.
  • Хотя данная спецификация неизбежно предлагает паттерны реализации, она не стремится определить Единственно Верную Реализацию Диспетчера событий — лишь то, как вызывающий код и Слушатели взаимодействуют с этим Диспетчером.

4. Подходы

4.1 Рассматриваемые варианты использования

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

  • Одностороннее уведомление. («Я сделал что-то — если тебе интересно, знай».)
  • Обогащение объекта. («Вот объект — измени его, прежде чем я что-то с ним сделаю».)
  • Сбор данных. («Передай мне все свои данные, чтобы я мог что-то с ними сделать».)
  • Альтернативная цепочка. («Вот объект — первый из вас, кто может с ним справиться, пусть сделает это и остановится».)

При дальнейшем рассмотрении Рабочая группа пришла к выводу, что:

  • Сбор данных является частным случаем обогащения объекта (коллекция — это и есть объект, который обогащается).
  • Альтернативная цепочка также является частным случаем обогащения объекта, поскольку сигнатура идентична, а рабочий процесс диспетчеризации почти аналогичен, хотя и включает дополнительную проверку.
  • Одностороннее уведомление — вырожденный случай остальных или может быть представлено как таковое.

Хотя концептуально одностороннее уведомление может выполняться асинхронно (в том числе с задержкой через очередь), на практике существует мало явных реализаций такой модели, что даёт меньше ориентиров для уточнения деталей (например, корректной обработки ошибок). После длительного обсуждения Рабочая группа решила не создавать отдельного рабочего процесса для одностороннего уведомления, поскольку оно вполне может быть представлено как вырожденный случай остальных.

4.2 Примеры применения

  • Уведомление об изменении конфигурации системы или о действии пользователя с возможностью для других систем реагировать способами, не влияющими на ход выполнения программы (например, отправка электронного письма или запись в журнал).
  • Передача объекта серии Слушателей для изменения перед сохранением в систему хранения данных.
  • Передача коллекции серии Слушателей для регистрации значений или изменения существующих, чтобы Отправитель мог действовать на основе всех собранных данных.
  • Передача контекстной информации серии Слушателей, позволяющей каждому из них «проголосовать» за вариант действия, с последующим принятием решения Отправителем на основе сводной информации.
  • Передача объекта серии Слушателей с возможностью для любого Слушателя досрочно завершить процесс до того, как остальные Слушатели завершат работу.

4.3 Неизменяемые события

Изначально Рабочая группа стремилась определить все события как неизменяемые объекты-сообщения, аналогично PSR-7. Однако это оказалось проблематичным во всех случаях, кроме одностороннего уведомления. В остальных сценариях Слушателям требовался способ возврата данных вызывающей стороне. Концептуально было рассмотрено три возможных подхода:

  • Сделать Событие изменяемым и изменять его на месте.
  • Требовать, чтобы события были эволюционируемыми (неизменяемыми, но с методами with*(), как в PSR-7 и PSR-13), а Слушатели возвращали Событие для передачи дальше.
  • Сделать Событие неизменяемым, но агрегировать и возвращать возвращаемое значение каждого Слушателя.

Однако Останавливаемые события (случай альтернативной цепочки) также нуждались в канале для указания на то, что дальнейшие Слушатели не должны вызываться. Это могло быть реализовано одним из способов:

  • Изменение Событий (например, вызов метода stopPropagation()).
  • Возврат дозорного значения из Слушателя (true или false) для указания на завершение распространения.
  • Эволюция Событий в остановленное состояние (withPropagationStopped()).

Каждый из этих вариантов имеет недостатки. Первый означает, что, по крайней мере для указания статуса распространения, события должны быть изменяемыми. Второй требует от Слушателей возвращать значение, по меньшей мере когда они намерены остановить распространение события; это может иметь последствия для существующих библиотек и потенциальные проблемы в плане документирования. Третий требует, чтобы Слушатели всегда возвращали Событие или изменённое Событие, а Диспетчеры — проверяли, является ли возвращаемое значение тем же типом, что и переданное Слушателю; это фактически возлагает бремя как на потребителей, так и на разработчиков реализаций, порождая больше потенциальных проблем при интеграции.

Кроме того, желаемой возможностью было определение необходимости остановки распространения на основе значений, собранных от Слушателей. (Например, остановиться, когда один из них предоставил определённое значение, или после того, как не менее трёх из них подняли флаг «отклонить запрос», или в иных подобных случаях.) Хотя технически это возможно реализовать с использованием эволюционируемых объектов, такое поведение по своей природе является зависимым от состояния, что делает его крайне неудобным как для разработчиков реализаций, так и для пользователей.

Возврат эволюционируемых событий из Слушателей также представлял трудности. Этот паттерн не используется ни одной известной реализацией ни в PHP, ни где-либо ещё. Кроме того, он возлагает на автора Слушателя обязанность помнить о необходимости вернуть Событие (дополнительная работа) и не возвращать какой-либо другой новый объект, который может оказаться не полностью совместимым с последующими Слушателями (например, подкласс или надкласс Событий).

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

Таким образом, остались два возможных варианта:

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

Под «громоздким» подразумевается необходимость применения многословного синтаксиса и/или реализаций. В первом случае авторы Слушателей должны были бы (а) создать новый экземпляр Событий с переключённым флагом распространения и (б) вернуть новый экземпляр Событий, чтобы Диспетчер мог его проверить:

function (SomeEvent $event) : SomeEvent
{
    // do some work
    return $event->withPropagationStopped();
}

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

foreach ($provider->getListenersForEvent($event) as $listener) {
    $returnedEvent = $listener($event);

    if (! $returnedEvent instanceof $event) {
        // This is an exceptional case!
        //
        // We now have an event of a different type, or perhaps nothing was
        // returned by the listener. An event of a different type might mean:
        //
        // - we need to trigger the new event
        // - we have an event mismatch, and should raise an exception
        // - we should attempt to trigger the remaining listeners anyway
        //
        // In the case of nothing being returned, this could mean any of:
        //
        // - we should continue triggering, using the original event
        // - we should stop triggering, and treat this as a request to
        //   stop propagation
        // - we should raise an exception, because the listener did not
        //   return what was expected
        //
        // In short, this becomes very hard to specify, or enforce.
    }

    if ($returnedEvent instanceof StoppableEventInterface
        && $returnedEvent->isPropagationStopped()
    ) {
        break;
    }
}

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

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

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

4.4 Регистрация слушателей

Эксперименты в ходе разработки спецификации показали, что существует широкий спектр жизнеспособных и законных способов, которыми Диспетчер может быть уведомлён о Слушателе. Слушатель:

  • может быть зарегистрирован явно;
  • может быть зарегистрирован явно на основе рефлексии его сигнатуры;
  • может быть зарегистрирован с числовым приоритетом;
  • может быть зарегистрирован с помощью механизма «до/после» для более точного управления порядком;
  • может быть зарегистрирован из контейнера сервисов;
  • может использовать этап предварительной компиляции для генерации кода;
  • может определяться на основе имён методов объектов в самом Событии;
  • может быть ограничен определёнными ситуациями или контекстами на основе произвольно сложной логики (только для определённых пользователей, только в определённые дни, только при наличии определённых системных настроек и т.д.).

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

Поэтому Рабочая группа решила инкапсулировать регистрацию Слушателей за интерфейсом ListenerProviderInterface. Объект-Поставщик может иметь доступный явный механизм регистрации, или несколько таких механизмов, или ни одного. Он также может быть сгенерированным кодом, полученным в ходе этапа компиляции. Однако это также разделяет ответственность за управление процессом диспетчеризации Событий и процессом сопоставления Событий со Слушателями. Таким образом, различные реализации могут сочетаться с различными механизмами Поставщиков по мере необходимости.

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

Хотя объединение Диспетчера и Поставщика в единый объект является допустимым и разрешённым вырожденным случаем, это НЕ РЕКОМЕНДУЕТСЯ, поскольку снижает гибкость системных интеграторов. Вместо этого Поставщик СЛЕДУЕТ реализовывать как зависимый объект.

4.5 Отложенные слушатели

Спецификация требует, чтобы все вызываемые объекты, возвращённые Поставщиком, ОБЯЗАТЕЛЬНО были вызваны (если распространение явно не остановлено) до того, как Диспетчер вернёт управление. Однако спецификация также прямо указывает, что Слушатели могут ставить события в очередь для последующей обработки, а не выполнять немедленное действие. Также вполне допустимо, чтобы Поставщик принимал регистрацию вызываемого объекта, а затем оборачивал его в другой вызываемый объект перед возвратом Диспетчеру. (В этом случае обёртка является Слушателем с точки зрения Диспетчера.) Это позволяет всем следующим поведениям быть законными:

  • Поставщики возвращают вызываемые Слушателей, которые были им предоставлены.
  • Поставщики возвращают вызываемые объекты, создающие запись в очереди, которая отреагирует на Событие другим вызываемым объектом в некоторый момент в будущем.
  • Слушатели сами могут создать запись в очереди, которая отреагирует на Событие в некоторый момент в будущем.
  • Слушатели или Поставщики могут инициировать асинхронную задачу, если выполняются в среде с поддержкой асинхронного поведения (при условии, что результат асинхронной задачи не требуется Отправителю).
  • Поставщики могут применять такую задержку или оборачивание к Слушателям избирательно, на основе произвольной логики.

В итоге именно Поставщики и Слушатели несут ответственность за определение того, когда безопасно откладывать ответ на Событие до некоторого момента в будущем. В этом случае Поставщик или Слушатель явно отказывается от возможности передавать значимые данные обратно Отправителю, однако Рабочая группа пришла к выводу, что именно они находятся в наилучшем положении для оценки безопасности такого подхода.

Хотя технически это является следствием дизайна, такой подход по существу аналогичен используемому в Laravel (начиная с Laravel 5) и доказал свою состоятельность на практике.

4.6 Возвращаемые значения

Согласно спецификации, Диспетчер ОБЯЗАН возвращать Событие, переданное Отправителем. Это требование продиктовано стремлением обеспечить более удобный опыт использования, допускающий сокращения, подобные следующим:

$event = $dispatcher->dispatch(new SomeEvent('some context'));

$items = $dispatcher->dispatch(new ItemCollector())->getItems();

Однако интерфейс EventDispatcher::dispatch() не имеет указанного возвращаемого типа. Это обусловлено прежде всего обратной совместимостью с существующими реализациями, чтобы им было проще перейти на новый интерфейс. Кроме того, поскольку события могут быть любыми произвольными объектами, возвращаемым типом мог бы быть только object, что дало бы лишь минимальную (хотя и ненулевую) пользу: такое объявление типа не предоставило бы IDE никакой полезной информации и не обеспечило бы принудительного возврата того же Событий. Таким образом, возвращаемый тип метода был оставлен синтаксически неуказанным. Тем не менее возврат того же объекта Событий из dispatch() по-прежнему является требованием, и его несоблюдение является нарушением спецификации.

5. Участники

Рабочая группа по Менеджеру событий состояла из:

5.1 Редактор

  • Larry Garfield

5.2 Спонсор

  • Cees-Jan Kiewiet

5.3 Члены рабочей группы

  • Benjamin Mack
  • Elizabeth Smith
  • Ryan Weaver
  • Matthew Weier O'Phinney

6. Голосования

7. Связанные ссылки