Путеводитель по Symfony: Диспетчер событий

В этом путеводителе мы будем рассматривать автономные библиотеки (также известные как «Компоненты»), предлагаемые Symfony для создания приложений.

Диспетчер событий

Symfony предлагает компонент EventDispatcher, который позволяет выполнять основные зарегистрированные функции в нашем приложении.

Все основывается на следующем интерфейсе:

<?php
namespace Symfony\Component\EventDispatcher;
interface EventDispatcherInterface
{
    /**
     * @param string   $eventName
     * @param callable $listener
     * @param int      $priority  High priority listeners will be executed first
     */
    public function addListener($eventName, $listener, $priority = 0);
    /**
     * @param string $eventName
     * @param Event  $event
     */
    public function dispatch($eventName, Event $event = null);
}

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

Готовая реализация может использоваться следующим образом:

<?php
use Symfony\Component\EventDispatcher\EventDispatcher;
$eventDispatcher = new EventDispatcher();
$eventDispatcher->addListener('something_happened', function () {
    echo "Log it\n";
}, 1);
$eventDispatcher->addListener('something_happened', function () {
    echo "Save it\n";
}, 2);
$eventDispatcher->dispatch('something_happened');

На выходе:

Save it
Log it

Поскольку второй слушатель был более приоритетным, его выполнили в первую очередь.

Примечание: Слушатели должны представлять собой объекты обратного вызова (callable/callback), например:

  • анономную функцию: $listener = function (Event $event) {};.
  • массив с экземпляром класса и именем метода: $listener = array($service, 'method');.
  • полностью определенное имя класса с именем статического метода: $listener = 'Vendor\Project\Service::staticMetod'.

Чтобы предоставить слушателям контекст (параметры и т.д.), мы можем создать подкласс Event:

<?php
use Symfony\Component\EventDispatcher\Event;
use Symfony\Component\EventDispatcher\EventDispatcher;
class SomethingHappenedEvent extends Event
{
    private $who;
    private $what;
    private $when;
    public function __construct($who, $what)
    {
        $this->who = $who;
        $this->what = $what;
        $this->when = new \DateTime();
    }
    public function who()
    {
        return $this->who;
    }
    public function what()
    {
        return $this->what;
    }
    public function when()
    {
        return $this->when;
    }
}
$eventDispatcher = new EventDispatcher();
$eventDispatcher->addListener('something_happened', function (SomethingHappenedEvent $event) {
    echo "{$event->who()} was {$event->what()} at {$event->when()->format('Y/m/d H:i:s')}\n";
});
$eventDispatcher->dispatch('something_happened', new SomethingHappenedEvent('Arthur', 'hitchhiking'));

Пример HttpKernel

Мы рассмотрели компонент HttpKernel в предыдущей статье. Он предлагает абстрактный класс Kernel, который в большой степени опирается на EventDispatcher.

На каждом ключевом этапе выполнения он обрабатывает следующие события:

  1. kernel.request: получает Request
  2. kernel.controller: выполняет объект обратного вызова (callable) (также известный как «Контроллер»)
  3. kernel.view: конвертирует возвращенное значение Контроллера в Response (при необходимости)
  4. kernel.response: возвращает Response

И в случае ошибки:

  • kernel.exception: устраняет ошибки Перед тем как возвращать Response, HttpKernel обрабатывает последнее событие:
  • kernel.finish_request: наведение внешнего порядка, отправка электронных сообщений и т.д. После отображения Response мы можем обработать следующее событие:
  • kernel.terminate: так же, как kernel.finish_request, за исключением того, что это не замедлит отображение запроса, если включен FastCGI

Событие Kernel Request

Слушатели, зарегистрированные на kernel.request, могут изменить объект Request (Запрос). Существует готовый зарегистрированный RouterListener, который задает следующие параметры в Request->attributes:

  • _route: имя маршрута, соответствующего Запросу
  • _controller: объект обратного вызова (callable), который обработает Запрос и вернет Ответ.
  • _route_parameters: параметры запроса, извлеченные из Запроса.

В качестве примера пользовательского Слушателя может выступать слушатель, который расшифровывает контент JSON и помещает его в Request->request:

<?php
namespace AppBundle\EventListener;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
/**
 * PHP does not populate $_POST with the data submitted via a JSON Request,
 * causing an empty $request->request.
 *
 * This listener fixes this.
 */
class JsonRequestContentListener
{
    /**
     * @param GetResponseEvent $event
     */
    public function onKernelRequest(GetResponseEvent $event)
    {
        $request = $event->getRequest();
        $hasBeenSubmited = in_array($request->getMethod(), array('PATCH', 'POST', 'PUT'), true);
        $isJson = (1 === preg_match('#application/json#', $request->headers->get('Content-Type')));
        if (!$hasBeenSubmited || !$isJson) {
            return;
        }
        $data = json_decode($request->getContent(), true);
        if (JSON_ERROR_NONE !== json_last_error()) {
            $event->setResponse(new Response('{"error":"Invalid or malformed JSON"}', 400, array('Content-Type' => 'application/json')));
        }
        $request->request->add($data ?: array());
    }
}

Еще один пример – начать транзакцию базы данных:

<?php
namespace AppBundle\EventListener;
use PommProject\Foundation\QueryManager\QueryManagerInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
class StartTransactionListener
{
    /**
     * @var QueryManagerInterface
     */
    private $queryManager;
    /**
     * @param QueryManagerInterface $queryManager
     */
    public function __construct(QueryManagerInterface $queryManager)
    {
        $this->queryManager = $queryManager;
    }
    /**
     * @param GetResponseEvent $event
     */
    public function onKernelRequest(GetResponseEvent $event)
    {
        $this->queryManager->query('START TRANSACTION');
    }
}

Событие Kernel Controller

Слушатели, зарегистрированные на kernel.controller, могут изменять объект Request (Запрос). Это очень полезная возможность, когда мы хотим изменить Контроллер.

Например, SensioFrameworkExtraBundle имеет ControllerListener, который анализирует аннотации контроллера на данном этапе.

Событие Kernel View

Слушатели, зарегистрированные на kernel.view , могут изменять объект Запрос (Request). Например, SensioFrameworkExtraBundle имеет TemplateListener, который использует аннотацию @Template: контроллерам нужно лишь вернуть массив, и слушатель создаст ответ при помощи Twig (массив будет представлен в качестве параметров Twig).

Событие Kernel Response

Слушатели, зарегистрированные на kernel.response, могут изменять объект Запрос. Имеется готовый зарегистрированный ResponseListener, который задает некоторые заголовки Ответа в соответствии с заголовком Запроса.

Событие Kernel Terminate

Слушатели, зарегистрированные на kernel.terminate, могут выполнять действия после того, как был дан Ответ (если наш веб-сервер использует FastCGI).

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

<?php
namespace AppBundle\EventListener\Pomm;
use PommProject\Foundation\QueryManager\QueryManagerInterface;
use Symfony\Component\HttpKernel\Event\PostResponseEvent;
class RollbackListener
{
    /**
     * @var QueryManagerInterface
     */
    private $queryManager;
    /**
     * @param QueryManagerInterface $queryManager
     */
    public function __construct(QueryManagerInterface $queryManager)
    {
        $this->queryManager = $queryManager;
    }
    /**
     * @param PostResponseEvent $event
     */
    public function onKernelTerminate(PostResponseEvent $event)
    {
        $this->queryManager->query('ROLLBACK');
    }
}

Примечание: Далее мы увидим, как зарегистрировать такого слушателя только для тестовой среды.

Событие Kernel Exception

Слушатели, зарегистрированные на kernel.exception, могут перехватить исключение и сгенерировать соответствующий объект Ответ.

В качесте пользовательского Слушателя может выступать слушатель, который документирует информацию об отладке и генерирует Ответ 500:

<?php
namespace AppBundle\EventListener;
use Psr\Log\LoggerInterface;
use Ramsey\Uuid\Uuid;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
class ExceptionListener
{
    /**
     * @var LoggerInterface
     */
    private $logger;
    /**
     * @param LoggerInterface $logger
     */
    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }
    /**
     * @param GetResponseForExceptionEvent $event
     */
    public function onKernelException(GetResponseForExceptionEvent $event)
    {
        $exception = $event->getException();
        $token = Uuid::uuid4()->toString();
        $this->logger->critical(
            'Caught PHP Exception {class}: "{message}" at {file} line {line}',
            array(
                'class' => get_class($exception),
                'message' => $exception->getMessage(),
                'file' => $exception->getFile(),
                'line' => $exception->getLine()
                'exception' => $exception,
                'token' => $token
            )
        );
        $event->setResponse(new Response(
            json_encode(array(
                'error' => 'An error occured, if it keeps happening please contact an administrator and provide the following token: '.$token,
            )),
            500,
            array('Content-Type' => 'application/json'))
        );
    }
}

Примечание: Ramsey UUID используется в качестве уникальной лексемы, на которую можно ссылаться.

Заключение

EventDispatcher представляет собой еще один пример простого, но при этом полезного компонента Symfony. HttpKernel использует его для конфигурации стандартного «приложения Symfony», а также для того, чтобы позволить нам изменять его поведение.

В этой статье мы рассмотрели основы работы этого компонента, когда он используется HttpKernel. Но вы можете создать свое собственное событие и обработать его, чтобы сделать свой код «Открытым для расширения, но закрытым для изменения» (принцип открытости/закрытости).