При выполнении приложения использующего php-фреймворк Symfony, запускается множество уведомлений о событиях. Зная как работать с событиями, можно предусмотреть автоматическое выполнение нужного вам кода в нужное время (при срабатывании нужного события). Кроме того события используют многие сервисы, например Doctrine. Бывает, что не обойтись без события которое возникает перед сохранением записи в базу данных. Или, возможно сразу после сохранения нужно отправить уведомление пользователю на email... Также можно создавать собственные события.
Примеры практического применения событий (свои и из документации) в данной статье. При написании использовалась версия 3.4

Создание событий.


1. Простые события
(анонимная функция или вызов определенного метода нужного класса):

<?php

namespace AppBundle\Controller;

use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class TestController
{

    public function showArgs(...$args)
    {
        dump($args);
    }

    /**
     * @Route("/test", name="test")
     */
    public function indexAction(EventDispatcherInterface $dispatcher)
    {
        $dispatcher->addListener('dump_event_args', [$this, 'showArgs']);
        $dispatcher->dispatch('dump_event_args');
        
        return new Response('<p>Test events</p>');
    }
}
Результат выполнения:
TestController.php on line 14:
array:3 [▼
0 => Event {#585 ▶}
1 => "dump_event_args"
2 => TraceableEventDispatcher {#121 ▶}
]
Test events


или так:
public function feedbackAction(EventDispatcherInterface $eventDispatcher)
{
    $eventDispatcher->addListener('test', function (){
        $this->data = [1,2,3];
    });

    $eventDispatcher->dispatch('test'); //вызывает выполнение анонимной функции слушателя 'test'
//        
В данных примерах метод dispatch, который означает наступление указанного события, вызывается сразу же. В реальном проекте он будет совсем в другом месте кода или другом классе.
При вызове метода dispatch() вторым параметром можно передать объект конкретного события:
$eventDispatcher->dispatch('feedbackEvent', $event);
которое будет доступно в слушателе события. Но для простых событий это обычно лишнее.


2. Сложные события
(события в виде отдельных классов).
Подобным образом созданы события многих сервисов.

Пример класса в котором создается 2 события:
class CustomMailer
{
    // ...

    public function send($subject, $message)
    {
        // dispatch an event before the method
        $event = new BeforeSendMailEvent($subject, $message);
        $this->dispatcher->dispatch('mailer.pre_send', $event);

        // get $foo and $bar from the event, they may have been modified
        $subject = $event->getSubject();
        $message = $event->getMessage();

        // the real method implementation is here
        $returnValue = ...;

        // do something after the method
        $event = new AfterSendMailEvent($returnValue);
        $this->dispatcher->dispatch('mailer.post_send', $event);

        return $event->getReturnValue();
    }
}

В данном случае создаются отдельные классы событий (BeforeSendMailEvent и AfterSendMailEvent), они позволят вносить изменения в данные которые обрабатывались до наступления этих событий. Вы можете заметить, что объекту события передаются аргументы:
$event = new BeforeSendMailEvent($subject, $message);
а значит событие предусматривает возможность работы с этими данными.

Теперь нужно создать указанные классы для передачи данных слушателям событий.
src/AppBundle/Event/BeforeSendMailEvent.php:
namespace AppBundle\Event;

use Symfony\Component\EventDispatcher\Event;

class BeforeSendMailEvent extends Event
{
    private $subject;
    private $message;

    public function __construct($subject, $message)
    {
        $this->subject = $subject;
        $this->message = $message;
    }

    public function getSubject()
    {
        return $this->subject;
    }

    public function setSubject($subject)
    {
        $this->subject = $subject;
    }

    public function getMessage()
    {
        return $this->message;
    }

    public function setMessage($message)
    {
        $this->message = $message;
    }
}

src/AppBundle/Event/AfterSendMailEvent.php:
namespace AppBundle\Event;

use Symfony\Component\EventDispatcher\Event;

class AfterSendMailEvent extends Event
{
    private $returnValue;

    public function __construct($returnValue)
    {
        $this->returnValue = $returnValue;
    }

    public function getReturnValue()
    {
        return $this->returnValue;
    }

    public function setReturnValue($returnValue)
    {
        $this->returnValue = $returnValue;
    }
}
Оба события позволяют получить некоторую информацию (например метод getMessage) и даже изменить эту информацию (например setMessage).


Получение объекта диспетчера для вызова события
При создании событий в своих сервисах вы размещаете строки с вызовом определенного события в нужном месте кода (как в примерах выше). За вызов событий отвечает диспетчер, который аккумулирует все события. Чтобы добавить в него вызов нужных событий, с начала необходимо получить данный объект. Способ получения различается в зависимости от версии Symfony.

Для symfony 3.2 и более ранних версий:
    public function indexAction()
    {
        $eventDispatcher = $this->get('event_dispatcher');
        //            
    }        
т.е. получаем его из сервис-контейнера.

Для symfony 3.3+
    public function indexAction(EventDispatcherInterface $eventDispatcher)
    {
      //            
    }  
получаем используя инъекцию зависимости в метод.


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

Теперь вы можете создать подписчика событий, чтобы подключиться к этому событию. Исходя из вышеописанных примеров, вы можете прослушать mailer.post_send событие и изменить возвращаемое значение метода.
Файл src/AppBundle/EventSubscriber/MailPostSendSubscriber.php:
namespace AppBundle\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use AppBundle\Event\AfterSendMailEvent;

class MailPostSendSubscriber implements EventSubscriberInterface
{
    public function onMailerPostSend(AfterSendMailEvent $event)
    {
        $returnValue = $event->getReturnValue();
        // modify the original ``$returnValue`` value

        $event->setReturnValue($returnValue);
    }

    public static function getSubscribedEvents()
    {
        return array(
            'mailer.post_send' => 'onMailerPostSend'
        );
    }
}

Это был пример слушателя события, но в Symfony разделяют классы слушателей событий и создание классов подписки на события.

Слушатели событий создаются в папке src/AppBundle/EventListener.
Например src/AppBundle/EventListener/ExceptionListener.php:
namespace AppBundle\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);
    }
}

Регистрация слушателя.
• yml
# app/config/services.yml
services:
    AppBundle\EventListener\ExceptionListener:
        tags:
            - { name: kernel.event_listener, event: kernel.exception }
Так же можно регистрировать используя php и xml.

Существует необязательный атрибут тега, method который определяет, какой метод будет выполняться при запуске события. По умолчанию имя метода равно on+ «имя события с верблюжьим именем». Если событием является kernel.exception метод, выполняемый по умолчанию onKernelException().

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


Создание класс для подписки на события.
Классы создаются в папке src/AppBundle/EventSubscriber
Например src/AppBundle/EventSubscriber/ExceptionSubscriber.php:
namespace AppBundle\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),
           ),
            //тут другие события (например)
            'feedbackEvent' => 'feedback'
            );
    }

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

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

    public function notifyException(GetResponseForExceptionEvent $event)
    {
        // ...
    }
}
services.yml уже автоматически настроен для загрузки сервисов из EventSubscriber. Будут вызываться методы один за другим.

Что выбрать?!
Слушатели и подписчики могут использоваться в одном и том же приложении. Решение использовать любой из них обычно зависит от личного вкуса. Однако для каждого из них есть некоторые незначительные преимущества:
  • Подписчиков легче использовать повторно, потому что знание событий хранится в классе, а не в определении службы. Именно по этой причине Symfony использует внутренних абонентов;
  • Слушатели более гибкие, поскольку пакеты могут включать или отключать каждый из них условно в зависимости от некоторого значения конфигурации.