Компонент EventDispatcher

Дата обновления перевода: 2021-05-12

Компонент EventDispatcher

Компонент EventDispatcher предоставляет инструменты, которые позволяют компонентам вашего приложения общаться друг с другом, запуская события и слушая их.

Вступление

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

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

Компонент Symfony EventDispatcher реализует шаблоны проектирования Mediator and Observer для того, чтобы сделать всё это возможным и предоставить вашим проектам быть действительно расширяемыми.

Возьмите простой пример из компонента HttpKernel. Когда объект Response уже создан, может быть ползеным позволить другим элементам в системе изменять его (например, добавлять некоторые кеш-заголовки) до его реального использования. Чтобы сделать это возможным, Ядро Symfony вызывает событие - kernel.response. Вот, как оно работает:

  • Слушатель (PHP-объект) сообщает центральному объекту диспетчеру, что он хочет слушать событие kernel.response;
  • В какой-то момент, ядро Symfony сообзщает объекту диспетчеру запустить событие kernel.response, передавая его с объектом Event, который имеет доступ к объекту Response;
  • Диспетчер уведомляет (т.е. вызывает метод) всех слушателей события kernel.response, позволяя каждому из них делать изменения в объекте Response.

Установка

1
$ composer require symfony/event-dispatcher

Также вы можете клонировать репозиторий https://github.com/symfony/event-dispatcher.

Note

Если вы устанавливаете этот компонент вне приложения Symfony, вам нужно подключить файл vendor/autoload.php в вашем коде для включения механизма автозагрузки классов, предоставляемых Composer. Детальнее читайте в этой статье.

Применение

See also

Эта статья объясняет как использовать функции EventDispatcher как независимого компонента в любом приложении PHP. Прочитайте статью События и слушатели событий для понимания как использовать его в приложениях Symfony.

События

Когда развёртывается событие, оно определяется уникальным именем (например, kernel.response), которое может слушать любое количество слушателей. Также создаётся и передаётся всем слушателям экземпляр Event. Как вы увидите позже, сам объект Event часто содержит данные о запускаемом событии.

Соглашения именования

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

  • Используйте только строчные буквы, цифры, точки (.) и нижние подчёркивания (_);
  • Добавляйте к именам префикс пространства имён с точкой (например, order., user.*);
  • Заканчивайте имена глаголом, который обозначает, какое действие было выполнено (например, order.placed).

Имена и объекты событий

Когда диспетчер уведомляет слушателей, он передаёт настоящий объект Event этим слушателям. Базовый класс Event очень прост: он содержит метод для остановки распространения события, и больше ничего.

See also

Прочтите "Объект Generic Event (событие общего назначения)", чтобы узнать больше об этом объекте базового события.

Зачастую, данные о конкретном событии должны быть переданы вместе с объектом Event, чтобы слушатели имели необходимую им информацию. В таком случае, можно передать специальный подкласс, который имеет дополнительные методы для извлечения и переопределения информации, при запуске события. Например, событие kernel.response использует FilterResponseEvent, который содержит методы, чтобы получать и даже заменять объект Response.

Диспетчер

Диспетчер - это центральный объект системы запуска событий. Обычно создаётся один диспетчер, который содержит реестр слушателей. Когда событие запускается через диспетчер, он уведомляет всех слушателей, зарегистрированных с этим событием:

1
2
3
use Symfony\Component\EventDispatcher\EventDispatcher;

$dispatcher = new EventDispatcher();

Соединение слушателей

Чтобы воспользоваться преимуществами существующего события, вам нужно соединить слушателя с диспетчером, чтобы он мог быть уведомлён, когда событие будет запущено. Вызов метода диспетчера addListener() ассоциирует все вызываемые PHP с событием:

1
2
$listener = new AcmeListener();
$dispatcher->addListener('acme.foo.action', array($listener, 'onFooAction'));

Метод addListener() имеет до трёх аргументов:

  1. Имя события (строка), которое хочет слушать этот слушатель;
  2. Вызываемое PHP, которое будет выполнено при запуске указанного события;
  3. Необязательное число приоритета (чем выше - тем важнее, следовательно этот слушатель будет запущен раньше), которое определяет, когда вызывается слушатель по отношению к другим слушателям (по умолчанию 0). Если два слушателя имеют одинаковый приоритет, они выполняются в том порядке, в котором были добавлены в диспетчер.

Note

PHP вызываемое - это переменная PHP, которая может быть использована функцией call_user_func() и возвращает true при передаче функции is_callable(). Это может быть экземпляр \Closure, объект, реализующий метод __invoke() (то, чем на самом деле являются замыкания), строка, представляющая функцию или массив, представляющий метод объекта или класса.

До этих пор вы видели, как можно зарегистрировать PHP объекты в качестве слушателей. Вы можете также зарегистрировать PHP Замыкания в качестве слушателей событий:

1
2
3
4
5
use Symfony\Component\EventDispatcher\Event;

$dispatcher->addListener('acme.foo.action', function (Event $event) {
    // будет выполнено при запуске события acme.foo.action
});

Когда слушатель зарегистрирован в диспетчере, он ждёт, пока не будет уведомления о событии. В примере выше, когда запускается событие acme.foo.action, диспетчер вызывает метод AcmeListener::onFooAction() и передаёт объект Event в виде единственного аргумента:

1
2
3
4
5
6
7
8
9
10
11
use Symfony\Component\EventDispatcher\Event;

class AcmeListener
{
    // ...

    public function onFooAction(Event $event)
    {
        // ... сделать что-то
    }
}

Аргумент $event - это объект события, который был передан при запуске события. Во многих случаях, передаётся специальный подкласс события с дополнительной информацией. Вы можете посмотреть документацию или реализацию каждого событий, чтобы определить, какой экземпляр передаётся.

Регистрации определений сервисов и тегирования их тегами kernel.event_listener и kernel.event_subscriber недостаточно для того, чтобы включить слушателей и подписчиков событий. Вы также должны зарегистрировать пропуск компилировщика под названием RegisterListenersPass() в конструкторе контейнера:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass;

$containerBuilder = new ContainerBuilder(new ParameterBag());
// регистрирует пропуск компилировщика, который обрабатывает теги сервиса
// 'kernel.event_listener' и 'kernel.event_subscriber'
$containerBuilder->addCompilerPass(new RegisterListenersPass());

$containerBuilder->register('event_dispatcher', EventDispatcher::class);

// регистрирует слушателя события
$containerBuilder->register('listener_service_id', \AcmeListener::class)
    ->addTag('kernel.event_listener', array(
        'event' => 'acme.foo.action',
        'method' => 'onFooAction',
    ));

// регистрирует подписчика событий
$containerBuilder->register('subscriber_service_id', \AcmeSubscriber::class)
    ->addTag('kernel.event_subscriber');

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\EventDispatcher\DependencyInjection\AddEventAliasesPass;
use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass;
use Symfony\Component\EventDispatcher\EventDispatcher;

$containerBuilder = new ContainerBuilder(new ParameterBag());
$containerBuilder->addCompilerPass(new AddEventAliasesPass([
    \AcmeFooActionEvent::class => 'acme.foo.action',
]));
$containerBuilder->addCompilerPass(new RegisterListenersPass(), PassConfig::TYPE_BEFORE_REMOVING);

$containerBuilder->register('event_dispatcher', EventDispatcher::class);

// регистрирует слушателя событий
$containerBuilder->register('listener_service_id', \AcmeListener::class)
    ->addTag('kernel.event_listener', [
        // will be translated to 'acme.foo.action' by RegisterListenersPass.
        'event' => \AcmeFooActionEvent::class,
        'method' => 'onFooAction',
    ]);

Note

Заметьте, что AddEventAliasesPass должен быть обработан до RegisterListenersPass.

По умолчанию, пропуск слушателей предполагает, что id сервиса диспетчера событий - event_dispatcher, что слушатели событий тегированы тегом kernel.event_listener, и что подписчики событий тегированы тегом kernel.event_subscriber. Вы можете изменить эти значения по умолчанию, передав пользовательские значения конструктору RegisterListenersPass.

Создание и запуск события

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

Создание класса событий

Представьте, что вы хотите создать новое событие - order.placed - которое запускается каждый раз, когда пользователь заказывает товар в вашем приложении. При запуске этого события, вы передаёте пользовательский экземпляр события, который имеет доступ к размещённому заказу. Начните с создания этого пользоватсклього класса события и его документирования:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
namespace Acme\Store\Event;

use Symfony\Component\EventDispatcher\Event;
use Acme\Store\Order;

/**
 * Событие order.placed запускается каждый раз, когда создаётся заказ
 * в системе.
 */
class OrderPlacedEvent extends Event
{
    public const NAME = 'order.placed';

    protected $order;

    public function __construct(Order $order)
    {
        $this->order = $order;
    }

    public function getOrder(): Order
    {
        return $this->order;
    }
}

Каждый слушатель теперь имеет доступ к заказу через метод getOrder().

Note

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

Запустите событие

Метод dispatch() уведомляет всех слушателей о данном событии. Используется два аргумента: имя события для запуска, и экземпляр Event для передачи каждому слушателю этого события:

1
2
3
4
5
6
7
8
9
10
use Acme\Store\Order;
use Acme\Store\Event\OrderPlacedEvent;

// создаёт или извлекает порядок каким-либо образом
$order = new Order();
// ...

// создаёт OrderPlacedEvent и запускает его
$event = new OrderPlacedEvent($order);
$dispatcher->dispatch(OrderPlacedEvent::NAME, $event);

Заметьте, что специальный объект OrderPlacedEvent создаётся и передаётся методу dispatch(). Теперь, любой слушатель события order.placed получит OrderPlacedEvent.

Использование подписчиков событий

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

Другим способом слушать события является через подписчика событий. Подписчик событий - это PHP класс, который способен сообщить диспетчеру, на какие именно события ему нужно подпистаься. Он реализует интерфейс EventSubscriberInterface, который требует одного статичного метода, под названием getSubscribedEvents(). Возьмите следующий пример подписчика, который подписывается на события kernel.response и order.placed:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
namespace Acme\Store\Event;

use Acme\Store\Event\OrderPlacedEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;

class StoreSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents()
    {
        return [
            KernelEvents::RESPONSE => [
                ['onKernelResponsePre', 10],
                ['onKernelResponsePost', -10],
            ],
            OrderPlacedEvent::NAME => 'onStoreOrder',
        ];
    }


    public function onKernelResponsePre(ResponseEvent $event)
    {
        // ...
    }

    public function onKernelResponsePost(ResponseEvent $event)
    {
        // ...
    }

    public function onStoreOrder(OrderPlacedEvent $event)
    {
        // ...
    }
}

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

1
2
3
4
5
use Acme\Store\Event\StoreSubscriber;
// ...

$subscriber = new StoreSubscriber();
$dispatcher->addSubscriber($subscriber);

Диспетчер автоматически зарегистрирует подписчика для каждого события, возвращённого методом getSubscribedEvents(). Этот метод возвращает массив, индексированный по именам событий, значения которых являются либо именем метода для вызова, либо массивом, составленным из именим метода для вызова и приоритетом.

Вышеописанный пример демонстрирует, как зарегистрировать несколько методов слушателя для одного и того же события в подписчике, а также, как передать приоритет каждого метода слушателя. Чем выше приоритет, тем раньше вызывается метод. В вышеописанном примере, когда запускается событие kernel.response, вызываются методы onKernelResponsePre() и onKernelResponsePost() в таком порядке.

Остановка потока / распространения событий

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

1
2
3
4
5
6
7
8
use Acme\Store\Event\OrderPlacedEvent;

public function onStoreOrder(OrderPlacedEvent $event)
{
    // ...

    $event->stopPropagation();
}

Теперь, любые слушатели order.placed, которые ещё не были вызваны, не будут вызваны.

Возможно определить, было ли событие остановлено с использованием метода isPropagationStopped(), который возвращает булево значение:

1
2
3
4
5
// ...
$dispatcher->dispatch($event, 'foo.event');
if ($event->isPropagationStopped()) {
    // ...
}

События и слушатели, знающие об EventDispatcher

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

Интроспекция имени события

Экземпляр EventDispatcher, так же как и имя события, которое запускается, передаются в качестве аргументов слушателя:

1
2
3
4
5
6
7
8
9
10
use Symfony\Contracts\EventDispatcher\Event;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

class Foo
{
    public function myEventListener(Event $event, $eventName, EventDispatcherInterface $dispatcher)
    {
        // ... сделать что-то с именем события
    }
}

Другие диспетчеры

Кроме распространённого EventDispatcher, компонент поставляется с некоторыми другими диспетчерами: