ZF2のEventManagerの特徴
EventManagerとSharedEventManager
zf2でのEventManagerはイベント供給元オブジェクトにつき一つのスコープとして使います。(もちろん、自由なので複数のオブジェクトを跨ぐことも、オブジェクト以外のスコープで作成することもできますが)
EventManager
Mvc上ではじめに現れるのは、ApplicationスコープでApplicationオブジェクトに付随したEventManagerとMvcEvent。また、個々のコントローラーでも、それぞれEventManagerとMvcEventが割り当てられます。そのため、イベントの名前空間という考え方はなく、対象オブジェクトがスコープを代表します。
では、アプリケーションの任意の場所でそれらにリスナーを登録するにはどうするかですが、
- 直接オブジェクトを取得して登録する。
- アプリケーションオブジェクトを取得して、そのEventManagerにattachする
- コントローラーを取得して、そのEventManagerにattachする
ApplicationやControllerなどを既知なオブジェクトであれば、直接attachすることができます。たとえば、Module.phpはデフォルトでonBootstrapメソッドが呼ばれますので、
<?php public function onBootstrap(MvcEvent $e) { $EventManager = $e->getApplication()->getEventManager(); $EventManager->attach('route', array($this, 'onRoute')); $EventManager->attach(MvcEvent::EVENT_DISPATCH_ERROR, array($this, 'onError'), -1); }
こういう書き方で、自身をリスナーとして登録することが可能です。
SharedEventManagerとインターフェース単位のイベント
Mvcフローでレスポンスにフィルターを加えたいとか、ルーティングに介入して別の値を付与したいとか、サービスを実行する前に権限チェックを加えたい、など、アプリケーションには共通の処理があります。zf1ではこれらを実装するとき、プラグインを利用していました。
EventManagerが各オブジェクトにスコープが絞られるため個々のオブジェクトを知らないと横断的リスナー(zf1でのプラグイン)を登録することができません。そこで、zf2ではSharedEventManagerを使います。
SharedEventManagerは、イベントをグローバルにシェアします。オフにもできますし、一定のスコープを与えることも可能ですが、通常はグローバルに使います。
個々のEventManagerをイベント対応オブジェクトに登録するとき、Identifierを登録します。その際、イベントのキーになるインターフェース名やクラス名をIDとして複数登録します。複数登録することで、リスナーに自由度を与えます。このIDをグローバルにシェアするのが、SharedEventManagerという風に理解していいと思います。
下記はAbstractControllerが自EventManagerを初期化するソース
<?php $events->setIdentifiers(array( 'Zend\Stdlib\DispatchableInterface', __CLASS__, get_called_class(), $this->eventIdentifier, substr(get_called_class(), 0, strpos(get_called_class(), '\\')) )); $this->events = $events; $this->attachDefaultListeners();
遅延静的束縛で、[インターフェース名, Abstract名, クラス名, 名前空間]をIdentifierとして登録しています。これにより、SharedEventManagerを経由して目的に応じたリスナー登録が可能になっています。
Zend\Mvc\View\Http\ViewManagerでコントローラーのdispatchイベントを-100のプライオリティでリッスンするというソースが下記です。直接コントローラーオブジェクトやコントローラーに付属するEventManagerを知る必要はなく、コントローラーの振る舞いを定義したインターフェースのイベントをリッスンしているわけです。
<?php $sharedEvents->attach('Zend\Stdlib\DispatchableInterface' , MvcEvent::EVENT_DISPATCH , array($injectViewModelListener, 'injectViewModel') , -100);
インターフェース名、クラス名をIDとして使うメリットは二つあります。
- クラス名、インターフェース名はグローバルに一意であり、名前の衝突が起きない。
- 横断的関心事はそもそもオブジェクトに対してではなく、インターフェースメソッド(もしくはクラスメソッド)に対する関心事であるため、意味が明白。
アプリケーション上の都合でも、設計上の意味的にもSharedEventManagerは自然です。
たとえば「ログリスナーがentityInterfaceを実装したオブジェクトのsaveイベントをリッスンしてロギングする」という、意味的に直結したイベント管理ができることになります。
- entityInterfaceのsaveイベント
- adminEntity のsaveイベント
adminEntity自身はsaveイベントをトリガーするだけで、グローバルなリスナー、個別リスナーのそれぞれにイベントを送ることができるわけです。リスナーにはグローバルにentityをリッスンするのか、adminEntityだけをリッスンするのかを選択できる柔軟性が生まれます。
EventManagerはオブジェクト単位で直接的、SharedEventManagerはグローバルで横断的なイベント管理を行うことができます。
trait ProvidesEventsが示す模範例
インターフェースベースのイベント管理と言っても、実際にイベントを発生させる側がそれに従ってくれなければ問題を生じます。trait ProvidesEventsでは、イベント発生側の模範例を示してくれています。
https://github.com/zendframework/zf2/blob/master/library/Zend/EventManager/ProvidesEvents.php
このソース中でIdentifierにはインターフェース名、クラス名相当のものを使うという模範が示されています。メンバ変数でIdentifierを追加することもできるので、PHP5.4↑な環境では、ProvidesEventsを使い、そうでない場合は、ProvidesEvents相当のソースを組み込むのがよいでしょう。
zf2推奨のトリガーとプライオリティ
zf1でもそうですが、特定のメソッドの前後でイベントをトリガーし、前後処理を行うパターンがあります。
zf2での推奨パターンとしては、前後をイベントで挟むのではなく、対象メソッドをイベントリスナーとしてプライオリティ1として登録し、trigger経由で実行します。他のリスナーは1以上ならpre、それ以下ならpostという具合に優先度をつけてイベントループ中に配置します。
zf2でよくあるイベント呼び出しのパターンとしては、主に2つあり、
- イベント経由で他のオブジェクトに処理を依頼する。
- 自オブジェクトのメソッド実行をイベント対応にする。
1の方は、呼び出し元はイベントをトリガーするだけで、主に処理をするオブジェクトをリスナーとして登録、リスナーが処理の主体になります。たとえば、ApplicationオブジェクトはgetRouter()->route()で直接依頼するのではなく、EVENT_ROUTEをトリガーしてRouteListenerに任せます。
2の方は、自オブジェクトをイベント対応にし、振る舞いの変更を可能にします。たとえば、fooメソッドをイベント対応にするとき、fooメソッド内ではイベントをトリガーし、onFooをプライオリティ1としてとしてリッスンします。
メソッドの振る舞いを根本的に変更したいとします。前後処理パターンの場合、メソッド側で変更可能性を考慮して注意深く実装する必要がありますが、1のパターンならリスナーを選択することで完全に処理を変更できます。
2のパターンなら、デフォルトのリスナーとして登録されたメソッドをdetachして代わりのリスナーを登録してしまうことができます。(tips:プライオリティ1のうち、最初に取得できるのがデフォルトのリスナーです。)文字通りメソッドの振る舞いを交換可能です。実際にonFooをdetachすることはないかもしれませんが、そこまで可能な仕組みになっています。また、detachせず、上位プライオリティを与えて条件次第で、fooメソッドを呼ぶ前に正常終了する方法もあります。
これらにより、メソッドの振る舞いを変更するために細かなクラス継承を行ったり、イベントフックを意識したメソッド実装を行う必要がなくなります。Zend\Mvcの各所でこのパターンで実装されているので、zf2でモジュールを作成する際にもこのパターンに従っておくとシェアしやすいと思います。
イベント対応にしたメソッドはリスナーと並列的扱えることで従来のフィルタ的な動作も、動作モデルを変更することも可能となっています。
イベントフロー、もうひとつの終了方法
zf2でのイベントをトリガーする方法のひとつとして、下記のようなものがあります。
<?php $result = $this->getEventManager()->trigger(MvcEvent::EVENT_DISPATCH, $e, function($test) { return ($test instanceof Response); });
コントローラーのアクションを呼び出すが、もし、リスナーが返す結果がレスポンスオブジェクトならイベントループを終了します。
これはイベントループの型の推移をわかりやすくしています。
リスナーの実装はイベントオブジェクトを与えられ、型Aまたは型Bを返す。型Aならループを継続し、型Bならループをただちに正常終了して型Bを結果とする。最後のリスナーまで到達したとき、最後に渡された型Aをイベントループの結果とする。
異常終了の際は、stopPropergation()をセットし、中断フラグをイベントオブジェクトにセットした上でレスポンス型Eで返す。通常Eはエラー内容を示す。
これにこだわる必要こそありませんが、このパターンは各種実装でも見られるクリーンなイベントループパターンなので、単にイベントを呼ぶ、呼ばれる形で介入するのではなくリスナーの振る舞いを決めるとよいですね。
これらは、zf1でのテンプレートメソッドパターンに乗ったイベントフローではショートカットして正常終了させる場合に事前に呼ぶ側で機構を準備する必要があったという反省に基づいているのかもしれません。stopPropergationによるイベントループの中断とは本質的に異なります。
zf2のEventManagerが冗長に見えるか、スマートに見えるか、それは見方次第ですが、ユースケースを想像しながらインスピレーションが沸いて楽しいと私は最近思います。