События и слушатели событий
Во время выполнения приложения 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); > >
Каждое событие получает немного разные типы объекта $event . Для события kernel.exception — это GetResponseForExceptionEvent. Смотрите справочник событий Symfony, чтобы увидеть, какой тип объекта предоставляет каждое из них.
Теперь, когда класс создан, вам просто нужно зарегистрировать его в качестве сервиса и уведомить Symfony, что он «слушатель» события kernel.exception , путём использования специального «тега»:
# config/services.yaml services: App\EventListener\ExceptionListener: tags: - name: kernel.event_listener, event: kernel.exception >
1 2 3 4 5 6 7 8 9 10 11 12 13
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 https://symfony.com/schema/dic/services/services-1.0.xsd"> services> service id="App\EventListener\ExceptionListener"> tag name="kernel.event_listener" event="kernel.exception"/> service> services> container>
1 2 3 4 5 6 7 8 9 10 11 12
// config/services.php namespace Symfony\Component\DependencyInjection\Loader\Configurator; use App\EventListener\ExceptionListener; return function(ContainerConfigurator $configurator) < $services = $configurator->services(); $services->set(ExceptionListener::class) ->tag('kernel.event_listener', ['event' => 'kernel.exception']) ; >;
Symfony следует этой логике, чтобы решить, какой метод выполнить внутри класса слушателя событий:
- Если тег kernel.event_listener определяет атрибут method , то это имя метода, который нужно выполнить;
- Если не определён атрибут method , попробуйте выполнить метод, имя которого состоит из on + «имя события camel-case» (например, метод onKernelException() для события kernel.exception );
- Если этот метод тоже не определён, попробуйте выполнить волшебный метод __invoke() (который делает слушатели событий вызываемыми);
- Если метод _invoke() тоже не определён, вызовите исключение.
Существует необязательный атрибут для тега 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.
Если ваши методы не вызываются, когда есть исключение, перепроверьте, что вы загружаете сервисы из каталога 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', ])); > >
Передача компилятора всегда будет раширять существующий список псевдонимов. Из-за этого, безопаснее регистрировать несколько экземпляров передач с разными конфигурациями.
Отладка слушателей событий
Вы можете узнать, какие слушатели зарегистрированы в диспетчере событий, используя консоль. Чтобы показать все события и их слушателей, выполните:
$ php bin/console debug:event-dispatcher
Вы можете получить зарегистрированных слушателя конкретного события, указав его имя:
$ php bin/console debug:event-dispatcher kernel.exception
или получить все, частично соответствующих имени события:
$ 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 , чтобы получить зарегистрированных слушателей для конкретного диспетчера событий:
$ php bin/console debug:event-dispatcher --dispatcher=security.event_dispatcher.main