HttpKernelInterface в Symfony

Symfony знаменит благодаря своему HttpKernelInterface:

namespace Symfony\Component\HttpKernel;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

interface HttpKernelInterface
{
    const MASTER_REQUEST = 1;
    const SUB_REQUEST = 2;

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

Реализация этого интерфейса должна содержать один метод и с его помощью иметь возможность каким-либо образом превратить полученный запрос в ответ. Если вы взглянете на любой из фронт-контроллеров в директории /web вашего Symfony проекта, вы можете увидеть, что этот метод handle() играет главную роль в обработке веб-запроса — чего и стоило ожидать:

// /web/app.php
$kernel = new AppKernel('prod', false);
// ...
$request = Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
// ... 

Сначала создаётся экземпляр ядра AppKernel. Это класс — специфичный для вашего приложения и вы можете найти его в диретории app: /app/AppKernel.php. Он позволяет регистрировать ваши бандлы и изменять некоторые основные настройки, такие как расположение директории с кэшем, или указать, какой конфигурационный файл нужно загрузить. Аргументы его конструктора — это наименование окружения и флаг активации режима отладки в ядре (debug mode).

Окружение

Окружением (или именем окружения) может быть любая строка. В основном, это способ определить, какой конфигурационный файл должен быть загружен (например, config_dev.yml или же config_prod.yml). Эта проверка производится в классе AppKernel:

public function registerContainerConfiguration(LoaderInterface $loader)
{
    $loader
        ->load(__DIR__.'/config/config_'.$this->getEnvironment().'.yml');
}

Режим отладки

В режиме отладки у вас будут следующие возможности:

  • Удобная и информативная страница ошибки, отображающая информацию о запросе для дальнейшей отладки;
  • Подробные сообщения об ошибках, если страница ошибки из предыдущего пункта не может быть отображена;
  • Исчерпывающая информация о времени выполнения отдельных частей приложения (начальная загрузка, обращения к базе данных, рендеринг шаблонов и так далее).
  • Расширенная информация о запросах (с использованием веб-профайлера и сопутствущей панели).
  • Автоматическая инвалидация кэша: эта функция позволяет не беспокоиться о том, что изменения в config.yml, routing.yml и прочих конфигурационных файлах не будут учтены без пересборки всего сервисного контейнера или сопоставителя маршрутов (routing matcher) для каждого запроса (однако, это занимает больше времени).

Продолжаем разбирать процесс обработки запроса: далее создается объект Request, базирующийся на существующих суперглобальных массивах ($_GET, $_POST, $_COOKIE, $_FILES и $_SERVER). Класс Request вместе с прочими классами компонента HttpFoundation предоставляет объектно-ориентированный интерфейс к этим суперглобальным массивам. Все эти классы также обрабатывают различные проблемные ситуации, которые могут возникать при использовании разных версий PHP или же разных платформ. В контексте Symfony всегда разумнее вместо суперглобальных переменных использовать объект Request для получения различных данных о запросе.

Итак, далее вызывается метод handle() экземпляра AppKernel. Его единственным аргументом является объект Request для текущего запроса. Аргументы для типа запроса («master») и нужно ли перехватывать и обрабатывать исключения (да, перехватывать) берутся по умолчанию. Результат метода handle() гарантированно будет экземпляром класса Response (также являющегося частью компонента HttpFoundation). И, наконец, ответ будет отправлен обратно клиенту, который сделал запрос, например, браузеру.

Загрузка ядра

Как вы уже могли догадаться, вся магия происходит внутри метода handle() в ядре. Вы можете найти реализацию этого метода в классе Kernel, который является родителем класса AppKernel:

// Symfony\Component\HttpKernel\Kernel

public function handle(
    Request $request,
    $type = HttpKernelInterface::MASTER_REQUEST,
    $catch = true
) {
    if (false === $this->booted) {
        $this->boot();
    }

    return $this->getHttpKernel()->handle($request, $type, $catch);
}

Во-первых, проверяется, что ядро загружено, прежде чем произойдёт обращение к HttpKernel. Процесс загрузки включает в себя:

  • Инициализация всех зарегистрированных бандлов;
  • Инициализация сервисного контейнера;

Бандлы и расширения контейнера

Среди Symfony-разработчиков бандлы прежде всего известны как место, где размещается разрабатываемый вами код. Каждый бандл должен иметь имя, которое отражает его назначение. Например, у вас могут быть такие бандлы: BlogBundle, CommunityBundle, CommentBundle, и так далее. Вы регистрируете ваши бандлы в AppKernel.php, добавляя их к существующему списку:

class AppKernel extends Kernel
{
    public function registerBundles()
    {
        $bundles = array(
            new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
            // ... тут прочие бандлы
            new Matthias\BlogBundle()
        );

        return $bundles;
    }
}

И это определённо хорошая идея — можно легко расширить функциональность вашего проекта, добавив лишь одну строчку кода. Тем не менее, когда мы смотрим на ядро Kernel и на то, как оно работает с бандлами, включая ваши, становится ясно, что бандлы прежде всего понимаются как способ расширения сервисного контейнера, а не как библиотеки кода. Вот почему вы найдёте директорию DependencyInjection внутри любого бандла, а внутри неё класс {наименованиеБандла}Extension. В процессе инициализации сервисного контейнера каждый бандл имеет возможность зарегистрировать собственные сервисы в сервисном контейнере, также можно добавить несколько параметров, и даже, при необходимости, модифицировать определения некоторых сервисов, прежде чем контейнер будет скомпилирован и выгружен в директорию с кэшем:

namespace Matthias\BlogBundle\DependencyInjection;

use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;

class MatthiasBlogExtension extends Extension
{
    public function load(array $configs, ContainerBuilder $container)
    {
        $loader = new XmlFileLoader($container,
        new FileLocator(__DIR__.'/../Resources/config'));

        // добавляем определения сервисов в контейнер
        $loader->load('services.xml');

        $processedConfig = $this->processConfiguration(
            new Configuration(),
            $configs
        );

        // добавляем параметр
        $container->setParameter(
            'matthias_blog.comments_enabled',
            $processedConfig['enable_comments']
        );
    }

    public function getAlias()
    {
        return 'matthias_blog';
    }
}

Имя, возвращаемое методом getAlias() — это фактически ключ, по которому вы можете устанавливать значения параметров (например в config.yml):

matthias_blog:
    enable_comments: true

Подробнее о конфигурировании бандлов вы узнаете в следующем разделе — Приёмы внедрения зависимостей. Каждый корневой ключ в конфигурации соответствует определенному бандлу. В примере выше вы могли заметить, что matthias_blog — это ключ к настройкам, относящимся к MatthiasBlogBundle. Так что для вас не будет большим сюрпризом, что это также справедливо для всех корневых ключей, которые вы можете встретить в файле config.ymlи прочих ему подобных: настройки, доступные по ключу framework относятся к FrameworkBundle, ключ security (даже если он определен в другом файле — security.yml) относится к SecurityBundle. Проще простого!

Создание сервисного контейнера

После того, как все бандлы подключили свои сервисы и параметры, создание контейнера завершается процессом, который называется «компиляция». В ходе этого процесса всё ещё остаётся возможность внести последние изменения в определения сервисов или изменить параметры. Также это хороший момент для того, чтобы проверить правильность и оптимизировать определения сервисов. После этого контейнер принимает свой окончательный вид и он дампится на диск в двух различных форматах: в XML файл с определениями всех обнаруженных сервисов и параметров и в PHP файл, готовый для использования в качестве единого и единственного сервисного контейнера для вашего приложения. Оба эти файла вы можете найти в директории с кэшем, соответствующей окружению с которым было загружено ядро, например, /app/cache/dev/appDevDebugProjectContainer.xml. XML файл выглядит как любой другой файл с определениями сервисов, только намного больше:

<service id="event_dispatcher" class="...\ContainerAwareEventDispatcher">
    <argument type="service" id="service_container"/>
    <call method="addListenerService">
        <argument>kernel.controller</argument>
        <!-- ... прочие аргументы, если нужно -->
    </call>
    <!-- ... прочие параметры сервиса, если нужно -->
</service>

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

class appDevDebugProjectContainer extends Container
{
    // ...

    protected function getEventDispatcherService()
    {
        $this->services['event_dispatcher'] =
            $instance = new ContainerAwareEventDispatcher($this);

        $instance->addListenerService('kernel.controller', ...);

        // ...

        return $instance;
    }

    //...
}

От Kernel до HttpKernel

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

// Symfony\Component\HttpKernel\Kernel

public function handle(
    Request $request,
    $type = HttpKernelInterface::MASTER_REQUEST,
    $catch = true
) {
    if (false === $this->booted) {
    $this->boot();
}

    return $this->getHttpKernel()->handle($request, $type, $catch);
}

Класс HttpKernel реализует интерфейс HttpKernelInterface и точно знает, как конвертировать запрос в ответ. Метод handle() выглядит так:

public function handle(
    Request $request,
    $type = HttpKernelInterface::MASTER_REQUEST,
    $catch = true
) {
    try {
        return $this->handleRaw($request, $type);
    } catch (\Exception $e) {
        if (false === $catch) {
            throw $e;
        }

        return $this->handleException($e, $request, $type);
    }
}

Как вы можете видеть, основная часть работы выполняется приватным методом handleRaw(), и блок try/catch здесь нужен для перехвата любых исключений. Когда аргумент $catch имеет значение true (что является значением по умолчанию для «master»-запросов), каждое исключение будет перехвачено. HttpKernel постарается найти кого-то, кто сможет создать объект Response для (см. также главу Обработка исключений.