アシアルブログ

アシアルの中の人が技術と想いのたけをつづるブログです

<Symfony Componentsシリーズ(1)> オブジェクトをつなぐEvent Dispatcher

こんにちは。小川です。

先日Symfony 2のアプリケーション構成を読むという記事で、Symfony 2の大まかなアプリケーションのディレクトリ構成と、KernelやBundleという存在について書きました。Symfony 2を語る上でSymfony Componentsの存在はかかせません。本日は一挙2本立て!Symfony Componentsの中でも特に重要になるEvent DispatcherコンポーネントとRequest Handlerコンポーネントをご紹介します。今回ご紹介するのはEvent Dispatcherコンポーネントです。

ちなみに本日2つ目の記事はこちらです
<Symfony Componentsシリーズ> Symfony 2の秘密兵器: Request Handler

Symfony Components

はじめに、コンポーネントについてご存じない方のためにコンポーネントについて説明します。symfony 1系はオールインワンのフレームワークでした。その中から単体でも使える機能、symfony 1.4の中からいくつか挙げると、Yamlパーサー/ダンパー、ルーティング、フォームなどといったものを独立させたものがコンポーネントです。現在、GitHubにて開発されているSymfonyのブランチの中には、次のようなコンポーネントが含まれています。

* Console
- コマンドライン・アプリケーション
* DependencyInjection
- DIコンテナ
* EventDispatcher
- Observerパターンの実装
* OutputEscaper
- 出力の自動エスケープ
* RequestHandler
- RequestからResponseまでの流れを統括
* Routing
- ルーティングの制御
* Templating
-フレキシブルなテンプレートエンジン
* Yaml
-YAMLパーサー/ダンパー

その他にも、FormやI18N、Fileなどが今後追加されると思われます。ただ、基本的には単体で動作するものの、一部のコンポーネントは他のコンポーネントを利用する場合があります。Dependency InjectionやRoutingにて設定ファイルをYAML形式で利用する場合はYAMLコンポーネントが必要になります。次の記事で紹介するRequest HandlerはEvent Dispatcherが必須になります。
また、クラスのオートロードを前提とした作りになっています。PHPの名前空間と主要フレームワークの対応についてという記事で大まかな名前空間のルールを定めてるお話をしましたが、それに準拠したオートロードの設定が必要です。もしないのであれば、Symfony\Foundation\UniversalClassLoaderクラスを利用するといいでしょう。

これらのコンポーネントはそれぞれが非常に有用なものになっています。SymfonyがログやキャッシュにZend Frameworkを用いるように、Symfony以外のところで、Symfonyの超便利機能をつまみ食いできるようになっていますので、ぜひぜひ活用してほしいなあと思います。

Symfonyの開発者であるFabien氏のプレゼン資料も公開されていますので、そちらもぜひ参考にしてみてください。


◆ Event Dispatcher

Event Dispatcherはsymfony 1.2から実装された機能です。内容は現在の1.4に入ってるものも、2.0で搭載されるものも同じです。というのも、このEvent Dispatcherは2つのクラスのみで構成されます。詳細は後述します。

みなさんJavaScriptはご存知ですか?例えば次のようなコードがあるとしましょう。



<!-- (1) -->
<input type="button" id="some_button" value="click me!" />

<script type="text/javascript">
//<![CDATA[

  // (2)
  function myListener(event) {
    alert('Event [' + event.type + '] has happened!'
      + ' Button#' + event.target.id + ' was clicked.');
  }

  // (3)
  document.getElementById('some_button').onclick = myListener;

//]]>
</script>


内容としては、(1)で定義したボタンをクリックすると、ポップアップが出てくる、というものです。この仕組みがわかるのであれば、Event Dispatcherもすぐわかることでしょう。
流れを説明しましょう。まず(1)です。これはただのinput要素です。idはsome_buttonになります。次に(2)と(3)です。(2)でmyListener()という関数を定義しています。これを(3)の部分で、idがsome_buttonの要素がクリックされたときに呼び出されるよう設定します。この場合、クリックをイベント、イベントに対して登録したmyListenerをイベントリスナーと呼びます。
(3)で、関数そのものをonclick属性に登録しています。こうすることによって、clickされたときに自動的にmyListenerが呼び出され、引数としてeventオブジェクトが渡されます。
JavaScriptでいうイベントとは、クリックやキーボード操作、ウィンドウの読み込みなど様々なものが定義されており、このeventオブジェクトには、何の操作が行われたか、どの要素に対して行われたか、マウスの座標はどこか、キーボードのどのキーが押されたのかなど、イベントによって様々なプロパティが定義されます。

ここまでの仕組みをわかっていれば、Event Dispatcherもすぐ理解できるでしょう。Event Dispatcherには以下の2つのクラスが定義されています。

* Event: (2)でmyListenerに渡している引数eventに当たる
* EventDispatcher: イベントリスナーの管理やイベントの通知を行う、JavaScriptに当たる

どのように使われるかみてみましょう。



<?php
use Symfony\Components\EventDispatcher\Event;
use Symfony\Components\EventDispatcher\EventDispatcher;

class Button
{
  private $id;
  private $dispatcher;

  public function __construct(EventDispatcher $dispatcher, $id)
  {
    $this->dispatcher = $dispatcher;
    $this->id = $id;
  }

  public function getId()
  {
    return $this->id;
  }

  public function click()
  {
    // notify button.click
    $this->dispatcher->notify(new Event($this, 'button.click', array('foo' => 'bar')));
  }
}

// (2)
function myListener(Event $event)
{
  echo sprintf("Event [%s] has happend!", $event->getName());

  $button = $event->getSubject();
  echo sprintf(" Button#%s was clicked.", $button->getId());

  // custom property
  $foo = $event['foo']; // alias: $event->getParameter('foo')
  echo sprintf("(foo: %s)", $foo);
}

$dispatcher = new EventDispatcher();

// (1)
$button = new Button($dispatcher, 'some_button');

$button->click();
# nothing happens...

// (3)
$dispatcher->connect('button.click', 'myListener');

$button->click();
# => Event [button.click] has happend! Button#some_button was clicked.(foo: bar)


クラスを書いたりしてたので長くなりました。先ほどのJavaScriptと極力同じようなものにしてみました。Buttonというクラスはボタン要素を表すイメージです。コンストラクタでEventDispatcherを受け取るようにしています。
次にmyListener()関数を定義しています。これはJavaScriptの(2)と同じです。

(1)ではButtonオブジェクトを生成しています。第2引数にボタンのIDを指定しています。先ほどと同じsome_buttonにします。
この直後にclick()メソッドを呼び出しています。click()メソッドの内部で、 $this->dispatcher->notify(new Event($this, 'button.click')) としていますが、これはbutton.clickというイベントが発生したことをEventDispatcherに通知(notify)するためのものです。Eventオブジェクトを作る際、第1引数が通知元となるオブジェクト(サブジェクト)、第2引数がイベント名です。イベント名は通常.(ドット)で区切って名前空間を持たせます。第3引数は必須ではありませんが、好きなパラメータを連想配列で渡すことが可能です。

その後、(3)で先ほど作ったEventDispatcherオブジェクトからconnect()というメソッドを実行しています。button.clickというイベントが実行されたらイベントリスナーとしてmyListener()関数を呼び出すための設定です。(3)でイベントリスナーの登録を行った後、再度click()メソッドを実行すると、またbutton.clickというイベントが発生します。今度はイベントリスナーとしてmyListener()が定義されていますので、myListener()が実行され、メッセージが出力されます。イベントリスナーの内部で引数$eventに対してgetName()とやるとイベント名の取得が、getSubject()メソッドを実行すると呼び出し元のオブジェクトが取得可能です。また配列形式でアクセスすると任意のパラメータが取得可能です。


なんとなくEvent Dispatcherの仕組みがわかりましたか?注目してほしいのは、Buttonクラスの定義は一切変えずに、リスナー(myFunction()関数)を登録する前と後で処理の内容が変わるところです。
通常Symfonyではフレームワーク全体で1つのEventDispatcherオブジェクトを共有しています。そしてSymfonyの内部の様々なところでイベントの通知が行われています。
イベントを定義することで、要するにクラスの定義を変えずに処理を外部から組み込むことが可能になります。つまり、SymfonyにはSymfonyそのものを拡張するポイントがいくつも設置されています。

通知方法は先ほどのnotify()も含めて3種類あります。

* notify($event)
- 登録されている全てのリスナーに順番に通知
* notifyUntil($event)
- 登録されているリスナーに順番に通知するが、リスナーがtrueを返した場合は通知を中断する
* filter($event, $value)
- 登録されているリスナーに順番に$valueを渡して値のフィルタリングを行う

Eventには戻り値の設定が可能です。例えばsymfony 1.4には、request.method_not_foundというイベントが定義されています。このイベントはRequestの__call()メソッドから通知されます。__call()は定義されていないメソッド呼び出しによって呼び出されるものです。request.method_not_foundイベントに対して、呼び出されたメソッド名に応じて特定の処理をして返すリスナーを定義しておけば、つまりはクラス定義をそのままに、メソッドの追加が可能になるわけです。この場合、メソッド名に対する処理が済んでおり後続のリスナーに通知する必要がないので、通知方法はnotifyUntilが用いられています。
method_not_foundというイベントはこの他にも様々なところで定義されています。sfComponent(sfActionの親クラス)にはcomponent.method_not_found、responseにはresponse.method_not_foundが定義されています。

filterは特定の値をフィルタリングするためのものです。例えばresponse.filter_contentイベントが挙げられます。このイベントではレスポンスとして返すHTMLなどの内容に対してフィルタリングをかけることが可能です。実際にsymfony 1.4ではこのイベントを利用して行っていることがあります。symfonyユーザーおなじみのWebデバッグツールバーをHTMLに組み込む処理です。この他だと、Formにbind()したときに、バインドした値をフィルタリングするためのform.filter_valuesなどがあります。

Event Dispatcherを利用することにより、継承を使わずに様々な拡張が可能です。
symfonyのドキュメントにイベントの一覧が用意されていますので、ぜひ1度見てみるといいかと思います。

このEvent Dispatcherの仕組みを知ることで、symfonyでできることがぐっと広がると思います。


では次に、Request Handlerコンポーネントをみていきましょう!
<Symfony Componentsシリーズ(2)> Symfony 2の秘密兵器: Request Handler