Метод handleRaw()
класса HttpKernel
— это замечательный пример кода, анализируя который, становится ясно, что алгоритм обработки запроса сам по себе не является детерминированным (т.е. допускает отклонения и изменения в процессе). Это означает, что у вас есть несколько различных способов для внедрения в этот процесс, путём которого вы можете полностью заменить или частично модифицировать ответ на промежуточных шагах его формирования.
Ранний ответ
Как вы думаете, когда вы можете взять контроль над обработкой запроса? Ответ — сразу же после начала его обработки. Как правило, HttpKernel
пытается сгенерировать ответ, выполняя контроллер. Однако любой слушатель (listener), который ожидает событие KernelEvents::REQUEST (kernel.request)
, может сгенерировать полностью уникальный ответ:
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
private function handleRaw(Request $request, $type = self::MASTER_REQUEST)
{
$event = new GetResponseEvent($this, $request, $type);
$this->dispatcher->dispatch(KernelEvents::REQUEST, $event);
if ($event->hasResponse()) {
return $this->filterResponse(
$event->getResponse(),
$request,
$type
);
}
// ...
}
Как вы можете видеть, объект события тут — это экземпляр GetResponseEvent
и он позволяет слушателям заменить объект Response
на свой, используя метод события setResponse()
, например:
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
class MaintenanceModeListener
{
public function onKernelRequest(GetResponseEvent $event)
{
$response = new Response(
'This site is temporarily unavailable',
503
);
$event->setResponse($response);
}
}
Регистрация слушателей событий (event listeners)
Диспетчер событий, используемый классом
HttpKernel
, также доступен как сервисevent_dispatcher
. Когда вы захотите автоматически зарегистрировать какой-нибудь класс как слушатель, вам нужно будет создать для него сервис и добавить ему тэгkernel.event_listener
илиkernel.event_subscriber
(в случае, если вы хотите реализовать интерфейсEventSubscriberInterface
).<service id="..." class="..."> <tag name="kernel.event_listener" event="kernel.request" method="onKernelRequest" /> </service>Или:
<service id="..." class="..."> <tag name="kernel.event_subscriber" /> </service>Вы также можете указать приоритет вашего слушателя, что может дать ему преимущество перед другими слушателями:
<service id="..." class="..."> <tag name="kernel.event_listener" event="kernel.request" method="onKernelRequest" priority="100" /> </service>Чем выше приоритет, тем раньше слушатель события будет уведомлен.
Слушатели kernel.request, о которых вам нужно знать
Фреймворк содержит много слушателей события kernel.request
. В основном это слушатели, выполняющие некоторые приготовления, прежде чем дать ядру возможность вызвать какой-либо контроллер. Например, один слушатель даёт возможность приложению использовать локали (например, локаль по умолчанию или же _locale из URI), другой обрабатывает запросы фрагментов страниц.
Тем не менее, имеется два основных игрока на стадии ранней обработки запроса: RouterListener
и Firewall
. Слушатель RouterListener
получает информацию о запрошенном пути из запроса Request
и пытается сопоставить этот путь с одним из известных маршрутов. Он хранит результат процесса сопоставления в объекте запроса в качестве атрибута, например, в виде имени контроллера, который соответствует найденному маршруту:
namespace Symfony\Component\HttpKernel\EventListener;
class RouterListener implements EventSubscriberInterface
{
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
$parameters = $this->matcher->match($request->getPathInfo());
// ...
$request->attributes->add($parameters);
}
}
Например, когда сопоставителя запросов (matcher) просят найти контроллер для пути /demo/hello/World
, а конфигурация маршрутов выглядит таким образом:
_demo_hello:
path: /demo/hello/{name}
defaults:
_controller: AcmeDemoBundle:Demo:hello
то параметры, возвращаемые методом match()
будут являться комбинацией значений, определённых в секции defaults:
, а также значений переменных (типа {name}
), которые будут заменены на их значения из запроса:
array(
'_route' => '_demo_hello',
'_controller' => 'AcmeDemoBundle:Demo:hello',
'name' => 'World'
);
Эти данные сохраняются в объекте Request
, в структуре типа parameter bag
, имеющей наименование attributes
. Несложно догадаться, что в дальнейшем, HttpKernel
проверит эти атрибуты и выполнит запрошенный контроллер.
Другой, не менее важный слушатель — это Firewall
. Как уже было отмечено ранее, RouterListener
не предоставляет объект Response
ядру HttpKernel, он лишь выполняет некоторые действия в начале процесса обработки запроса.Firewall
же, напротив, иногда даже принудительно возвращает некоторые предопределённые экземпляры ответов, например, когда пользователь не аутентифицирован, хотя должен был быть, так как запрашивается защищённая страница. Firewall
(посредством сложного процесса) форсирует редирект на страницу логина (например), или устанавливает некоторые заголовки, которые обязуют пользователя ввести его логин и пароль и аутентифицироваться при помощи HTTP-аутентификации.
Определение контроллера для запуска
Выше мы уже видели, что RouterListener
устанавливает атрибут запроса, именуемый _controller
и содержащий некоторую ссылку на контроллер, который необходимо выполнить. Эта информация не известна HttpKernel
. Вместо этого имеется специальный объект — ControllerResolver
, который ядро запрашивает, чтобы получить контроллер для обработки текущего запроса:
private function handleRaw(Request $request, $type = self::MASTER_REQUEST)
{
// событие "kernel.request"
...
if (false === $controller = $this->resolver->getController($request)) {
throw new NotFoundHttpException();
}
}
Резолвер является экземпляром класса, реализующего интерфейс ControllerResolverInterface
:
namespace Symfony\Component\HttpKernel\Controller;
use Symfony\Component\HttpFoundation\Request;
interface ControllerResolverInterface
{
public function getController(Request $request);
public function getArguments(Request $request, $controller);
}
Позднее он будет использоваться для определения аргументов для контроллера, но его первичной задачей является определение контроллера. Стандартный резолвер получает контроллер из атрибута _controller
обрабатываемого запроса:
public function getController(Request $request)
{
if (!$controller = $request->attributes->get('_controller')) {
return false;
}
...
$callable = $this->createController($controller);
...
return $callable;
}
Так как в большинстве случаев контроллер будет представлен в виде строки, указывающей так или иначе на класс, объект контроллера необходимо создать, перед тем как вернуть его.
… вот это всё, что может быть контроллером
ControllerResolver
из компонента HttpKernel Component поддерживает:
- Массив вызываемых объектов (callable) ([объект, метод] или [класс, статический метод])
- Вызываемые (invokable) объекты (объекты с магическим методом
__invoke()
, такие как анонимные функции, которые являются экземплярами класса\Closure
)- Классы вызываемых объектов
- Обычные функции
Все прочие определения контроллеров, которые представлены в виде строки, должны следовать шаблону
class::method
. Также ControllerResolver из FrameworkBundle добавляет дополнительные шаблоны для имён контроллеров:
- BundleName:ControllerName:actionName
- service_id:methodName
После создания экземпляра контроллера,
ControllerResolver
также проверяет, реализует ли данный контроллер интерфейсContainerAwareInterface
, и если да, то вызывает его методsetContainer()
, чтобы передать ему контейнер. Вот почему контейнер по умолчанию доступен в стандартном контроллере.
Возможность замены контроллера
Давайте же вернёмся в HttpKernel
: контроллер теперь полностью доступен и почти готов к выполнению. Но даже если мы предположим, что controller resolver выполнил всё, что в его силах, для того, чтобы подготовить контроллер к вызову перед его выполнением, сейчас имеется последний шанс заменить его каким-либо другим контроллером (которым может быть любой callable элемент). Этот шанс нам предоставляет событие KernelEvents::CONTROLLER
(kernel.controller
):
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
private function handleRaw(Request $request, $type = self::MASTER_REQUEST)
{
// событие "kernel.request"
// определяем контроллер при помощи controller resolver
...
$event = new FilterControllerEvent($this, $controller, $request, $type);
$this->dispatcher->dispatch(KernelEvents::CONTROLLER, $event);
$controller = $event->getController();
}
Вызов метода setController()
объекта класса FilterControllerEvent
делает возможной замену контролера, который был подготовлен к исполнению:
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
class ControllerListener
{
public function onKernelController(FilterControllerEvent $event)
{
$event->setController(...);
}
}
Распространение событий (event propagation)
Когда вы переопределяете промежуточный результат, например, когда вы полностью заменяете контроллер после того, как наступило событие
kernel.filter_controller
, вы можете не захотеть, чтобы прочие слушатели этого события, которые будут вызваны после вашего смогли бы провернуть тот же трюк. Вы можете это сделать, вызвав метод события:$event->stopPropagation();
Также удостоверьтесь, что ваш слушатель имеет более высокий приоритет и будет вызван первым. См. также
Регистрация слушателей событий
.
Примечательные слушатели события kernel.controller
Фреймворк сам по себе не имеет слушателей события kernel.controller
. То есть только сторонние бандлы, которые слушают это событие для того, чтобы определить тот факт, что контроллер был определён и что он будет выполнен.
Слушатель ControllerListener
из бандла SensioFrameworkExtraBundle
, к примеру, выполняет кое-какую весьма важную работу прямо перед выполнением контроллера, а именно: он собирает аннотации типа @Template
и @Cache
и сохраняет их в виде атрибутов запроса с тем же именем, но с префиксом — подчёркиванием: _template
и _cache
. Позднее, в процессе обработки запроса, эти аннотации (или конфигурации, как они названы в коде этого бандла) будут использованы для рендеринга шаблона или же для того, чтобы установить заголовки, относящиеся к кэшированию.
ParamConverterListener
из того же банла умеет конвертировать аргументы контролера, например, загружать сущность (entity) по парметру id
, определённому в маршруте:
/**
* @Route("/post/{id}")
*/
public function showAction(Post $post)
{
...
}
Преобразователи параметров (Param converters)
Бандл
SensioFrameworkExtraBundle
укомплектован конвертеромDoctrineParamConverter
, который помогает конвертировать пары имя/значение (например id), в сущности (ORM) или документы (ODM). Но вы также можете создать свои преобразователи параметров. Вам всего лишь нужно создать класс, реализующий интерфейсParamConverterInterface
, создать определение сервиса для него и присвоить ему тагrequest.param_converter
. См. также документацю к @ParamConverter.
Сбор аргументов для выполнения контроллера
После того, как отработали слушатели, которые могли бы заменить контроллер, мы можем быть уверены, что результирующий контроллер — тот, который нам нужен. Следующий шаг — сбор аргументов, которые будут использоваться для выполнения результирующего контроллера:
private function handleRaw(Request $request, $type = self::MASTER_REQUEST)
{
// событие "kernel.request"
// определяем контроллер при помощи controller resolver
// событие "kernel.controller"
...
$arguments = $this->resolver->getArguments($request, $controller);
}
Экземпляр controller resolver запрашивается для получения аргументов контроллера. Стандартный ControllerResolver
компонента HttpKernel
использует рефлексию (reflection) и атрибуты из объекта Request, для того, чтобы определить аргументы контроллера. Он перебирает все параметры метода контроллера. Для определения каждого из аргументов используется такая логика:
Выполнение контроллера
Наконец, пришло время выполнить контроллер. Получаем ответ и двигаемся дальше:
private function handleRaw(Request $request, $type = self::MASTER_REQUEST)
{
// событие "kernel.request"
// определяем контроллер при помощи controller resolver
// событие "kernel.controller"
// используем controller resolver для того, чтобы получить аргументы контроллера
...
$response = call_user_func_array($controller, $arguments);
if (!$response instanceof Response) {
...
}
}
Как вы можете помнить из документации Symfony, контроллер должен возвращать объект Response
. Если же контроллер этого не сделал, какая-то другая часть приложения должна иметь возможность конвертировать возвращаемое значение в объект Response
тем или иным образом.
Вход в слой представления (view)
Если вы решили вернуть из вашего контроллера объект Response
, вы можете таким образом срезать угол и обойти шаблонизатор, например, вернув уже готовую HTML разметку:
class SomeController
{
public function simpleAction()
{
return new Response(
'<html><body><p>Старый добрый HTML</p></body></html>'
);
}
}
Тем не менее, когда вы вернёте что-нибудь другое (как правило — массив с переменными рендеринга для шаблона), возвращаемое значение нужно конвертировать в объект Response
, прежде чем он будет использован в качестве результата, который будет отправлен на клиент (в браузер пользователя). Ядро HttpKernel
не привязано ни к какому конкретному шаблонизатору типа Twig
. Вместо этого оно использует диспетчер событий (event dispatcher), чтобы позволить любому слушателю события KernelEvents::VIEW
(kernel.view) создать правильный ответ, основанный на значении, которое вернул контроллер (даже если он полностью проигнорирует это значение):
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
private function handleRaw(Request $request, $type = self::MASTER_REQUEST)
{
// событие "kernel.request"
// определяем контроллер при помощи controller resolver
// событие "kernel.controller"
// используем controller resolver для того, чтобы получить аргументы контроллера
// исполняем контроллер
...
$event = new GetResponseForControllerResultEvent(
$this,
$request,
$type,
$response
);
$this->dispatcher->dispatch(KernelEvents::VIEW, $event);
if ($event->hasResponse()) {
$response = $event->getResponse();
}
if (!$response instanceof Response) {
// тут ядру ОООООЧЧЧЕНЬ нужен ответ...
throw new \LogicException(...);
}
}
Слушатели этого события могут использовать метод setResponse()
объекта события GetResponseForControllerResultEvent
:
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
class ViewListener
{
public function onKernelView(GetResponseForControllerResultEvent $event)
{
$response = new Response(...);
$event->setResponse($response);
}
}
Примечательные слушатели события kernel.view
Слушатель TemplateListener
из арсенала SensioFrameworkExtraBundle
получает значение, которое вернул контроллер и использует его в качестве переменных для рендеринга шаблона, который должен быть указан при помощи аннотации @Template
(храниться это значение будет в атрибуте запроса _template
):
public function onKernelView(GetResponseForControllerResultEvent $event)
{
$parameters = $event->getControllerResult();
// получаем движок шаблонизатора
$templating = ...;
$event->setResponse(
$templating->renderResponse($template, $parameters)
);
}
Фильтрация ответа
Ну и в самом конце, прямо перед тем, как вернуть объект Response
в качестве финального результата обработки текущего объекта запроса Request
, будет уведомлен любой слушатель события KernelEvents::RESPONSE
(kernel.response):
private function handleRaw(Request $request, $type = self::MASTER_REQUEST)
{
// событие "kernel.request"
// определяем контроллер при помощи controller resolver
// событие "kernel.controller"
// используем controller resolver для того, чтобы получить аргументы контроллера
// конвертируем рещультат, который вернул запрос в объект Response
return $this->filterResponse($response, $request, $type);
}
private function filterResponse(Response $response, Request $request, $type)
{
$event = new FilterResponseEvent($this, $request, $type, $response);
$this->dispatcher->dispatch(KernelEvents::RESPONSE, $event);
return $event->getResponse();
}
Слушатели события могут модифицировать объект ответа Response
и даже полностью его заменить:
class ResponseListener
{
public function onKernelResponse(FilterResponseEvent $event)
{
$response = $event->getResponse();
$response->headers->set('X-Framework', 'Symfony2');
// or
$event->setResponse(new Response(...));
}
}
Примечательные слушатели события kernel.response
Слушатель WebDebugToolbarListener
из комплекта инструментов бандла WebProfilerBundle
внедряет HTML и JavaScript код в конце ответа, для того, чтобы панель профайлера отобразилась (как правило, в конце страницы).
Слушатель ContextListener
из компонента Symfony Security Component
сохраняет сериализованную версию токена безопасности в сессии. Это позволяет ускорить процесс аутентификации при следующем запросе. В компонент Security Component
также входит слушатель ResponseListener
, который устанавливает cookie, содержащий информацию о remember-me
. Содержимое этого cookie может быть использовано для авто-логина пользователя, даже если оригинальная сессия уже была завершена.