Компонент EventDispatcher

Дата оновлення перекладу 2022-04-30

Компонент EventDispatcher

Компонент EventDispatcher надає інструменти, що дозволяють компонентам вашого додатку спілкуватися одне з одним, оголошуючи події та слухаючи їх.

Вступ

Об'єктно-орієнтований код пройшов довгий шлях, щоб гарантувати розширюваність коду. Створюючи класи, які мають чітко визначені задачі, ви робите ваш код гнучкішим, і розробник може розширювати їх за допомогою підкласів для налаштування цієї поведінки. Але якщо вони хочуть поділитися змінами з іншими розробниками, які також створили свої власні підкласи, спадкування коду більше не буде рішенням.

Уявіть реальний приклад, де ви хочете надати систему плагінів для вашого проекту. Плагін мусить мати можливість додавати методи або робити щось до чи після того, як виконується метод, не втручаючись в роботу інших плагінів. Це складна задачка для одного тільки спадкування, і навіть якщо б множинне спадкування було можливим в PHP, воно має свої недоліки.

Компонент Symfony EventDispatcher реалізує шаблони проектування Посередник та Спостерігач для того, щоб зробити це все можливим та надати вашим проектам можливіть бути дійсно розширюваними.

Візьміть простий приклад з компоненту 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, щоб слухачі мали необхідну їм інформацію. В такому випадку, можна передати спеціальний підклас, який має додаткові методи для добування та перевизначення інформації, при оголошенні події. Наприклад, подія 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, компонент надається з деякими іншими диспетчерами: