Symfony Advent Calendar 2017の21日目の記事です。

はじめに

新人研修でSymfony4のフレームワーク本体のコードリーディングに取り組んでいる澤井です。

弊社が提供するサービスのバックエンドは、主にSymfonyで開発しています。開発に参加するためには、Symfonyに対する深い理解が必要です。そのためにSymfonyの本体のコードリーディングを行っています。

コードリーディングは、PhpStormのステップ実行を使って全体の流れを把握した上で、個々のクラスを詳しく見ながら該当箇所の公式ドキュメントを読む、という手順で進めました。

今回は、特に重点的に調べたHttpKernel Componentについて調べたことをまとめてみました。HttpKernel Componentは、HTTPリクエストを受け取ってHTTPレスポンスを返す、Symfony4アプリケーションの骨格となるコンポーネントです。

全体の処理の流れ

Symfony4アプリケーションがHTTPリクエストを受け取ってHTTPレスポンスを返す流れは、以下のようになります。

symfony4_http_kernel_summury

  1. フロントコントローラ(index.php)が、HTTPリクエストからRequestクラスのオブジェクトを作成
  2. フロントコントローラが、Kernelクラス(Symfony\Component\HttpKernel\Kernelクラスを継承)のオブジェクトを作成
  3. Kernelオブジェクトが、サービスコンテナを初期化
  4. その結果、サービスとしてHttpKernelクラスのオブジェクトを使えるようになる
  5. HttpKernelオブジェクトが、Requestオブジェクトを受け取り、コントローラを実行し、Responseクラスのオブジェクトを返す
  6. フロントコントローラが、ResponseオブジェクトからHTTPレスポンスを作成

以下、本記事の主題であるHttpKernelクラスの働きを見ていきます。

HttpKernelクラスの働き

HttpKernelクラスは、Requestオブジェクトを受け取り、コントローラを実行し、Responseオブジェクトを返します。HttpKernelクラスは、それらの処理をイベントを使って実現します。

HttpKernelクラスが発行する主なイベントは以下のとおりです。各イベントの詳細は、Built-in Symfony Events (Symfony Docs)を参照してください。

イベント名 KernelEventsの定数 リスナーへ渡す引数
kernel.request KernelEvents::REQUEST GetResponseEvent
kernel.controller KernelEvents::CONTROLLER FilterControllerEvent
kernel.controller_arguments KernelEvents::CONTROLLER_ARGUMENTS FilterControllerArgumentsEvent
kernel.view KernelEvents::VIEW GetResponseForControllerResultEvent
kernel.response KernelEvents::RESPONSE FilterResponseEvent

以下のコマンドで、イベントへ設定されているリスナーを確認することができます。

$ bin/console debug:event-dispatcher イベント名

HttpKernelクラスの処理の流れ

HttpKernelクラスの主な処理は、HttpKernel::handle()に呼び出されるHttpKernel::handleRaw()が担います。handleRaw()のコードは以下のようになります。

HttpKernel::handleRaw()の処理内容

コード内に「著者注」として番号を付けている箇所については、このあと解説します。

<?php
// 著者注
// Symfony\Component\HttpKernel\HttpKernel
// vendor/symfony/http-kernel/HttpKernel.php

// ...

private function handleRaw(Request $request, int $type = self::MASTER_REQUEST)
{
    // ...

    // 著者注
    // (1) 
    $event = new GetResponseEvent($this, $request, $type);
    $this->dispatcher->dispatch(KernelEvents::REQUEST, $event);

    if ($event->hasResponse()) {
        return $this->filterResponse($event->getResponse(), $request, $type);
    }

    // 著者注
    // (2)
    if (false === $controller = $this->resolver->getController($request)) {
        throw new NotFoundHttpException(sprintf('Unable to find the controller for path "%s". The route is wrongly configured.', $request->getPathInfo()));
    }

    // 著者注
    // (3)
    $event = new FilterControllerEvent($this, $controller, $request, $type);
    $this->dispatcher->dispatch(KernelEvents::CONTROLLER, $event);
    $controller = $event->getController();

    // 著者注
    // (4)
    $arguments = $this->argumentResolver->getArguments($request, $controller);
    $event = new FilterControllerArgumentsEvent($this, $controller, $arguments, $request, $type);
    $this->dispatcher->dispatch(KernelEvents::CONTROLLER_ARGUMENTS, $event);
    $controller = $event->getController();
    $arguments = $event->getArguments();

    // 著者注
    // (5)
    $response = call_user_func_array($controller, $arguments);

    if (!$response instanceof Response) {
        // 著者注
        // (6)
        $event = new GetResponseForControllerResultEvent($this, $request, $type, $response);
        $this->dispatcher->dispatch(KernelEvents::VIEW, $event);

        if ($event->hasResponse()) {
            $response = $event->getResponse();
        } else {
            $msg = sprintf('The controller must return a response (%s given).', $this->varToString($response));

            if (null === $response) {
                $msg .= ' Did you forget to add a return statement somewhere in your controller?';
            }
            throw new \LogicException($msg);
        }
    }
    // 著者注
    // (7)
    return $this->filterResponse($response, $request, $type);
}

(1) kernel.requestイベントを発行

kernel.requestイベントは、複数のリスナーを持ちます。今回は、コントローラを特定するRouterListener::onKernelRequest()リスナーを見ていきます。RouterListener::onKernelRequest()リスナーは、呼び出すコントローラ名とルート名の配列を返します(キーは_controllerと_route)。

返された配列は、Request::attributes属性へ設定されます。この値は、コントローラを取得する処理で使われます。

(2) コントローラを取得

ControllerResolver::getController()は、(1)で設定したRequest::attributes属性からコントローラの情報を取得し、コントローラを作成します。

ControllerResolver::getController()の主な処理は、以下のとおりです。

  • コントローラのインスタンスを作成(引数には何も渡さない)
  • コントローラがContainerAwareInterfaceを実装している場合は、setContainer()でサービスコンテナを設定

(3) kernel.controllerイベントを発行

コントローラを実行する前に、必要な前処理を行います。kernel.controllerイベントが行う処理の例として、以下のものがあります。

  • プロファイラが有効な場合は、プロファイラの情報を集める
  • @ParamConverterが使われている場合は、ParamConverterListener::onKernelController()リスナーが、スカラー値をオブジェクトへ変換し、Requestオブジェクトに保持(このオブジェクトは、コントローラを実行するときに引数として渡される)

(4) 引数を取得

ArgumentValueResolverInterface::getArguments()で、コントローラへ渡す引数を取得します。引数の特定は、以下のように行われます。

  • コントローラの引数と同名のキー名がRequest::attributes属性にあれば、その値を引数として使う
  • コントローラの引数がタイプヒンティングにRequestクラスを指定している場合は、Requestオブジェクトを引数として使う
  • コントローラの引数が可変長引数で、Request::attributes属性に同名のキーがあり、値が配列のときは、その配列を引数として使う

(5) コントローラを実行

引数を指定してコントローラを実行します。

(6) kernel.viewイベントを発行

コントローラからの戻り値がResponseオブジェクトでない場合は、kernel.viewイベントでResponseオブジェクトへ変換します。

デフォルトでは、kernel.viewイベントのリスナーは登録されていませんが、SensioFrameworkExtraBundleをインストールすることで、リスナーが登録されます。例えば、レスポンスにTwigテンプレートを使うときは、TemplateListener::onKernelView()リスナーがテンプレートとデータをパースしてResponseオブジェクトへ変換します。

(7) kernel.responseイベントを発行

HttpKernel::filterResponse()でkernel.responseイベントが発行されます。kernel.responseイベントは、Responseオブジェクトがクライアントへレスポンスを送信する前に発行されます。このイベントをフックすることで、例えば、HTTPレスポンスヘッダを変更したり、Cookieを追加したりといった処理を差し込むすることができます。

HttpKernelクラスの処理イメージ

これまでの流れを図にすると、以下のようになります。

symfony4_http_kernel

Symfony2.8からの変更点

コントローラへ渡す引数は、Symfony2.8ではControllerResolverInterface::getArguments()で取得しますが、Symfony4ではArgumentValueResolverInterface::getArguments()で取得します。

そのため、Symfony4.0ではHttpKernelのコンストラクタの第4引数へ、ArgumentValueResolverInterfaceを実装したオブジェクトを渡す必要があります(Symfony2.8には第4引数はありません)。

また、Symfony2.8にはないkernel.controller_argumentsイベントが、Symfony4にはあります(kernel.controller_argumentsイベントはSymfony3.1から追加)。

ルーティング処理の流れ

HttpKernelクラスが担う処理の中で、ルーティングを処理する部分が複雑だと感じたので、ルーティング処理について詳しく見ていきます。

ルーティングの概要は、リクエストから呼び出すコントローラの情報をRequest::attributes属性へ設定する処理になります。

kernel.requestイベント発行

HttpKernel::handleRaw()内で、イベント名(kernel.request)とGetResponseEventオブジェクトを引数として渡してEventDispatcher::dispatch()を呼び出し、kernel.requestイベントを発行します。

<?php
// 著者注
// Symfony\Component\HttpKernel\HttpKernel
// vendor/symfony/http-kernel/HttpKernel.php

// ...

private function handleRaw(Request $request, int $type = self::MASTER_REQUEST)
{
    // ...

    $event = new GetResponseEvent($this, $request, $type);
    $this->dispatcher->dispatch(KernelEvents::REQUEST, $event); 

    // ...
}

Request::attributes属性へコントローラ情報を設定

EventDispatcher::dispatch()は、リスナーを引数として渡してEventDispatcher::doDispatcher()を呼び出します。

<?php
// 著者注
// Symfony\Component\EventDispatcher\EventDispatcher
// vendor/symfony/event-dispatcher/EventDispatcher.php

// ...

public function dispatch($eventName, Event $event = null)
{
    if ($listeners = $this->getListeners($eventName)) {
        $this->doDispatch($listeners, $eventName, $event);
    }

    // ...
}

EventDispatcher::doDispatch()は、イベントへ設定されているリスナーをすべて実行します。Kernel.requestイベントは、複数のリスナーが設定されています。その中のRouterListener::onKernelRequest()リスナーが、ルーティングを処理します。

<?php
// 著者注
// Symfony\Component\EventDispatcher\EventDispatcher
// vendor/symfony/event-dispatcher/EventDispatcher.php

// ...

protected function doDispatch($listeners, $eventName, Event $event)
{
    foreach ($listeners as $listener) {

        // ...

        \call_user_func($listener, $event, $eventName, $this); 
    }
}

RouterListener::onKernelRequest()リスナーは、Router::matcherRequest()を呼び出し、戻り値として受け取るコントローラ名とルート名の配列をRequest::attributes属性へ設定します。

例えば、Request::attributes属性へ設定される値は、以下のようなものになります。

<?php
[
    '_controlle' => 'App\\Controller\\DefaultController::index',
    '_route' => 'default',
]
<?php
// 著者注
// Symfony\Component\HttpKernel\EventListener\RouterListener

// ...

public function onKernelRequest(GetResponseEvent $event)
{
    // ...

    try {
        if ($this->matcher instanceof RequestMatcherInterface) {   
            $parameters = $this->matcher->matchRequest($request);  
        } else {
            $parameters = $this->matcher->match($request->getPathInfo());
        }

        // ...

        $request->attributes->add($parameters);
        unset($parameters['_route'], $parameters['_controller']);
        $request->attributes->set('_route_params', $parameters);
    } catch (ResourceNotFoundException $e) {

        // ...

    }
}

Router::matchRequest()は、Router::getMatcher()でルーティングのマッチング情報が書かれているsrcDevProjectContainerUrlMatcherクラスのオブジェクトを取得し、取得したオブジェクトのmatchRequest()を呼び出します(matchRequestはSymfony\Component\Routing\Matcher\UrlMatcherから継承)。

<?php
// 著者注
// Symfony\Component\Routing\Router
// vendor/symfony/routing/Router.php

// ...

public function matchRequest(Request $request)
{
    $matcher = $this->getMatcher();

    // ...

    return $matcher->matchRequest($request);
}

srcDevProjectContainerUrlMatcher::matchRequest()は、srcDevProjectContainerUrlMatcher::match()を呼び出します。

<?php
// 著者注
// Symfony\Component\Routing\Matcher\UrlMatcher

// ...

public function matchRequest(Request $request)
{
    $this->request = $request;

    $ret = $this->match($request->getPathInfo());  

    $this->request = null;

    return $ret;
}

srcDevProjectContainerUrlMatcher::match()で、ルーティングのマッチングを行いコントローラを特定しコントローラ情報を返します。

<?php
// 著者注
// srcDevProjectContainerUrlMatcher
// var/cache/dev/srcDevProjectContainerUrlMatcher.php

// ...

class srcDevProjectContainerUrlMatcher extends Symfony\Bundle\FrameworkBundle\Routing\RedirectableUrlMatcher
{
    // ...

    public function match($pathinfo)
    {
        // ...

        // default
        if ('/default' === $pathinfo) {
            return array (  '_controller' => 'App\\Controller\\DefaultController::index',  '_route' => 'default',);
        }

        // ...

補足:サービスコンテナ作成の流れ

本記事では、Symfony4で中心的な役割を担うHttpKernelクラスを見てきました。

Symfony4は、各種の機能をサービスとして提供します。Symfony4内部では、HttpKernelもサービスとして提供されます。そこで、サービスを管理するサービスコンテナが作成される流れを調べました。

サービスコンテナが作成される流れは、以下のようになります。

  1. index.phpがSymfony\Component\HttpKernel\Kernel::handle()を呼び出します(以下のメソッドは全てSymfony\Component\HttpKernel\Kernelクラスのメソッド)。
  2. handle()はboot()を呼び出します。
  3. boot()はinitializeContainer()を呼び出します。
  4. initializeContainer()は、サービスコンテナを作成しKernel::containerへ設定します。

以下、作成の流れを詳しく見ていきます(XXXXはSymfony4が自動で命名するため、環境により異なります)。

(1) Kernel:handle()を呼び出す

<?php
// 著者注
// public/index.php

// ...

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

// ...

(2) Kernel::handle()からKernel::boot()を呼び出す

<?php
// 著者注
// Symfony\Component\HttpKernel\Kernel(App/Kernelの親クラス)
// vendor/symfony/http-kernel/Kernel.php

// ...

namespace Symfony\Component\HttpKernel;

// ...

abstract class Kernel implements KernelInterface, RebootableInterface, TerminableInterface
{
    // ...

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

        // ...
    }

    // ...
}

(3) Kernel:boot()がサービスコンテナを初期化するinitializeContainer()を呼び出す

<?php
// 著者注
// Symfony\Component\HttpKernel\Kernel
// vendor/symfony/http-kernel/Kernel.php

// ...

public function boot()
{
    // ...

    $this->initializeContainer();

    // ...
}

(4) Kernel::initializeContainer()がKernel::containerへサービスコンテナを設定する

<?php
// 著者注
// Symfony\Component\HttpKernel\Kernel

// ...

protected function initializeContainer()
{
    // ...

    $this->container = require $cache->getPath(); 

    // ...
}

$cache->getPath()が返すvar/cache/dev/srcDevProjectContainer.phpの中身は、以下のようになります。このファイルで、ContainerXXXX\srcDevProjectContainerクラスのオブジェクトを作成します。このオブジェクトがサービスコンテナです。

<?php
// 著者注
// var/cache/dev/srcDevProjectContainer.php
if (!class_exists(\ContainerXXXX\srcDevProjectContainer::class, false)) {
    require __DIR__.'/ContainerXXXX/srcDevProjectContainer.php';   
}

if (!class_exists(srcDevProjectContainer::class, false)) {
    class_alias(\ContainerXXXX\srcDevProjectContainer::class, srcDevProjectContainer::class, false);
}

return new \ContainerXXXX\srcDevProjectContainer();

srcDevProjectContainerクラスは、Symfony\Component\DependencyInjection\Containerを継承しているので、get()で保持しているサービスを取得できます。

<?php
// 著者注
// サービスコンテナ
// var/cache/dev/ContainerXXXX/srcDevProjectContainer.php

// ...

namespace ContainerXXXX;

use Symfony\Component\DependencyInjection\Container;

// ...

class srcDevProjectContainer extends Container
{
   // ...
}

おわりに

今回は、HttpKernelクラスの処理について見てきました。Symfony4が行っている処理を知ることで、Symfony4の理解が深まったと感じています。

最初は、Symfony4のような複雑で巨大なコードを読むことができるか不安でしたが、ステップ実行などの道具を使って少しずつ読んでいくことで、当初考えていたよりもコードを読むことができ、Symfony4の理解を深めることができました。

これからも、自分が使うツールに自信を持つために、コードリーディングを続けていきたいと思います。