[Symfony2]Symfony2 Deep Tour 3

前回の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’イベントなどの流れや、イベントによって実行される各イベントリスナーの動作を見ていく予定です。

About: uechoco