[Symfony2]Symfony2 Deep Tour 4

Written by uechoco 11月 29
[Symfony2]Symfony2 Deep Tour 4 はコメントを受け付けていません。
この記事を読む時間:1524くらい

前回の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

[Symfony2]第1回Symfony2勉強会に参加しました

Written by uechoco 11月 24
[Symfony2]第1回Symfony2勉強会に参加しました はコメントを受け付けていません。
この記事を読む時間:215くらい

2010/11/20(土)に第1回Symfony2勉強会に参加してきました。会場はジンガジャパン株式会社様です。会場提供ありがとうございます!

Symfony2は現在バージョンPR3、アルファ版すら出ていない鋭意製作中のフレームワークです。それを先取りして勉強しようという勉強会でした。50名が参加しました。Symfonyユーザ会の後藤さんによるワークショップが3つと、LTは ちょびえさん、小川さん が行いました。第1回 Symfony2 勉強会レポートなどにワークショップやLTのスライドが載っています。

勉強会の準備として全員にPHP5.3やMongoDBが動作するPC環境を持参してもらうことになっていたのですが、見た感じ殆どの人が環境を用意してワークショップに臨んでいた気がします。結構みなさんレベルが高いです!

そして、早くも第2回Symfony2勉強会の開催告知もなされました。会場は同じくジンガジャパン株式会社様で、日程は2011/1/15(土)を予定しているようです。後報は日本Symfonyユーザー会のMLをチェックとのことです。

私自身もSymfony2は当然やったことはなかったのですが、せっかく様々なエンジニアの方が参加し、Symfonyのエバンジェリストも参加し、触れ合える機会でしたので、しっかりと予習をしてきました。やっぱり予習していると勉強会の理解度が半端ないです。これはみなさんにもおすすめです。勉強会駆動勉強(Workshop Driven Study)とでも名づけて流行らせたですね。ちなみに予習した内容は3回くらい続けてSymfony2についての記事を書いたのでよかったら見てください。少しペースは落としますが、これからもSymfony2についての記事は書き続けます。

もし、Symfony2に現段階で興味のある方は、日本Symfonyユーザー会のWebサイトを追ったりIRCチャットに参加したりMLを追ったり、あるいは実際にgithubから最新のソースコードをチェックアウトして最新のドキュメントを見つつ勉強してみてください。

[Symfony2]Symfony2 Deep Tour 3

Written by uechoco 11月 20
[Symfony2]Symfony2 Deep Tour 3 はコメントを受け付けていません。
この記事を読む時間:1643くらい

前回のDeep Tour 2では、DIコンテナが生成されてKernelのブートが完了し、HttpKernel::handle()が呼ばれますってところで終わりました。今回はHttpKernel::handle()の説明をしようと思ったのですが、前段階として、EventDispatcherの説明だけをします。HttpKernel::handle()の説明は次回です^^;

イベント駆動プログラミング

Symfony2はイベント駆動型プログラミングされています。Kernelのブートが完了すると、Symfony2は様々なイベントを発生させることでプログラムを進めていきます。

Wikipediaを参考に、イベント駆動型プログラミングの用語を簡単に説明しておきます。

  • イベント:プログラムの流れとは別に発生する事象。GUIプログラミングでは、「キーボードが押された」「ある時刻になった」などがイベントに相当する。Symfony2ではEventクラスがこれを表している。
  • イベントリスナー:イベントが発生したときに実行されるサブルーチン(プログラムの小さな塊、関数など)のこと。一般的にはイベントフックやイベントハンドラなどとも呼ばれる。Symfony2のいくつかのイベントリスナーではXxxxxListner::resolve()やXxxxxListner::filter()という形式のクラスとメソッドをイベントリスナーとして用いるのが一般的(かもしれない)。例えば’core.request’イベントが発生するとRequestListener::resolve()が呼ばれる。
  • イベントディスパッチャ:発生したイベントをイベントハンドラに振り分ける機能。Symfony2ではEventDispatcherクラスが担当する。
  • イベントドリブン:イベント駆動の英語。Event Driven。
  • フロー駆動プログラミング:従来の上から下に流れるようにプログラムが走っていくプログラミングスタイルのこと。イベント駆動プログラミングと比較される。

Symfony2におけるEventDispatcher

Symfony2では、イベントディスパッチャの汎用的な実装としてSymfony\Component\EventDispatcher\EventDispatcherクラスがあります。このクラスはSymfony\Componentの名前空間にあるので、別のSymfony2以外のプログラムにこのEventDispatcherだけコピーして使っても動きます。Symfony2の中核となるFrameworkBundleでは、このEventDispatcherにもう少し機能を付け加えたクラスを新たに定義してイベントディスパッチャとして使用しています。クラス構造としては以下のようになります。

  • 非デバッグ時
    • Symfony\Bundle\FrameworkBundle\EventDispatcherクラスが使用される(自動的にイベントリスナーを登録する機能を付加している)
      • Symfony\Component\EventDispatcher\EventDispatcherクラスを継承している
  • デバッグ時
    • Symfony\Bundle\FrameworkBundle\Debug\EventDispatcherクラスが使用される(デバッグに便利な機能を付加している)
      • Symfony\Bundle\FrameworkBundle\EventDispatcherクラスを継承している
        • Symfony\Component\EventDispatcher\EventDispatcherクラスを継承している

EventDispatcherという名前のクラスが3つも出てきました。Symfony2で「EventDispatcherクラス」という場合、特に断りがなければ基本的にはSymfony\Bundle\FrameworkBundle\EventDispatcherクラスのことを指していると思ってください。
先ほど、「自動的にイベントリスナーを登録する機能を付加している」と書きましたが、EventDispatcher::setContainer()メソッドのことです。これは後で説明します。

自動的に登録されるイベントリスナー

前回のDeep Tour 2では、ContainerBuilderクラスがDIコンテナを生成する際に、自動的にFrameworkExtension::configLoad()メソッドなどを呼び出していることを触れました。一応おさらいしておきましょう。

  • ContainerBuilderクラス:DIコンテナを生成するためのDIコンテナ。登録済みのバンドルの設定を読み込んで、appDevDebugProjectContainerクラスやappProdProjectContainerクラスを生成する。
  • Extension派生クラス:DIコンテナ拡張クラス。各バンドルに複数個定義でき、ContainerBuilderによって自動的に読み込まれる。
  • FrameworkExtension:FrameworkBundleに定義されたExtension派生クラスの1つ。Resources\configディレクトリに含まれている様々なxmlファイルによってHttpKernelクラスやEventDispatcherクラスを使用することを宣言している。

少しだけFrameworkExtension::configLoad()メソッド内で読み込まれるxmlファイルについて深く掘り下げます。configLoad()メソッドで最初に読み込まれるのはweb.xml(/src/vendor/symfony/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml)です。web.xmlでは、8つのクラスをサービス登録(DIコンテナに登録し、インスタンスを生成できるように)しています。このxmlファイルのうち、request_listener・esi_listener・response_listener・exception_listenerのサービス定義に注目すると、これら4つのサービスに「kernel.listener」という共通のタグ付けがされています。以下にrequest_listenerのサービス定義を引用していますが、tagタグがそれに当たります。

  1. <service id="request_listener" class="%request_listener.class%">
  2.             <tag name="kernel.listener" />
  3.             <argument type="service" id="router" />
  4.             <argument type="service" id="logger" on-invalid="ignore" />
  5.         </service>

このようにサービスにタグ名を付けると便利なことがあります。生成されたappDevDebugProjectContainerクラスの後半にあるfindTaggedServiceIds()メソッドを観てみてください。とっても長い配列が定義されていますが、kernel.listenerというキーに対して、上記の4つのサービスID(サービス名)が定義された部分が冒頭に見つかると思います。つまり、DIコンテナに対して、$container->findTaggedServiceIds(‘kernel.listener’);と呼ぶと上記4つのサービス名を一括で取得することが出来るようになります。このメソッド呼び出しはあとで出てきます。

再びFrameworkExtension::configLoad()メソッドの話に戻ります。途中でservices.xmlファイルも読み込んでいることと思います。前回説明したように、このxmlファイルによってEventDispatcherクラスをサービス登録しているのですが、もう1度そのサービス定義を引用して見てみましょう。

  1. <service id="event_dispatcher" class="%event_dispatcher.class%">
  2.             <call method="setContainer">
  3.                 <argument type="service" id="service_container" />
  4.             </call>
  5.         </service>

前回は説明を省略しましたが、setContainer()メソッドをコールしろという定義があるのがわかると思います。これはインスタンスの生成後に呼ばれるメソッド定義です。実際にappDebDebugProjectContainer::getEventDispatcherService()メソッドの定義を見てみると、たしかにインスタンスの生成後にsetContainer()メソッドが呼ばれています。引用してきましょう。

// /app/cache/dev/appDevDebugProjectContainer.php L169-179
[phpcode]
protected function getEventDispatcherService()
{
if (isset($this->shared[‘event_dispatcher’])) return $this->shared[‘event_dispatcher’];

$class = ‘Symfony\\Bundle\\FrameworkBundle\\Debug\\EventDispatcher’;
$instance = new $class($this->get(‘logger’, ContainerInterface::NULL_ON_INVALID_REFERENCE));
$this->shared[‘event_dispatcher’] = $instance;
$instance->setContainer($this);

return $instance;
}
[/phpcode]

つまり、EventDispatcherクラスのインスタンスが生成された直後に、EventDispatcher::setContainer()メソッドが呼ばれています。このメソッドの内部も見ておきましょう。

// /src/vendor/symfony/src/Symfony/Bundle/FrameworkBundle/EventDispatcher.php
[phpcode]
public function setContainer(ContainerInterface $container)
{
foreach ($container->findTaggedServiceIds(‘kernel.listener’) as $id => $attributes) {
$priority = isset($attributes[0][‘priority’]) ? $attributes[0][‘priority’] : 0;

$container->get($id)->register($this, $priority);
}
}
[/phpcode]

おや、先程のindTaggedServiceIds(‘kernel.listener’)がこんなところで出てきました(わざとらしいw)。このメソッド内でやっていることを説明すると、「kernel.listenerというタグ名の付いたサービスの一覧を取得し、それぞれのregister()メソッドを呼ぶ」処理をしています。RequestListener::register()を見てみると、イベントディスパッチャに、’core.request’イベントのイベントリスナーとして登録しているのがわかります。同様に、ResponseListener::regiter()・EsiListener::register()では’core.response’イベントのイベントリスナーとして、ExceptionListener::register()では’core.exception’イベントのイベントリスナーとして登録しているようです。ちなみにこれらのイベントは次回解説するHttpKernelから発生します。

// /src/vendor/symfony/src/Symfony/Bundle/FrameworkBundle/RequestListener.php
[phpcode]
public function register(EventDispatcher $dispatcher, $priority = 0)
{
$dispatcher->connect(‘core.request’, array($this, ‘resolve’), $priority);
}
[/phpcode]

だいぶ説明が長くなりましたが、結論としては、「’kernel.listener’というタグの付いたサービスはEventDispatcherがインスタンス化された直後にイベントリスナーとして登録することができる」わけです。

まとめ:kernel.listenerが登録されるまでのフロー

kernel.listenerが登録されるまでのフローをおさらいしておきましょう。関係ないところは省略しています。

  • フロントコントローラからKernel::handle()メソッドが呼ばれる
  • Kernel::boot()メソッドが呼ばれ、ContainerBuilderによってappDevDebugProjectContainer DIコンテナが生成される
    • Kernelの様々な設定値がセットされる
    • 登録済みの各バンドル内に存在するExtension派生クラスが実行され、各バンドルの設定値が格納される
      • FrameworkExtensionによって、HttpKernelクラスやEventDispatcherクラスのサービスが定義される
      • このとき、web.xmlにて、RequestListener・ResponseLitener・EsiListener・ExceptionListenerにkernel.lisnerというタグが付けられる
  • appDevDebugProjectContainer::getHttpKernelService()メソッドが呼ばれる
  • HttpKernelクラスのインスタンスはまだないので、インスタンスが作成される。この時引数の1つとしてEventDispatcherクラスのインスタンスが必要なため、appDevDebugProjectContainer::getEventDispatcherService()メソッドが呼ばれる
  • EventDispatcherクラスのインスタンスが生成される。
  • EventDispatcher::setContainer()メソッドが呼ばれる
    • kernel.listenerのタグが付いたサービスの一覧を取得し、それぞれのインスタンスのregister()メソッドを呼ぶ
    • 4つのインスタンスのregister()メソッドにてそれぞれ、イベントリスナーが登録される
  • HttpKernel::handle()メソッドが呼ばれる
  • 以下、次回に続く

今回はEventDispatherと初期イベントリスナーについて解説しました。次回はHttpKernelクラスから呼ばれる’core.requst’イベントなどの流れや、イベントによって実行される各イベントリスナーの動作を見ていく予定です。

[CakePHP]ランダム順ページネート

Written by uechoco 11月 19
[CakePHP]ランダム順ページネート はコメントを受け付けていません。
この記事を読む時間:353くらい

最近「このリストはランダム順でページングしてほしい」という要望を何度かもらいました。ランダムなんだから順番なんてないだろう!って思うのですが、サイトのリリース時にはコンテンツが少ないのでランダム順というのは結構見栄えがいいようです。

ただ、プログラムはめんどくさいんです。現在、CakePHP 1.3.3とMySQLを使用していますが、一番簡単なのは
[phpcode]
$this->paginate = array(‘order’=>’RAND()’);
[/phpcode]
として、すべてをMySQLに任せてしまうことです。これは非常に簡単です。コンテンツやユーザ数が少ないうちはパフォーマンスもそれほど気にはなりません(※ORDER BY RAND()は重い処理ということだけは覚えておいてください)。ただ、すぐにクライアントにこう言われてしまいます。「1ページ目と2ページ目に同じものが含まれている」とか「1ページ目に戻ったらさっきと全く違う結果になった」とかです。

一般的な人が思う「ランダム順にページネート」というのは、

  • 別のページからそのページに来たときにランダムが切り替わる
  • ページングしている間は同じページには同じ内容を表示する(1ページ目->2ページ目->1ページ目ともどると、1回目と2回目の1ページ目の内容は同じ)

さて、どうしたものでしょうか。私の場合は

  • RAND()関数に乱数シードを与えることで、同一シードでは同じ結果が返る
  • 乱数シードをページング中にセッションに保持しておけば、結果順を固定できる

ということを利用して、以下のようなプログラムを書きました。

[phpcode]
// environment: CakePHP 1.3.3
function search() {
$this->set(‘title_for_layout’, ‘ランダム ページネーション’);

$page = isset($this->params[‘named’][‘page’]) ? $this->params[‘named’][‘page’] : null;
if ($page !== null) {
$seed = $this->Session->read(‘User.search.seed’);
} else {
$seed = mt_rand();
$this->Session->write(‘User.search.seed’, $seed);
}

$this->paginate = array(‘order’=>’RAND((‘.$seed.’))’, ‘limit’=>10);
$this->set(‘user_list’, $this->paginate(‘User’));
}
[/phpcode]

ページング中(2ページ目を見たり、また1ページ目に戻ったり)であれば、URLに「page:n」が付くことを利用して、その時はセッションから乱数シードを取得しています。逆に「page:n」がない場合は他のページから来たものとみなして、乱数シードを再生成しています。

ちなみに、RAND()関数の括弧が二重になっているのはちょっとしたハックです。実は一重だと、CakePHPが乱数シードの数字を勝手にバッククォートでくくってしまいます。括弧を二重にすることで、ただしいRAND()関数が呼べるようになります。

今のところこれで適当に動いています。もっといい方法があったら教えてください!

[Symfony2]Symfony2 Deep Tour 2

Written by uechoco 11月 19
[Symfony2]Symfony2 Deep Tour 2 はコメントを受け付けていません。
この記事を読む時間:1322くらい

前回は、フロントコントローラが起動して、Kernelが実行されたところまでを書きました。今回はKernelの初期化時にDIコンテナが生成される所までを書こうかと思います。実際のHttpリクエストなどを処理するHttpKernelが実行されるところ以降は次回となります(ここらへんはEventDispatcherフル稼働なのでまだ理解できていません)。

では深く潜っていきましょう!

Kernelのブート [@/src/vendor/symfony/src/Symfony/Component/HttpKernel/Kernel.php]

Kernelクラスのインスタンス$kernelは、そのKernelがブート済みかどうかのフラグ$this->bootedを持っています。基本的にフロントコントローラから呼ばれた最初のKernel::handle()ではまだブートしていないはずなので、Kernel::boot()が呼ばれます。

boot()メソッドの冒頭では、デバッグフラグが立っていない場合だけ、bootstrap.php(/src/vendor/symfony/src/Symfony/Component/HttpKernel/bootstrap.php)をインクルードします。このファイルは幾つかの主要なクラスの定義を1つのファイルにまとめて、コメントなどの不要なものを削ったもののようなのですが、文字通りブートストラップ(ブートするための一連の動作)なのでしょう。このファイル読み込みがデバッグフラグを立っている場合と立っていない場合にどれだけ効果があるのかはよくわかりません。

つぎに、Kernel::initializeContainer()メソッドで、DIコンテナクラスを初期化します。

DIコンテナの初期化 [@/src/vendor/symfony/src/Symfony/Component/HttpKernel/Kernel.php]

DIコンテナは、DI(Dependency Injection:依存性の注入)というデザインパターンにおける様々なオブジェクトを管理するためのクラスです。あるクラスが内部で他のクラスのインスタンスを作ると、そのクラス同士は結合度が高くなってしまいます(密結合な状態)。DIでは、クラス間の依存性をソースコードから取り除くことで、疎結合な状態を作りだすことができます。クラスが疎結合だとどんなメリットがあるかといえば、単体テストのテストコードは書きやすく単純化すると思います。DIを表す別の概念名としてIoC(Inversion of Control:制御の反転)という言葉があります。
※注:まー私がオブジェクト指向の人間ではないので、実際開発のメリットがどれくらい有るのか全く想像できてないです。。。

DIコンテナのクラス名はKernel名+環境名(+デバッグ時はDebug)+ProjectContainerと命名されます。Sandboxの初期設定であれば、appProdProjectContainerクラスと、appDevDebugProjectContainerクラスの2種類が生成される可能性があります。クラスはキャッシュディレクトリに作られます。それぞれ/app/cache/prod/と、/app/cache/devにあると思います。Symfony2では、環境に応じてカスタマイズされたDIコンテナクラスが自動的に作成されます。あと、DIコンテナはContainerクラスまたはそれを継承したクラスがその機能を保持しています。

DIコンテナクラスは、デバッグ時であればKernel::needsReload()メソッドによって再生成する必要があるかを判定していますが、デバッグじゃない時($this->debug === false)は1度DIコンテナクラスが出来てしまうとファイルが意図的に消されるまではクラスは再生成されません。ここでは、ファイルが無かったりリロードが必要だった場合を想定し、DIコンテナクラスが生成されるフローを追っていきます。DIコンテナクラスの生成は、Kernel::buildContainer()メソッドが行います。

Kernel::buildContainer()メソッドでは、ContainerBuilderクラスにKernelの様々なパラメータや登録済みバンドルのExtension派生クラスなどを与えて、ビルダーを作成します。このビルダーもContainerクラスを継承した一種のDIコンテナです(DIコンテナを作るためのDIコンテナ?)。また、AppKernel::registerContainerConfiguration()メソッドに/config/config_dev.ymlファイルをロードするように定義をしているかと思いますが、この中の設定値も読み込みます。あとはこのContainerBuilderに登録した内容をPhpDumperクラスを用いて、appDevDebugProjectContainerというPHPファイルとしてダンプし、所定のファイルに書きこめばDIコンテナが作成されます。一応デバッグ時にはリロード時の判断材料に使われるmeta定義ファイルも同じ場所に同時に生成されます。

この時点でappProdProjectContainerクラス、もしくはappDevDebugProjectContainerクラスのファイルが存在していますので、インクルードし、インスタンスを生成します。Kernel::initializeContainer()メソッドの返り値はこのDIコンテナのインスタンスとなります。

補足議題:Extension派生クラス

ContainerBuilderは、各バンドルのExtension派生クラスも読み込むと先ほど書きました。このExtensionクラスというのはDIコンテナを拡張するためのクラス(意味合いを正確に把握しているわけではないので、後日調べます)です。例えば、FrameworkBundleには、FrameworkExtensionクラスとSecurityExtensionクラスの2つのExtensionが存在します。バンドル内に/DependencyInjectionというフォルダがあり、その中に*Extension.phpというファイルがあれば、それらのExtension派生クラスファイルはすべてContainerBuilderに登録されます。Extension派生クラスには別名(エイリアス)を定義する必要があります。例えば、FrameworkExtensionであれば’app’、TwigExtensionであれば’twig’、DoctrineMongoDBExtensionであれば’doctrine_odm’です。それぞれ、各クラスのgetAlias()メソッドを見ればわかります。これらのエイリアス名を見た覚えはないでしょうか?

QuickTourをやった方はconfig.ymlの設定をいくつかいじったりしたかと思いますが、まさにこのエイリアス名を用いてで設定を書いたはずです。’app.config:’、’twig.config’、’doctrine_odm.mongodb:’ですね。ドットの後の文字はなんでしょうか。それぞれのExtension派生クラスを見てみると、FrameworkExtensionにはconfigLoad()メソッドが、TwigExtensionにはconfigLoad()メソッドが、DoctrineMongoDBExtensionにはmonodbLoad()メソッドが定義されているので、これらのメソッド名と設定値が対応しているのでしょう(どのようにこれらのメソッドが呼ばれるかは後日調べます)。

また、これらの設定は省略しても動くことがあります。YAMLだとチルダで初期設定が適用されるのはsymfony 1.x系ユーザならおなじみです。初期設定が適用されるということは、どこかで初期設定を定義するファイルがあり、それらを読み込んでいる箇所があるということです。FrameworkBundleの中をよく見ると、Resources/configフォルダの中に、幾つものxmlファイルがあり、これらのxmlファイルを前述のconfigLoad()内で読み込んでいます。

Kernelのブートの続き [@/src/vendor/symfony/src/Symfony/Component/HttpKernel/Kernel.php]

再びKernel::boot()メソッド内に戻ってきました。DIコンテナを初期化できたら、ClassCollectionLoader::load()静的メソッドで、コアクラス群をロードします。ファイルの読み込み回数を減らすために1つのファイルに集約されたクラス定義ファイルを作る仕組みがあるんだと思うんですが、私が試した限りでは、キャッシュされたクラス定義ファイルの中身は空っぽでした。いずれまた調査します。

最後に登録済みのバンドル毎にboot()メソッドを持っているので、個々のバンドルもブートしたら、Kernelのブートは完了です。

いきなりHttpKernel::handle()? [@/src/vendor/symfony/src/Symfony/Component/HttpKernel/Kernel.php]

そういえば、Kernel::boot()メソッドは、元々Kernel::handle()から呼ばれていました。だいぶ遠回りしてもどってきました。Kernelのブートが終わった後はDIコンテナからHttpKernelクラスのインスタンスを取得し、HttpKernel::handle()メソッドを呼びます。こんなカンジのソースコードになっています。

[phpcode]
return $this->container->getHttpKernelService()->handle($request, $type, $catch);
[/phpcode]

今まで1度たりとも「HttpKernel」なんてものは出てきてないのに、なぜDIコンテナはHttpKernelのインスタンスを取得できるのでしょうか。一応$this->containerはappDevDebugProjectContainerクラスのインスタンスなので、このDIコンテナの中を見てみると、たしかにgetHttpKernelService()メソッドはあります。おまけにインスタンスがなければその場で作成するようです(Singleton的な感じ)。またHttpKernelクラスのコンストラクタに渡すためのEventDispacherクラスのインスタンスとControllerResolverクラスのインスタンスも同じDIコンテナの中から取得し、インスタンスがなければその場で作成するようです。つまり、ContainerBuilderによってこのDIコンテナクラスが自動的に作られた時点で、HttpKernelやEventDispatcherを使用することを把握し、インスタンスを取得できるようにこれらのメソッドが定義されたことになります。では、ContainerBuilderはいつこれらのクラスを使用することを把握したのでしょうか?少し前に上がったFrameworkExtensionのconfigLoad()メソッド内で、’services.xml’ファイルを読み込んでいますが、このXMLファイルの中でこれらのクラスをserviceとして登録しています。ContainerBuilderは様々なバンドルで登録されたserviceを貯めこんで、PhpDumpするときに対応するメソッドを記述しているようです。

※注:個人的にちょっと不便だなっと思ったのは、HttpKernelもEventDispatcherも生成するときのソースコードはnew $classなんです。その直前に$class変数にクラス名を表す文字列を定義しているのですが、EclipseでCommand(Ctrl)+クリックしてもそのファイルに飛べないんですよね。

次はHttpKernel::handle()メソッドの中を解析する予定ではありますが、パッと見でここから先はイベント駆動でプログラムが実行されていくような感じになるので、EventDispatcherの動きやEventの命名規則を調べていない現段階では迷宮入りしそうです。もう少し学習してから続きを書きます。ではまたー