В этом путеводителе мы будем рассматривать автономные библиотеки (также известные как «Компоненты»), предлагаемые 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
.
На каждом ключевом этапе выполнения он обрабатывает следующие события:
kernel.request
: получает Requestkernel.controller
: выполняет объект обратного вызова (callable) (также известный как «Контроллер»)kernel.view
: конвертирует возвращенное значение Контроллера вResponse
(при необходимости)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
. Но вы можете создать свое собственное событие и обработать его, чтобы сделать свой код «Открытым для расширения, но закрытым для изменения» (принцип открытости/закрытости).