Symfony. Компонент HttpKernel.
Symfony

Вольный перевод официальной документации.

Компонент HttpKernel обеспечивает структурированный процесс, в результате которого объект Request преобразуется в Response, при помощи компонента EventDispatcher.

HttpKernel достаточно гибок, чтобы на его базе создавать полнофункциональные фреймворки (Symfony), микро-фреймвори (Silex) или мощные CMS системы (Drupal).

Установка

Вы можете установить компонент двумя разными способами:

Затем, подключить в своем скрипте автозагрузчик – require ‘vendor/autoload.php’, для включения механизма автозагрузки, который предоставляет composer. В противном случае, ваше приложение не сможет воспользоваться классами данного компонента Symfony.

Процесс работы

Каждое взаимодействие через HTTP, начинается с запроса и заканчивается ответом. Ваша задача, как разработчика, создать PHP код, который проанализирует запрос и сгенерирует нужный ответ.

Схематично данный процесс можно описать следующим образом:

  • Пользователь набирает в браузере адрес сайта
  • Браузер отправляет запрос на сервер
  • Symfony предоставляет разработчику объект Request
  • Разработчик, на основании объекта Request, подготавливает объект Response
  • Сервер посылает ответ браузеру пользователя
  • Браузер отображает ответ пользователю

Как правило, для обработки запросов используется какая-то готовая система или фреймворк, в задачи которого входит обеспечение безопасности, маршрутизации и т.д. так чтобы разработчик мог заниматься непосредственно самим проектом, а не отвлекаться на разработку вспомогательных инструментов. Тем более, что они из проекта в проект чаще всего идентичны.

Конкретные реализации таких систем сильно различаются. Компонент HttpKernel предоставляет интерфейс, которые описывает процесс, начиная с запроса, заканчивая ответом. Таким образом, компонент может быть использован, как основа для любого проекта или фреймворка, независимо от архитектуры, которая будет построена поверх него.

namespace Symfony\Component\HttpKernel;

use Symfony\Component\HttpFoundation\Request;

interface HttpKernelInterface
{
    // ...

    /**
     * @return Response A Response instance
     */
    public function handle(
        Request $request,
        $type = self::MASTER_REQUEST,
        $catch = true
    );
}

Внутри компонента, HttpKernel::handle() – конкретная реализация интерфейса HttpKernelInterface::handle() – определяет метод, который принимает объект Request, а возвращает Response.




Изучение работы данного процесса (последовательности) – ключ к пониманию работы компонента (а значит и фреймворка Symfony или любого другого продукта, построенного на нем).

HttpKernel работает на событиях:

HttpKernel::handle() — генерирует события. Это делает метод и гибким и абстрактным, поскольку вся «работа» фреймворка/проекта построенного на компоненте, на самом деле, выполняется в обработчиках этих событий.

Для лучшего понимания, рассмотрим каждый этап и покажем какие именно события и как реализованы, на примере фреймворка Symfony.

В целом, использование HttpKernel очень простое: включает в себя создание экземпляра EventDispatcher (диспетчер событий), а так же экземпляров ArgumentResolver и ControllerResolver, о которых детально поговорим позже. Для запуска ядра вам потребуется добавить обработчики событий, которые рассмотрим далее:

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpKernel;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Controller\ArgumentResolver;
use Symfony\Component\HttpKernel\Controller\ControllerResolver;

// создаем объект Request
$request = Request::createFromGlobals();

$dispatcher = new EventDispatcher();
// ... добавляем обработчики

// создаем controllerResolver и ArgumentResolver
$controllerResolver = new ControllerResolver();
$argumentResolver = new ArgumentResolver();

// инициализируем HttpKernel
$kernel = new HttpKernel($dispatcher, $controllerResolver, new RequestStack(), $argumentResolver);

// запускаем метод ядра, который на базе Request вернет Response
// посредствам генерирования событий и вызова контроллера-обработчика.
$response = $kernel->handle($request);

// отправляем браузеру заголовки и контент
$response->send();

// генерируем событие kernel.terminat
$kernel->terminate($request, $response);

Полностью рабочий пример приводится в конце статьи.

Предупреждение! С версии 3.1, конструктор HttpKernel может принимать четвертый аргумент – объект, который должен быть реализацией интерфейса ArgumentResolverInterface. В версии 4.0 этот аргумент станет обязательным.

1) Событие kernel.request

Обычно используется, для добавления информации (заполнения) объекта Request, инициализации подсистем фреймворка, или сразу же для возврата объекта Response, если это необходимо (например когда система безопасности запретила доступ). Возврат Response, по сути, означает завершение работы.

Kernel.request – самое первое событие, которое генерирует HttpKernel::handle(). На него могут быть подписаны различные обработчики (listeners).




Некоторые обработчики событий – такой как система безопасности – могут располагать достаточной информацией, чтобы создать объект Response сразу же. Например, если в ходе проверки выяснится, что пользователь не имеет доступа, обработчик может вернуть объект RedirectResponce для перенаправления на страницу авторизации. Или ошибку 403 – доступ запрещен.

Если на данном этапе обработчиком возвращается объект Response, все промежуточные события пропускаются и процесс переходит сразу же к событию kernel.response.




Остальные обработчики просто инициализируют подсистемы фреймворка, или добавляют в объект Request дополнительную информацию. Например, обработчик может определять и записывать в объект требуемый язык.

Еще один часто используемый обработчик – URI роутер. Он анализирует информацию, содержащуюся в Request, и определяет, какой контроллер должен быть вызван для генерации контента. По сути, объект Request содержит, своего рода, хранилище данных, которое прекрасно подходит для передачи параметров между подсистемами. То есть, если роутер определил, какой должен быть вызван контроллер, он может сохранить эту информацию в данном объекте. Она будет использована в дальнейшем другой подсистемой, такой как ControllerResolver, к примеру.

Когда один из обработчиков события kernel.request возвращает объект Response, выполнение других обработчиков данного события прекращается. Другими словами обработчики с меньшим приоритетом выполнены не будут.

Kernel.request во фреймворке Symfony.

Наиболее важным подписчиком события kernel.request во фреймворке, является RouterListner. Этот класс запускает слой маршрутизации, который возвращает массив с дополнительной информацией на основании запроса, включая имя контроллера (_controller) и все дополнительные параметры из маршрута, например {slug}. Для более подробной информации, смотрите описание компонента Routing

2) Подготовка контроллера (Resolve the Controller)

Допустим, обработчики события kernel.request не вернули Response. Следующий этап компонента HttpKernel – определить и подготовить контроллер. Контроллер — это часть вашего кода, которая создает и заполняет объект Response для конкретной страницы сайта. Единственное требование к нему – возможность быть вызванным (функция, метод или Closure).

Но как вы будете определять, какой именно контроллер следует вызвать, зависит исключительно от вашего проекта. Эту задачу решает «controller resolver» — класс, который реализует интерфейс ControllerResolverInterface. Объект данного класса передается в конструктор HttpKernel при инициализации.




Ваша задача, написать класс, и реализовать два метода: getController() и getArguments(). Вообще-то, реализация по умолчанию уже существует, вы можете ее использовать или изучить ее код, в целях обучения – класс ControllerResolver из первого примера. Код его интерфейса приведен ниже:

namespace Symfony\Component\HttpKernel\Controller;

use Symfony\Component\HttpFoundation\Request;

interface ControllerResolverInterface
{
    public function getController(Request $request);

    public function getArguments(Request $request, $controller);
}

Предупреждение! Метод getArgument() класса ControllerResolver и соответствующий ему метод, описанный в интерфейсе, объявлен устаревшим, начиная с версии 3.1 и будет удален в версии 4.0. Вы можете использовать ArgumentResolver, который реализует ArgumentResolverInterface, вместо этого.

Внутри метода HttpKernel::handle() сначала вызывается getController() объекта ControllerResolver. В этот метод передается Request и он должен вернуть вызываемую функцию (PHP callable), т.е. контроллер, выбранный на основании информации, содержащейся в Request.

Второй метод – getArguments(), будет вызван после события kernel.controller.

Подготовка контроллера во фреймворке Symfony

Symfony использует встроенный ControllerResolver. (На самом деле, используется подкласс, отнаследованный от него, который расширяет фнкционал). Этот класс извлекает информацию из объекта Request, которую записал туда роутер (RouterListner).

getController

ControllerResolver ищет ключ _controller, в объекте Request. Помните, говорили о «хранилище» для передачи параметров между подсистемами? Эта информация помогает «вернуть» PHP callable следующим образом:

  1. Имя контроллера (ключ _controller), записанное в формате AcmeDemoBundle:Default:index, заменяется на строку другого формата (которая содержит полное имя класса и метода), принятого в Symfony – то есть Acme\DemoBundle\Controller\DefaultController::indexAction. Правила этого преобразования как раз и реализованы в подклассе ControllerResolver’a, они специфичны для Symfony.
  2. Создается объект вашего целевого контроллера, причем никакие параметры в его конструктор не передаются.
  3. Если ваш контроллер реализует интерфейс ContainerAwareInterface, вызывается метод setContainer(), у объекта (п.2) и в него передается контейнер. Этот шаг тоже реализован в подклассе ControllerResolver (поставляется/используется вместе с Symfony).

3) Событие kernel.controller

Обычно используется для инициализации подсистем или изменений контроллера, перед тем как он будет запущен.

После того, как контроллер был определен и подготовлен, HttpKernel::handle() генерирует событие kernel.controller. Подписчики данного события могут инициализировать части системы, необходимые после того, как некоторые вещи были определены (т.е. имя контроллера, информация о роуте и т.д.), но перед тем как он будет запущен.




Обработчики данного события так же могут полностью поменять контроллер на другой, с помощью вызова setController() объекта FilterControllerEvent, который им передается.

Kernel.controller во фреймворке Symfony

Есть несколько обработчиков события kernel.controller в Symfony и они по большей части касаются сбора информации для профайлера (дебаггера), когда тот активирован.

Одни интересный обработчик подключается из SensioFrameworkExtraBundle, он идет из коробки Standard Edition. Функционал @ParamConverter позволяет передать объект целиком (к примеру, Post объект) вашему контроллеру, вместо скалярных значений (параметра id, что был в роуте). Обработчик ParamConverterListner использует рефлексию (reflection), чтобы «посмотреть» на каждый из параметров метода вашего контроллера, и пытается применить различные подходы, для конвертирования их в объект, который потом сохраняется в атрибутах Request. В следующей секции станет ясно, почему это важно.

4) Извлечение аргументов контроллера (вызываемого метода)

Далее, HttpKernel::handle() вызывает ArgumentResolverInterface::getArguments(). Помните, что контроллер возвращенный методом getController() можно запустить (callable). Цель getArguments() вернуть массив аргументов, которые должны быть переданы методу. Как именно это будет реализовано, полностью зависит от вас, однако, встроенный ArgumentResolver может быть хорошим примером.




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

Получение аргументов контроллера во фреймворке Symfony

Теперь, когда вы знаете что такое вызываемый контроллер (обычно метод ), ArgumentResolver использует reflection (механизм PHP), применительно к объекту вашего класса, и получает имена каждого из аргументов. Потом он перебирает их все, и решает, какое значение должно быть передано для каждого из них следующим образом:

  1. Если в объекте Request, точнее в «хранилище атрибутов», содержится ключ, соответствующий имени аргумента, используется значение оттуда. Например, имя первого аргумента контроллера – $slug, и в атрибутах объекта Request присутствует slug, то используется его значение. Обычно slug записывается в «хранилище» Request обработчиком RouterListner.
  2. Если у аргумента указан тип Symfony — Request, тогда в качестве значения аргумента передается объект Request. Если вы расширили класс Request своим подклассом, то передается в качестве аргумента он.
  3. Когда у контроллера переменное количество аргументов (variadic function) и «хранилище» Request содержит массив для этого аргумента, он будет доступен через variadic.

Перечисленные функции предоставляются резолверами, которые реализуют ArgumentValueResolverInterface. Существует четыре реализации, которые отвечают за стандартное поведение Symfony, но вы можете их кастомизировать. Реализовав указанный интерфейс, и передав объект в ArgumentResolver, вы можете расширить описанный функционал.

5) выполнение контроллера

Следующий шаг прост! HttpKernel::handle() запускает контроллер.




Задача контроллера сформировать ответ для текущего запроса. Это может быть HTML страница, JSON строка или что-то другое. В отличие от других этапов, которые мы рассматривали, данный шаг полностью реализуется разработчиком проекта, для каждой его страницы.

Обычно, контроллер возвращает объект Response. Если это так, работа ядра на этом практически закончена. Следующим шагом будет только событие kernel.response.




Но если контроллер возвращает нечто отличное от Response, тогда ядру еще есть над чем поработать. Оно генерирует kernel.view (ибо конечная цель всегда объект Response).

Контроллер обязательно должен что-то возвращать. Если он вернет null, будет немедленно выброшено исключение.

6) Событие kernel.view

Цель: трансформировать возвращенные контроллером данные в объект Response, если требуется.

Когда контроллер возвращает просто данные, а не объект Response, ядро генерирует еще одно событие – kernel.view. Задача его обработчиков, получить то, что вернул контроллер (например массив данных) и создать из него объект Response.




Данная особенность может быть полезной, если вы планируете использовать слой view. На данном этапе, если ни один из подписчиков события не вернул Response, выбрасывается исключение. Потому что либо контроллер, либо обработчик должен всегда возвращать упомянутый объект.

Если один из подписчиков события вернул Response, цепочка прекращается, так что с более низким приоритетом не будут выполнены.

Kernel.view во фреймворке Symfony

По умолчанию нет обработчиков данного события. Однако один из бандлов – SensioFrameworkExtraBundle добавляет его. Если ваш контроллер возвращает массив, и вы указали в аннотации @Template перед ним, тогда обработчик генерирует страницу на базе указанного темплейта и возвращает объект Response.

Так же существуцет еще один популярный бандл – FOSRestBndle, который обеспечивает гибкий слой view. Может возвращать различные типы контента (HTML, JSON, XML и т.д.)

7) Событие kernel.response

Обычно используется для модификации объекта Response, прежде чем он будет отправлен пользователю.

Конечная цель ядра (HttpKernel) – создание объекта Response из Request. Он может быть получен в процессе обработки события kernel.request, возвращен непосредственно контроллером, или одним из обработчиков события kernel.view.

Независимо, кто именно создал Response, еще одно событие будет сгенерировано сразу же после этого – kernel.response. Задача обработчиков последнего, модифицировать объект каким-либо образом, — добивать в него дополнительные заголовки, куки или даже изменить его контент (например, добавить JavaScript перед тэгом ).

После обработки события, полностью готовый объект Response будет возвращен методом handle() о котором мы все время говорили. Дальше вы можете вызвать send(), он отправит заголовки и контент пользователю.

kernel.response во фреймворке Symfony.

Несколько обработчиков данного события предусмотрены фреймворком, цель которых модифицировать ответ каким-то образом. Например, WebDebugToolbarListner добавляет JavaScript в конец страницы, когда вы находитесь в окружении dev. В результате, будет отображена панель инструментов. Другой обработчик, ContextListner сериализует информацию о текущем пользователе и записывает в сессию, чтобы сохранить между запросами.

8) Событие kernel.terminate

Используется для выполнения «тяжелых» действий, после того как ответ был отправлен клиенту.

Самое последнее и уникальное событие HttpKernel, потому что обрабатывается после handle() и после того, как страница отправлена в браузер пользователя. Помните, чем заканчивался код из примера?

// отправляем браузеру заголовки и контент
$response->send();

// генерируем событие kernel.terminat
$kernel->terminate($request, $response);

Как видите, вызов $kernel->terminate(), после отправки данных, сгенерирует событие kernel.terminate. В обработчиках вы можете производить определенные действия, которые можно отложить и которые могли бы затормозить отправку ответа пользователю. Для комфорта пользователей, ответ должен быть готов как можно быстрее. В качестве примера действия, которое можно отложить на потом, можно привести отправку электронного письма.

Важно! Внутри HttpKernel использует встроенную в PHP функцию fastcgi_finish_request(). В настоящий момент только сервер PHP FPM имеет возможность отправки ответа пользователю, до того, как процесс PHP будет завершен. При использовании других серверов, подписчики события kernel.terminate все же будут работать, но ответ клиенту не будет отправлен вплоть до их завершения.

Kernel.terminate во фреймворке Symfony

Если вы используете SwiftmailerBundle с memory spooling, тогда EmailSenderListener активируется, — он отправляет почту, которую вы поставили в очередь во время запроса, после отсылки данных браузеру.

Работа с исключениями: событие kernel.exception

Цель: обработать некоторые типы исключений и на их основе создать подходящий объект Response.

В том случае, когда выбрасывается исключение (throw exception), независимо от точки выполнения внутри HttpKernel::handle(), генерируется другое событие – kernel.exception. Внутри, метод handle() завернут в блок try-catch. Поэтому, когда выбрасывается исключение, начинается обработка события kernel.exception, чтобы система могла каким-то образом среагировать.




Каждому подписчику данного события, передается объект GetResponseForExceptionEvent, который вы можете использовать для доступа к оригинальному исключению, через метод getException(). Обычно, обработчик проверяет тип исключения и создает подходящий объект Response.

Например, для генерации страницы 404, вы можете выбросить специальный тип исключения, затем добавить обработчик события kernel.exception, который будет «ждать» указанное исключение, чтобы создать и вернуть Response (страницу) с ошибкой 404. Вообще-то, компонент HttpKernel уже поставляется вместе с ExceptionListner и может обрабатывать данный тип исключений по умолчанию, и не только его.

Если один из обработчиков события kernel.exception, возвращает объект Response, дальнейшее выполнение цепочки обработчиков прекращается. Это значит, те, что с меньшим приоритетом не будут выполнены.

Kernel.exception во фреймворке Symfony

ExceptionListener из HttpKernel, является основным для компонента и называется ExceptionListener. Решает несколько задач:

  1. Выброшенное исключение конвертируется в объект FlattenException, который содержит всю информацию о запросе, но может быть сериализован и выведен на экран.
  2. Оригинальное исключение реализует HttpExceptionInterface, то есть доступны методы getStatusCode() и getHeaders(). Они нужны для заполнения заголовков и статуса объекта FlattenException. Всё это используется на следующем шаге, для генерирования окончательного ответа. Если вы хотите добавить определенные HTTP заголовки, используйте метод setHeaders(), класса HttpException.
  3. Оригинальное исключение реализует RequestExceptionInterface, тогда статус объекта FlattenException заполняется кодом 400, и никакие заголовки не модифицируются
  4. Контроллер выполнился и «передал» объект FlattenException. Другой контроллер, в свою очередь, передается обработчику события, через конструктор. В итоге последний возвращает объект Response для страницы с ошибкой.

ExceptionListner компонента Security — другой важный обработчик , его цель перехватывать исключения, связанные с безопасностью, и «помогать» пользователю пойти авторизацию, к примеру. (т.е. перенаправить на страницу с формой)

Создание обработчика события.

Как вы уже догадались, вы можете создавать и привязывать обработчики к любому событию, генерируемому компонентом, во время выполнения handle(). Обычно, это PHP класс с методами, которые необходимо выполнить, но это может быть что угодно. Для более полной информации, изучите документацию компонента EventDispatcher.

Имя каждого события ядра, определено константой в классе KernelEvents. Каждому обработчику передается единственный аргумент — объект подкласса KernelEvent. Он содержит информацию о текущем состоянии системы. Каждое событие передает свой:

Имя события: KernelEventsConstant: Объект, передаваемый в обработчик события:
kernel.request KernelEvents::REQUEST GetResponseEvent
kernel.controller KernelEvents::CONTROLLER FilterControllerEvent
kernel.view KernelEvents::VIEW GetResponseForControllerResultEvent
kernel.response KernelEvents::RESPONSE FilterResponseEvent
kernel.finish_request KernelEvents::FINISH_REQUEST FinishRequestEvent
kernel.terminate KernelEvents::TERMINATE PostResponseEvent
kernel.exception KernelEvents::EXCEPTION GetResponseForExceptionEvent

Полный пример использования

При использовании HttpKernel, можете свободно добавлять любые обработчики событий ядра, использовать резолверы контроллеров, если они реализуют ControllerResolverInterface. А так же использовать любые резолверы аргументов, если они реализуют ArgumentResolverInterface. Однако, HttpKernel уже поставляется со встроенными обработчиками и всем необходимым для создания рабочей системы.

use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Controller\ArgumentResolver;
use Symfony\Component\HttpKernel\Controller\ControllerResolver;
use Symfony\Component\HttpKernel\EventListener\RouterListener;
use Symfony\Component\HttpKernel\HttpKernel;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;

$routes = new RouteCollection();
$routes->add('hello', new Route('/hello/{name}', array(
    '_controller' => function (Request $request) {
        return new Response(
            sprintf("Hello %s", $request->get('name'))
        );
    })
));

$request = Request::createFromGlobals();

$matcher = new UrlMatcher($routes, new RequestContext());

$dispatcher = new EventDispatcher();
$dispatcher->addSubscriber(new RouterListener($matcher, new RequestStack()));

$controllerResolver = new ControllerResolver();
$argumentResolver = new ArgumentResolver();

$kernel = new HttpKernel($dispatcher, $controllerResolver, new RequestStack(), $argumentResolver);

$response = $kernel->handle($request);
$response->send();

$kernel->terminate($request, $response);

Подзапросы

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




Для выполнения подзапроса, используйте HttpKernel::handle(), но изменив второй параметр:

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpKernelInterface;

// ...

// Создаем другой запрос вручную, если нужно
$request = new Request();

// для примера указываем имя контроллера явным образом
$request->attributes->set('_controller', '...');

$response = $kernel->handle($request, HttpKernelInterface::SUB_REQUEST);
// что-то делает с полученным ответом
</pre>

Этот код создает еще один полный цикл запрос-ответ, в котором новый объект Request преобразуется в Response. Единственная разница, что некоторые обработчики событий (например, компонента Security), работают исключительно с основным (master) запросом. Каждому подписчику передается какой-то из подклассов KernelEvent (см. таблицу), где реализован метод isMasterRequest(), для проверки, является ли текущий запрос основным.

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

<pre lang="php">
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
// ...

public function onKernelRequest(GetResponseEvent $event)
{
    if (!$event->isMasterRequest()) {
        return;
    }

    // ...
}

Поиск ресурсов (Locating Resources)

Компонент HttpKernel отвечает так же за механизм бандлов (Bundle), используемый в Symfony. Главная фишка бандлов, что они могут переопределять любые ресурсы, используемые в проекте (конфигурационные файлы, темплейты, контроллеры, языковые файлы и т.д.)

Механизм переопределения работает благодаря тому, что ресурсы указываются не по их точному пути, а по логическому. Например, файл services.xml, который хранится в директории Resources/config/ бандла AppBundle, указывается как @AppBundle/Resources/config/services.xml. Этот логический путь будет использован при переопределении данного файла, даже если вы измените директорию AppBundle.

Компонент HttpKernel содержит метод locateResource(), он используется для перевода логических путей в фактические:

use Symfony\Component\HttpKernel\HttpKernel;

// ...
$kernel = new HttpKernel($dispatcher, $resolver);
$path = $kernel->locateResource('@AppBundle/Resources/config/services.xml');

Добавить комментарий