Компонент EventDispatcher

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

Вступление

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

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

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

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

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

Установка

Вы можете установить компонент 2 разными способами:

Then, require the vendor/autoload.php file to enable the autoloading mechanism provided by Composer. Otherwise, your application won't be able to find the classes of this Symfony component.

Применение

События

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

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

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

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

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

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

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

Зачастую, данные о конкретном событии должны быть переданы вместе с объектом 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');

По умолчанию, пропуск слушателей предполагает, что 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
{
    const NAME = 'order.placed';

    protected $order;

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

    public function getOrder()
    {
        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
namespace Acme\Store\Event;

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

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

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

    public function onKernelResponsePost(FilterResponseEvent $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('foo.event', $event);
if ($event->isPropagationStopped()) {
    // ...
}

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

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

Сокращения диспетчера

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

1
$dispatcher->dispatch('order.placed');

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

1
2
3
if (!$dispatcher->dispatch('foo.event')->isPropagationStopped()) {
    // ...
}

Или:

1
2
$event = new OrderPlacedEvent($order);
$order = $dispatcher->dispatch('bar.event', $event)->getOrder();

и так далее.

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

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

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

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

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

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

Эта документация является переводом официальной документации Symfony и предоставляется по свободной лицензии CC BY-SA 3.0.