События и слушатели событий

Дата обновления перевода: 2023-01-18

События и слушатели событий

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

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

Все примеры, показанные в этой статье, используют одно и то же событие KernelEvents::EXCEPTION в целях последовательности. В вашем приложении вы можете использовать любое событие и даже смешивать некоторые из них в одном абоненте.

Создание слушателя событий

Самым распространённым способом принять событие является его регистрация в слушателе событий:

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
// src/EventListener/ExceptionListener.php
namespace App\EventListener;

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;

class ExceptionListener
{
    public function onKernelException(ExceptionEvent $event)
    {
        // Вы получаете объект исключения из полученного события
        $exception = $event->getException();
        $message = sprintf(
            'My Error says: %s with code: %s',
            $exception->getMessage(),
            $exception->getCode()
        );

        // Настройте ваш объект ответа, чтобы он отображал детали исключений
        $response = new Response();
        $response->setContent($message);

        // HttpExceptionInterface - это специальный тип исключения, который
        // содержит статус кода и детали заголовка
        if ($exception instanceof HttpExceptionInterface) {
            $response->setStatusCode($exception->getStatusCode());
            $response->headers->replace($exception->getHeaders());
        } else {
            $response->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR);
        }

        // Отправляет изменённый объект ответа событию
        $event->setResponse($response);
    }
}

Tip

Каждое событие получает немного разные типы объекта $event. Для события kernel.exception - это GetResponseForExceptionEvent. Смотрите справочник событий Symfony, чтобы увидеть, какой тип объекта предоставляет каждое из них.

Теперь, когда класс создан, вам просто нужно зарегистрировать его в качестве сервиса и уведомить Symfony, что он "слушатель" события kernel.exception, путём использования специального "тега":

  • YAML
  • XML
  • PHP
1
2
3
4
5
# config/services.yaml
services:
    App\EventListener\ExceptionListener:
        tags:
            - { name: kernel.event_listener, event: kernel.exception }

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

  1. Если тег kernel.event_listener определяет атрибут method, то это имя метода, который нужно выполнить;
  2. Если не определён атрибут method, попробуйте выполнить метод, имя которого состоит из on + "имя события camel-case" (например, методonKernelException() для события kernel.exception);
  3. Если этот метод тоже не определён, попробуйте выполнить волшебный метод __invoke() (который делает слушатели событий вызываемыми);
  4. Если метод _invoke() тоже не определён, вызовите исключение.

Note

Существует необязательный атрибут для тега kernel.event_listener под названием priority, который по умолчанию равняется 0 и контролирует порядок выполнения слушателей (чем выше приоритет, тем раньше выполняется слушатель). Это полезно,когда вам нужно гарантировать, что один слушатель будет выполнен перед другим. Приоритеы внутренних слушателей Symfony обычно колеблются в диапазоне от -255до 255, но ваши собственные слушатели могут использовать любое положительное или отрицательное целое число.

Определение слушателей событий с PHP-атрибутами

Альтернативным способом определения слушателя событий является использование PHP-атрибута AsEventListener. Это позволяет сконфигурировать слушателя внутри его класса, без необходимости добавления какой-либо конфигурации во внешних файлах:

1
2
3
4
5
6
7
8
9
10
11
12
namespace App\EventListener;

use Symfony\Component\EventDispatcher\Attribute\AsEventListener;

#[AsEventListener]
final class MyListener
{
    public function __invoke(CustomEvent $event): void
    {
        // ...
    }
}

Вы можете добавлять множество атрибутов #[AsEventListener()], чтобы сконфигурировать разные методы:

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

use Symfony\Component\EventDispatcher\Attribute\AsEventListener;

#[AsEventListener(event: CustomEvent::class, method: 'onCustomEvent')]
#[AsEventListener(event: 'foo', priority: 42)]
#[AsEventListener(event: 'bar', method: 'onBarEvent')]
final class MyMultiListener
{
    public function onCustomEvent(CustomEvent $event): void
    {
        // ...
    }

    public function onFoo(): void
    {
        // ...
    }

    public function onBarEvent(): void
    {
        // ...
    }
}

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

Еще одним способом принимать события является подписчик событий - класс, который определяет один или более методов, которые слушают одно или более событий. Главное отличие от слушателя событий заключется в том, что подписчики всегда знают, какие события они слушают.

Если разные методы подписчиков событий слушают одно и то же событие, их порядок определяется параметром priority. Это значение является положительным или отрицательным целым числом, которое по умолчанию равно 0. Чем больше число, тем раньше вызывается метод. Приоритетность агрегируется для всех слушателей и подписчиков, поэтому ваши методы могут быть вызвано до или после методов, определенных в других слушателях и событиях. Чтобы узнать больше о подписчиках событий, прочтите Компонент EventDispatcher.

Следующий пример иллюстрирует подписчика событий, который определяет несколько методов, которые принимают одно и то же событие kernel.exception:

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
// src/EventSubscriber/ExceptionSubscriber.php
namespace App\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\KernelEvents;

class ExceptionSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents()
    {
        // вернуть подписанные события, их методы и приоритеты
        return array(
           KernelEvents::EXCEPTION => array(
               array('processException', 10),
               array('logException', 0),
               array('notifyException', -10),
           )
        );
    }

    public function processException(GetResponseForExceptionEvent $event)
    {
        // ...
    }

    public function logException(GetResponseForExceptionEvent $event)
    {
        // ...
    }

    public function notifyException(GetResponseForExceptionEvent $event)
    {
        // ...
    }
}

Вот и все! Ваш файл services.yaml должен уже быть настроен так, чтобы загружать сервисы из каталога EventSubscriber. Об остальном позаботится Symfony.

Tip

Если ваши методы не вызываются, когда есть исключение, перепроверьте, что вы загружаете сервисы из каталога EventSubscriber и активировали автоконфигурацию . Вы также можете вручную добавить тег kernel.event_subscriber.

События запросов, проверка типов

Одна страница может делать несколько запросов (один главный и множество под-запросов - обычно с помощью ). Для главных событий Symfony, вам может понадобиться проверить, относится ли событие к "главному" запросу или "под-запросу":

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/EventListener/RequestListener.php
namespace App\EventListener;

use Symfony\Component\HttpKernel\Event\RequestEvent;

class RequestListener
{
    public function onKernelRequest(RequestEvent $event)
    {
        if (!$event->isMainRequest()) {
            // ничего не делайте, если это не основной запрос
            return;
        }

        // ...
    }
}

Некоторые вещи, как то проверка информации в настоящем запросе, могут не понадобиться в приёмниках под-запросов.

Слушатели или подписчики

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

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

Псевдонимы событий

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/EventSubscriber/RequestSubscriber.php
namespace App\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;

class RequestSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            RequestEvent::class => 'onKernelRequest',
        ];
    }

    public function onKernelRequest(RequestEvent $event)
    {
        // ...
    }
}

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

Отображение псведонимов можно расширить для пользовательских событий, зарегистрировав передачу компилятора AddEventAliasesPass:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/Kernel.php
namespace App;

use App\Event\MyCustomEvent;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\EventDispatcher\DependencyInjection\AddEventAliasesPass;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;

class Kernel extends BaseKernel
{
    protected function build(ContainerBuilder $container)
    {
        $container->addCompilerPass(new AddEventAliasesPass([
            MyCustomEvent::class => 'my_custom_event',
        ]));
    }
}

Передача компилятора всегда будет раширять существующий список псевдонимов. Из-за этого, безопаснее регистрировать несколько экземпляров передач с разными конфигурациями.

Отладка слушателей событий

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

1
$ php bin/console debug:event-dispatcher

Вы можете получить зарегистрированных слушателя конкретного события, указав его имя:

1
$ php bin/console debug:event-dispatcher kernel.exception

или получить все, частично соответствующих имени события:

1
2
$ php bin/console debug:event-dispatcher kernel // matches "kernel.exception", "kernel.response" etc.
$ php bin/console debug:event-dispatcher Security // matches "Symfony\Component\Security\Http\Event\CheckPassportEvent"

Система security использует по диспетчеру событий для каждого файерволла. Используйте опцию --dispatcher, чтобы получить зарегистрированных слушателей для конкретного диспетчера событий:

1
$ php bin/console debug:event-dispatcher --dispatcher=security.event_dispatcher.main