Події та слухачі подій

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

Події та слухачі подій

Під час виконання додатку 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
  • 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\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.

Події запитів, перевірка типів

Одна сторінка може робити декілька запитів (один головний та безліч під-запитів - зазвичай за допомогою ). Для головних подій 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\RequestEvent;

class RequestListener
{
    public function onKernelRequest(RequestEvent $event)
    {
        // Метод isMainRequest() було представлено в Symfony 5.3.
        // У попередніх версіях він називався isMasterRequest()
        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"

Система безпеки використовує по диспетчеру подій для кожного брандмауеру. Використовуйте опцію --dispatcher, щоб отримати зареєстрованих слухачів для конкретного диспетчеру подій:

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