[Symfony2]Symfony2 Deep Tour 4

前回のDeep Tour 3では、EventDispatcherだけを解説したので、いよいよHttpKernel::handle()の中を見ていきます。

HttpKernel::handle()メソッド [@/src/vendor/symfony/src/Symfony/Component/HttpKernel/HttpKernel.php]

Kernel::handle()から、HttpKernel::handle()が呼ばれます。HttpKernel::handle()メソッドは、基本的にはparent::handle()、すなわちBaseHttpKernelクラスのhandle()メソッドを呼び出すだけなのですが、前後に$currentRequestという変数があり、すでにDIコンテナに登録されているRequestインスタンスを取得してKernelクラスから渡ってきたRequestに置き換えています。どういう場合に既にDIコンテナに登録されているのかはわかりませんので、このへんはいずれまた調査します。

ちなみに、var_dump()を挟んでみたら、HttpKernel::handle()が2回実行されていることがわかりました。うーわかんね。

とりあえず、parent::handle()にあたる、BaseHttpKernel::handle()メソッドを見てみましょう。

BaseHttpKernel::handle()メソッド [@/src/vendor/symfony/src/Symfony/Component/HttpKernel/BaseHttpKernel.php]

try〜catchを無視すれば、BaseHttpKernel::handleRaw()メソッドを呼び出しているだけです。もし、例外が発生して内部のクラスでキャッチされなかった場合は、’core.exception’イベントが発生するようです。

BaseHttpKernel::handleRaw()メソッド [@/src/vendor/symfony/src/Symfony/Component/HttpKernel/BaseHttpKernel.php]

まず初めに、’core.request’イベントが発生します。$this->dispatcher->notifyUntil()で呼び出しているので、最初に実行結果がnullを返さないイベントリスナーが見つかるまで実行されます。ちなみに、前回解説したように、’core.request’イベントにはRequestListener::resolve()メソッドがイベントリスナーとして登録されています。

補足議題:RequestListenerクラス [@/src/vendor/symfony/src/Symfony/Bundle/FrameworkBundle/RequestListener.php]

EventDispatcherによって’core.request’イベントのリスナーとしてRequestListener::resolve()メソッドが実行されます。RequestListenerクラスの説明は全くしていませんでしたが、サービスの定義はFrameworkBundleのweb.xmlにあり、引数としてrouterサービスとloggerサービスを必要とします。routerサービスの定義はrouting.xmlにあり、第1引数はrouting_loaderサービス、第2引数はrouting.resource変数、第3引数は様々なクラス名のコレクション(配列)となっています。実際のインスタンス生成時のphpコードを載せておきます。

[phpcode]
protected function getRouterService()
{
if (isset($this->shared[‘router’])) return $this->shared[‘router’];

$class = ‘Symfony\\Component\\Routing\\Router’;
$instance = new $class(
$this->getRouting_LoaderService(),
‘/mnt/hgfs/localweb02/symfony-sandbox/app/config/routing_dev.yml’,
array(
‘cache_dir’ => ‘/mnt/hgfs/localweb02/symfony-sandbox/app/cache/dev’,
‘debug’ => true,
‘generator_class’ => ‘Symfony\\Component\\Routing\\Generator\\UrlGenerator’,
‘generator_base_class’ => ‘Symfony\\Component\\Routing\\Generator\\UrlGenerator’,
‘generator_dumper_class’ => ‘Symfony\\Component\\Routing\\Generator\\Dumper\\PhpGeneratorDumper’,
‘generator_cache_class’ => ‘app’.’_’.’dev’.’UrlGenerator’,
‘matcher_class’ => ‘Symfony\\Component\\Routing\\Matcher\\UrlMatcher’,
‘matcher_base_class’ => ‘Symfony\\Component\\Routing\\Matcher\\UrlMatcher’,
‘matcher_dumper_class’ => ‘Symfony\\Component\\Routing\\Matcher\\Dumper\\PhpMatcherDumper’,
‘matcher_cache_class’ => ‘app’.’UrlMatcher’
)
);
$this->shared[‘router’] = $instance;

return $instance;
}
[/phpcode]

ちなみに、loggerサービスは、ZendBundleのZendExtensionクラスから登録されます。ZendBundleに含まれるlogger.xmlにはzend.loggerというサービス名で登録されていますが、config_dev.ymlなどに、「zend.config { logger: ~}」という設定があれば、ZendExtension::registerLoggerConfiguration()メソッドが呼ばれて、そのなかで、zend.loggerサービスにloggerというエイリアスがはられます。これでloggerサービスも使える状態になります。ちなみに、内部ではloggerがなくても動くように設計されています。

RequestListener::resolve()メソッド [@/src/vendor/symfony/src/Symfony/Bundle/FrameworkBundle/RequestListener.php]

resolve()メソッドの冒頭では、リクエストの種類がMASTER_REQUESTの場合だけ、Router::setContext()メソッドが呼ばれます。引数のコンテキスト配列に与えるのは、RequestクラスのgetBaseUrl()、getMethod()、getHost()、isSecure()メソッドを与えます。getBaseUrl()メソッドは「/app_dev.php」などのUrlを返します。getMethod()メソッドは、HTTPのメソッド(GET・POST・PUT・DELETEなど)を返します。getHost()メソッドはホスト名に相当する文字列を返します。isSecure()メソッドはHTTPかどうかを判定して返します。

その後、Router::match()メソッドにRequest::getPathInfo()メソッドを与えて、一致するルーティングルールを検索します。この中身はちょっと複雑なので(読みづらいので)飛ばします。返り値として、以下のようなパラメータを返します。このパラメータをRequest::attributesパラメータバッグに登録します。

// /app_dev.phpのRouter::match()の返り値

  1. array(2) { ["_controller"]=> string(72) "Symfony\Bundle\FrameworkBundle\Controller\DefaultController::indexAction" ["_route"]=> string(8) "homepage" }

// /app_dev.php/hello/UechocoのRouter::match()の返り値

  1. array(4) { ["_controller"]=> string(63) "Application\HelloBundle\Controller\HelloController::indexAction" ["_format"]=> string(4) "html" ["name"]=> string(7) "Uechoco" ["_route"]=> string(5) "hello" }

RequestListener::resolve()メソッドは、コントローラの選定を終えて、Requestクラスのインスタンスにその情報を保存しておくだけです。

BaseHttpKernel::handleRaw()メソッド [@/src/vendor/symfony/src/Symfony/Component/HttpKernel/BaseHttpKernel.php]

‘core.request’イベントはEventDispatcher::notifyUntil()で呼ばれていたので、1つのイベントリスナーで処理されたらイベントの実行は終わります。’core.request’イベントはBaseHttpKernel::handleRaw()メソッドから呼ばれていました。’core.request’イベントの後はControllerResolver::getController()メソッドでコントローラのcallable(call_user_func()関数で呼べる形式)を取得します。たいていはarray(コントローラクラス, アクションメソッド)形式だと思います。

ControllerResolverクラスは、/src/vendor/symfony/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerResolver.phpに定義されていますが、/src/vendor/symfony/src/Symfony/Component/HttpKernel/Controller/ControllerResolver.phpの同名のクラスを継承しています。

この後、’core.controller’イベントがEventDispatcher::filter()メソッドで呼ばれています。標準ではこのイベントに対するリスナーはないようです。コントローラを横取りして別のコントローラに置き換えるとか、値を入れこむとか、そういう処理ができるのかも知れません。

そして、ControllerResolver::getArguments()メソッドでコントローラのメソッドに与える引数を取得し、call_user_func_array()でコントローラを呼び出します。返り値はResponseクラスを期待しています。

この後、’core.view’イベントがEventDispatcher::filter()メソッドで呼ばれています。標準ではこのイベントに対するリスナーはないようです。Responseクラスのオブジェクトに対して、Viewレンダリングされる前に加工を施すようなことができそうです。

この後はBaseHttpKernel::filterResponse()メソッドが呼ばれて、先程のResponseクラスのオブジェクトに対して’core.response’イベントによるfilter()が行われます。この’core.response’イベントのリスナーには、標準でEsiListener::filter()とResponseListener::filter()、えバッグ中はProfilerListener::handleResponse()の3つが登録されています。

app_dev.php [@/web/app_dev.php]

イベントリスナーによって処理されたResponseオブジェクトは、最終的にapp_dev.phpで呼び出したAppKernel::handle()の返り値として返ってきます。そしてResponse::send()メソッドが呼ばれ、Httpレスポンスがechoされて、ブラウザに表示されます。

まとめ

HttpKernelに入ってからイベントが呼ばれてちょっとややこしいので、大雑把にシーケンス図を描いてみました。文章と照らし合わせてみてみてください。

PDF版はこちら : sf2_http_kernel_sequence_diagram

一応、だいぶはしょりながらですが、今回の記事までで、フロントコントローラからHTTPレスポンスの出力までの一連の流れを解説しました。Controllerがrenderで何やってるかとか、細かいイベントリスナーの挙動とか残ってるんですが、次回以降に説明したいと思います。次回のお題は未定ですw

About: uechoco