[Symfony2]Symfony2 Deep Tour 2

前回は、フロントコントローラが起動して、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の命名規則を調べていない現段階では迷宮入りしそうです。もう少し学習してから続きを書きます。ではまたー

About: uechoco