「Zend Framework2 beta2 を触ってみる。 (featuring EventManager)」

この記事は"Zend Framework 日めくり Calendar 2012"の「Zend Framework2 beta2 を触ってみる。 (featuring EventManager)」として書きました。

Zend Framework 2.0もbeta2がリリースされ、近くRCそして今春には正式リリースになるそうです。
個人的な印象ですが、ZF1で懸念していた多くの設計上の問題が、ZF2では改善されており、新しいfeatureを追加して別のフレームワークになるのではなく、フレームワークの品質向上に向けた一貫した改善プロセスを踏めているようです。
今日は、ZF1からZF2への変更点の中で、MVCレイヤーにおける変更点とその鍵になるEventManagerに焦点を当て、ZF2の理解に役立つドキュメントやその他リソース、最後にSkeletonアプリを用いたquick startのログをご紹介します。
quick startログへのリンク

ZF1からZF2へ

Zend Frameworkでは、アプリケーションの構造を自由に構成できましたが、推奨のアプリケーション構成もあります。その部分でどのような変化があったかを確認します。
まず、ZF1ですが、FrontControllerパターンでした。FrontControllerから各コントローラーへdispatchループし、モジュール別MVCで具体的な処理を行います。
http://framework.zend.com/manual/ja/learning.quickstart.intro.html
アプリケーションの基本的な処理フローをURL解析、処理、表示という基本的な流れとすると、ZF1ではfrontControllerのdispatchメソッド内で処理フローを決定するテンプレートメソッドパターンと言ってもいいかもしれません。
基本的なフロー(実際Webアプリの大半はこのパターンですが)を踏んでいる分には全く問題ないのですが、実行フローに影響させようとすると、dispatchメソッドそのものを変更せざるをえません。たとえば、routing直後にキャッシュからレスポンスを返すには、dispatchメソッドの変更もしくはオーバーライドが必要でした。
アプリケーションの品質を維持したまま多様性を実現するのに、ZF1.8で追加されたZend_ApplicationによるDIは有効です。しかし、FrontControllerパターンは維持されたため、品質とパフォーマンスのバランスでみるとパフォーマンスが犠牲になったような印象を受けます。
ZF2ではFrontControllerパターンではなく、DIとEventループがアプリケーション構成の基本になります。いずれの方法も言葉単独では違いがわからないのでコードを追いながら、ZF2のEventManagerの振舞いについてみていきたいと思います。

ZF2 MVCの基本的アプリケーション構造とBootstrap

ZF2での基本的な起動スクリプトとしては下記のようなものになります。
http://packages.zendframework.com/docs/latest/manual/en/zend.mvc.html#zend.mvc.intro.bootstrapping-an-application

<?php
// Assuming $config is the merged config from all modules
$bootstrap   = new Bootstrap($config);
$application = new Application();
$bootstrap->bootstrap($application);
$application->run()->send();

ZF2のApplicationクラスはZend\Mvc\AppContextインターフェースを実装したコンテキストとして振舞います。ZF1でFrontController::dispatchで行われてた基本的な処理フローはZF2ではZend\Mvc\Applicationクラスのrunメソッドに集約され、EventManagerが使われます。
自分自身をデフォルトのEventListnerとしてイベントループからrouteメソッドやdispatchメソッド呼ばせています。Applicationの基本的振舞いを変えたければ、Applicationクラス相当のものを作成すればよく、コンテキストとして適切に実装できます。たとえば、社内フレームワークのリプレースを検討している場合や既存のCMS等のクローンを作成するときもそれらのアプリケーションが持っている基本的な処理フローやイベント発生をApplicationクラスとして実装すればよいことになります。
Bootstrapクラスは設定を読み込んで初期設定やインスタンス管理を行います。Applicationコンテキストを作成したら、そのコンテキストに必要な初動を行い(Bootstrap::bootstrap)、コンテキストでアプリケーションを実行します。

<?php
    /**
     * Run the application
     * 
     * @triggers route(MvcEvent)
     *           Routes the request, and sets the RouteMatch object in the event.
     * @triggers dispatch(MvcEvent)
     *           Dispatches a request, using the discovered RouteMatch and 
     *           provided request.
     * @triggers dispatch.error(MvcEvent)
     *           On errors (controller not found, action not supported, etc.), 
     *           populates the event with information about the error type, 
     *           discovered controller, and controller class (if known). 
     *           Typically, a handler should return a populated Response object
     *           that can be returned immediately.
     * @return SendableResponse
     */
    public function run()
    {
        $events = $this->events(); //EventManager instance
        $event  = new MvcEvent();  //implement EventDescription 
        $event->setTarget($this);
        $event->setRequest($this->getRequest())
              ->setRouter($this->getRouter());

        $result = $events->trigger('route', $event, function ($r) {
            return ($r instanceof Response);
        });
        if ($result->stopped()) {
            $response = $result->last();
            return $response;
        }

        $result = $events->trigger('dispatch', $event, function ($r) {
            return ($r instanceof Response);
        });

        $response = $result->last();
        if (!$response instanceof Response) {
            $response = $this->getResponse();
            $event->setResponse($response);
        }

        $events->trigger('finish', $event);

        $response = $response;
        return $response;
    }

EventManager::triggerを見ると数パターンの呼びだし方がありますが、この部分が、EventManagerの基本パターンのうちの一つになります。ここでは、第1引数はイベント名、第2引数はイベントの対象に関する情報を格納するEventDescriptionインスタンス、第3引数はイベントを終了する条件を返すコールバックになります。
Applicationコンテキストでpriority=1でコールバックされるrouteメソッドでは、正常終了するとZend\Mvc\Router\RouteMatchでルート情報を返しますが、routing中にキャッシュを特定出来た場合はここでResponseを返すと、コールバックがtrueとなり、stoppedフラグを立ててイベントループを直ちに終了します。これにより、余分な処理をせずにスマートにキャッシュコンテンツを返すことができます。ZF1に比べるとかなりクールですね。
routing中にエラーが発生すると、dispatch.errorイベントを発生させ、こちらも直ちにエラーコントローラー等でエラー処理することになります。

<?php
        $result = $events->trigger('route', $event, function ($r) {
            return ($r instanceof Response);
        });
        if ($result->stopped()) {
            $response = $result->last();
            return $response;
        }

SPLオブザーバーパターンとEventManager

イベントといえば、オブザーバーパターンも有名ですが、我らがSPLにもSplObserverという、まんまオブザーバーパターン用インターフェースがあります。なぜEventManagerはSPLを使わなかったのか。
http://d.hatena.ne.jp/noopable/20090524/1243120866
こちらでも書きましたが、オブザーバーパターンを素直に実装すると、イベントを監視される側のソースが必要以上に煩雑になってしまうことがあります。SplSubjectの機能部分を監視される側から分離して仲介クラスに任せれば煩雑さは消えるかもしれません。しかしそれだけではなく、Zend\EventManagerはSplインターフェースとは本質的に意図が異なる実装になっているんですね。
処理を中断するスマートかつ汎用的な方法を用意したり、イベントの対象を管理しやすい方法を用意してくれており、アプリケーション全体で統一できるメリットは大きいです。
イベントを処理する他のパターンとZend\EventManagerのコンセプトの違いについては、こちらのZF Leadの方のスライドに詳しく出ているのでもしよかったら見てみてください。
http://www.slideshare.net/weierophinney/zend-framework-20-patterns-tutorial/148

traitとEventManager

EventManagerをtraitで使う例が出ています。
http://www.slideshare.net/weierophinney/zend-framework-20-patterns-tutorial/201
これ、いいですね。EventManagerを使うシチュエーションとしては、バリデーターやフィルター、アクセス制御を適用するケースも考えられますが、たとえばアクセス制御用EventManagerを作成してモデルやコントローラーなどにtraitして使えればうれしいなぁ。そういう展望が立つと、traitが使えない環境でもそれを意識した管理ポリシーが立ちますね。