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

Во время выполнения приложения 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\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;

class ExceptionListener
{
    public function onKernelException(GetResponseForExceptionEvent $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
    1
    2
    3
    4
    5
    # config/services.yaml
    services:
        App\EventListener\ExceptionListener:
            tags:
                - { name: kernel.event_listener, event: kernel.exception }
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    <!-- config/services.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service id="AppBundle\EventListener\ExceptionListener">
                <tag name="kernel.event_listener" event="kernel.exception" />
            </service>
        </services>
    </container>
    
  • PHP
    1
    2
    3
    4
    5
    6
    7
    // config/services.php
    use App\EventListener\ExceptionListener;
    
    $container
        ->autowire(ExceptionListener::class)
        ->addTag('kernel.event_listener', array('event' => 'kernel.exception'))
    ;
    

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

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

New in version 4.1: Поддержка метода __invoke() для создания вызываемого слушателя событий была представлена в Symfony 4.1.

Note

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

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

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

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

Следующий пример иллюстрирует подписчика событий, который определяет несколько методов, которые принимают одно и то же событие 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\GetResponseForExceptionEvent;
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.

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

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

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

use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\HttpKernel;
use Symfony\Component\HttpKernel\HttpKernelInterface;

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

        // ...
    }
}

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

Приёмники или абоненты

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

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

Отладка приёмников событий

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

1
$ php bin/console debug:event-dispatcher

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

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

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