[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コードを載せておきます。
-
protected function getRouterService()
-
{
-
-
$class = 'Symfony\\Component\\Routing\\Router';
-
$instance = new $class(
-
$this->getRouting_LoaderService(),
-
'/mnt/hgfs/localweb02/symfony-sandbox/app/config/routing_dev.yml',
-
'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;
-
}
ちなみに、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()の返り値
-
array(2) { ["_controller"]=> string(72) "Symfony\Bundle\FrameworkBundle\Controller\DefaultController::indexAction" ["_route"]=> string(8) "homepage" }
// /app_dev.php/hello/UechocoのRouter::match()の返り値
-
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
