События Symfony, приводящие к ответу

Метод 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, для того, чтобы определить аргументы контроллера. Он перебирает все параметры метода контроллера. Для определения каждого из аргументов используется такая логика:

Логика controller resolver'а

Выполнение контроллера

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

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 может быть использовано для авто-логина пользователя, даже если оригинальная сессия уже была завершена.