ZendFramework2ガイド

解析編

イベントマネージャ

EventManagerクラスの役割と存在意義

Zend2のMVCはイベント駆動型です。

アプリケーションは小さな処理の集まりと見ることが出来ます。オブジェクト指向の場合は特にそうです。クラスがあり、メソッドがあります。そしてそれらの小さな処理には流れがあり、順番があります。
普通に考えればそれらの必要な処理があるべき順番に呼び出されて処理が流れていきます。
しかしZend2では少し考え方が異なります。必要な処理を直接呼び出すことはしません。直接やることはいくつかのイベントを順番に発火していくことだけです。そして各イベントにはそのタイミングで必要な処理がそのイベント内でまた順番付けられて登録されています。
これら前者と後者の大きな違いは、処理を順番に呼び出す側のクラスが、フレームワークの動作に必要な処理を直接呼び出しているか、いないかの違いです。
前者は呼び出す側のクラスが呼び出される側のクラスのインスタンスを生成して直接メソッドを実行します。
それに対し、後者の場合は呼び出す側のクラスがやっていることは、イベントを発火するだけのことです。処理に必要なクラスの名前も知らなければどんなメソッドが存在するかも知りません。

このような、呼び出す側と呼び出される側が直接依存しない状態を疎結合と呼びますが、Zend2の重視している思想がこの疎結合なのです。
疎結合を実現する一つの手法としてイベント駆動型が取り入れられていると言えるでしょう。

ではイベントを発火するというのは具体的にはどういうことなのか。
イベントといっても、UI操作連動型のイベントではありません。サーバーサイドのプログラムなので当然です。
クライアントサイドのJavascriptはUI操作駆動型です。この場合のイベントとはクリックイベントとか、フォーカス取得イベントとか言うような、ユーザーの操作タイミングのイベントです。
ではZend2でいうイベント駆動とはどういうことかというと、上では「呼び出す側」と表現している、アプリケーションを統括するクラスにより呼び出される特定のタイミングのイベントです。呼び出すタイミングは呼び出す側のクラスにプログラミングされているわけですから、毎回同じタイミングです。UI操作連動型とは意味が異なります。言ってみれば、全体処理を大きく分類したものをイベントとよんでいるような感じです。そしてそれらの処理分類は呼び出されるべき順番があるので、それぞれの分類を順番に呼び出す、というのがZend2のイベント駆動という感じです。

で、それらのイベントを管理するのがEventManagerというクラスです。
EventManagerはイベントのリストを持っており、更にそれぞれのイベントには復数のメソッドや関数のリストを持たせることが出来ます。
上で「呼び出す側」とか「アプリケーションを統括する」と表現しているのは実際にはApplicationというクラスですが、このApplicationというクラスがEventManagerを持っていて、そのEventManagerに対して特定のイベントの実行を指示する。そのようにして各イベントを決められた順番で呼び出していくわけです。
そしてEventManagerは、イベントの実行指示があると、そのイベントに割り当てられたメソッドや関数を、これまた順番通りに呼び出していく。
という感じで処理が進んでいくわけです。

この仕組みにより、呼び出す側と呼び出される側の仲介となっているのがEventManagerということになります。疎結合実現に大きな貢献をする重要なクラスです。
そういう意味で、EventManagerはZend2の思想の核となるクラスといえるのです。

イベントへの処理の登録(EventManager::attach)

EventManagerも、まずはイベントに対して処理を登録しないことには何の役にも立ちません。
処理を登録するのはEventManagerのattachメソッドです。

vendor/ZF2/library/Zend/EventManager/EventManager.php
namespace Zend\EventManager;

class EventManager implements EventManagerInterface
{

    public function attach($event, $callback = null, $priority = 1)
    {
        if ($event instanceof ListenerAggregateInterface) {
            return $this->attachAggregate($event, $callback);
        }

        if (null === $callback) {
            throw new Exception\InvalidArgumentException(sprintf(
                '%s: expects a callback; none provided',
                __METHOD__
            ));
        }

        if (is_array($event)) {
            $listeners = array();
            foreach ($event as $name) {
                $listeners[] = $this->attach($name, $callback, $priority);
            }
            return $listeners;
        }

        if (empty($this->events[$event])) {
            $this->events[$event] = new PriorityQueue();
        }

        $listener = new CallbackHandler($callback, array('event' => $event, 'priority' => $priority));

        $this->events[$event]->insert($listener, $priority);
        return $listener;
    }

このメソッドの処理内容から分かるポイントがいくつかあります。

まず、$eventsというフィールドに複数のイベントを管理しており、一つ一つのイベントには複数の処理が保持されるということ。

次に、$eventsに直接格納されているのはZend\Stdlib\PriorityQueueのインスタンスであるということ。
PriorityQueueはその名の通り、優先順位付けのある処理をキュー管理するクラスです。キューといえば先入れ先出しのデータ構造のことですが、これはその中でも優先度付きキューというやつで、この構造としての機能をそのままクラスとして実現したのがこのPriorityQueueクラスです。EventManagerでは、attachメソッドで指定された関数やメソッドをPriorityQueueに追加していくわけですが、その実行順序は引数で指定された値をもとに設定されます。

次に、PriorityQueue内で管理されている関数やメソッドなどの処理の一つ一つはPriorityQueueに直接格納されるのではなく、Zend\Stdlib\CallbackHandlerというクラスのインスタンスを生成し、その中に内包する形でPriorityQueueに格納されます。

その他に読み取れることは、引数で指定するイベント名は配列でもOKということです。
中程にある、 if (is_array($event)) の部分で配列の場合の処理をしていますが、
この場合、配列で指定された複数のイベント名に対して、同じ処理を登録できるということになります。普通はこういう登録の仕方はあまりないと思いますが。

あとはメソッドの最初の部分を見ると、引数の$eventがListenerAggregateInterfaceのインスタンスであった場合にはattachAggregateメソッドに処理を託しているのがわかります。通常は$eventには文字列でイベント名を指定するわけですが、ListenerAggregateInterfaceを実装したクラスもOKということになるわけです。
attachAggregateメソッドを見てみましょう。

    public function attachAggregate(ListenerAggregateInterface $aggregate, $priority = 1)
    {
        return $aggregate->attach($this, $priority);
    }

引数で渡ってきたListenerAggregateInterfaceを実装したクラスのインスタンスのattachメソッドを実行しているだけです。

namespace Zend\Mvc;

class RouteListener implements ListenerAggregateInterface
{
    protected $listeners = array();

    public function attach(EventManagerInterface $events)
    {
        $this->listeners[] = $events->attach(MvcEvent::EVENT_ROUTE, array($this, 'onRoute'));
    }

ルーティング処理の起点となるRouteListenerクラスです。ListenerAggregateInterfaceを実装しており、attachメソッドではEventManagerのインスタンスに対してattachメソッドでイベント名指定でメソッド登録しています。

ややこしいですが、EventManagerのattachメソッドにRouteListenerのインスタンスを渡した場合、EventManagerのattachメソッド内では最終的にRouteListenerのattachメソッドを呼び出しており、そのRouteListenerのattachメソッド内ではEventManagerのattachメソッドにイベント名指定でメソッドを登録している。
ぐるりと一周回って結局イベント名指定でEventManagerのattachメソッドに登録されることになるわけです。

イベントの発火(EventManager::trigger)

attachメソッドによって特定のイベントに登録された処理群を実行させるのがtriggerメソッドです。

namespace Zend\EventManager;

class EventManager implements EventManagerInterface
{

    public function trigger($event, $target = null, $argv = array(), $callback = null)
    {
        if ($event instanceof EventInterface) {
            $e        = $event;
            $event    = $e->getName();
            $callback = $target;
        } elseif ($target instanceof EventInterface) {
            $e = $target;
            $e->setName($event);
            $callback = $argv;
        } elseif ($argv instanceof EventInterface) {
            $e = $argv;
            $e->setName($event);
            $e->setTarget($target);
        } else {
            $e = new $this->eventClass();
            $e->setName($event);
            $e->setTarget($target);
            $e->setParams($argv);
        }

        if ($callback && !is_callable($callback)) {
            throw new Exception\InvalidCallbackException('Invalid callback provided');
        }

        $e->stopPropagation(false);

        return $this->triggerListeners($event, $e, $callback);
    }

triggerメソッドではtriggerListenersメソッドに処理を託しているだけです。
前半の条件分岐は、様々な引数パターンに対応するためのものです。
以下の4パターンに対応しているのがわかります。

パターン1
 引数1:EventInterfaceを実装するクラスのインスタンス
 引数2:コールバック関数

パターン2
 引数1:イベント名(文字列)
 引数2:EventInterfaceを実装するクラスのインスタンス
 引数3:コールバック関数

パターン3
 引数1:イベント名(文字列)
 引数2:ターゲットクラス名
 引数3:EventInterfaceを実装するクラスのインスタンス
 引数4:コールバック関数

パターン4
 引数1:イベント名(文字列)
 引数2:ターゲットクラス名
 引数3:パラメーター配列
 引数4:コールバック関数

最も標準的な感じなのはパターン2で3番目の引数未指定という実行の仕方です。
Applicationクラスで生成されるEventManagerでの各イベントの発火時もだいたいこれです。
例えばbootstrapイベントの発火を見てみましょう。

$events->trigger(MvcEvent::EVENT_BOOTSTRAP, $event);

2番目の引数で渡すEventのインスタンスは、triggerによって呼び出されるメソッドの引数に渡されてきます。
上のbootstrapイベントを発火するtriggerではMvcEventのインスタンスが渡されています。MvcEventについては後ほど詳しく見ていきます。

で、triggerメソッドの実質的な処理はtriggerListenersメソッドに書かれているわけですので、triggerListenersメソッドの方を見て行きましょう。

class EventManager implements EventManagerInterface
{

    protected function triggerListeners($event, EventInterface $e, $callback = null)
    {
        $responses = new ResponseCollection;
        $listeners = $this->getListeners($event);
		
        $sharedListeners         = $this->getSharedListeners($event);
        $sharedWildcardListeners = $this->getSharedListeners('*');
        $wildcardListeners       = $this->getListeners('*');
        if (count($sharedListeners) || count($sharedWildcardListeners) || count($wildcardListeners)) {
            $listeners = clone $listeners;

            $this->insertListeners($listeners, $sharedListeners);

            $this->insertListeners($listeners, $sharedWildcardListeners);

            $this->insertListeners($listeners, $wildcardListeners);
        }
		
        foreach ($listeners as $listener) {
            $listenerCallback = $listener->getCallback();
			
            $responses->push(call_user_func($listenerCallback, $e));

            if ($e->propagationIsStopped()) {
                $responses->setStopped(true);
                break;
            }

            if ($callback && call_user_func($callback, $responses->last())) {
                $responses->setStopped(true);
                break;
            }
        }

        return $responses;
    }

最初にResponseCollectionのインスタンスを生成していますが、この後のイベント処理の処理結果をここに格納し、最終的にこれをreturnします。呼び出し側では戻り値であるResponseCollectionのインスタンスから処理結果を取り出すことができるわけです。

次にgetListenersメソッドを実行していますが、これは指定のイベントに登録された処理のリストを取得します。
メソッドの処理内容をみてみましょう。

    public function getListeners($event)
    {
        if (!array_key_exists($event, $this->events)) {
            return new PriorityQueue();
        }
        return $this->events[$event];
    }

単純に$this->events[$event]をreturnしているだけです。
上の方で解説しているattachメソッドを見てもらえれば分かる通り、$this->events[$event]にはPriorityQueueのインスタンスが入っており、このPriorityQueueのインスタンスの中に、attachメソッドで登録された複数のメソッドやら関数やらがリスト管理されています。

triggerListenersメソッドにもどって、
メインの処理は後半部分なので前半部分は一旦無視して後で見ます。

triggerListenersの後半を見てみましょう。

    protected function triggerListeners($event, EventInterface $e, $callback = null)
    {
        $responses = new ResponseCollection;
        $listeners = $this->getListeners($event);

                ・
                ・
                ・
		
        foreach ($listeners as $listener) {
            $listenerCallback = $listener->getCallback();
			
            $responses->push(call_user_func($listenerCallback, $e));

            if ($e->propagationIsStopped()) {
                $responses->setStopped(true);
                break;
            }

            if ($callback && call_user_func($callback, $responses->last())) {
                $responses->setStopped(true);
                break;
            }
        }

IteratorAggregateを実装していると、foreachにそのままかけて順次処理が可能になります。
PriorityQueueの場合、foreachで得られる要素は何かというと、CallbackHandlerのインスタンスです。上で解説しているattachメソッドを見てもらえればわかります。

foreach内の最初の行で、

$listenerCallback = $listener->getCallback();

とやっていますが、$listenerがCallbackHandlerのインスタンスになるわけです。
そしてCallbackHandlerに内包する形でメソッドや関数が保持されているわけですが、このCallbackHandlerのgetCallbackメソッドにより、その内包されたメソッドまたは関数が取り出せます。
なので、$listenerCallbackは登録されているメソッドまたは関数そのものになります。

そして次の行でこのメソッドまたは関数をcall_user_funcによって実行し、その戻り値をそのままResponseCollectionのインスタンスにpushしています。

その後の、

            if ($e->propagationIsStopped()) {
                $responses->setStopped(true);
                break;
            }

の部分ですが、まずifの条件となっている$e->propagationIsStopped()が何なのか。
$eは引数で渡ってきた、Eventまたはそれを継承するクラスのインスタンスで、propagationIsStoppedは単にこのインスタンスが持つフラグを返すクラスです。このフラグは、$e->stopPropagation()メソッドを実行する事でtrueがセットされます。$eはcall_user_funcで呼び出すメソッドや関数を実行する際の2番目の引数に渡されており、これはつまり呼び出されるメソッドや関数の引数として渡されることになります。そしてそこでその渡ってきたEventのインスタンスのstopPropagationメソッドを実行すると、その直後の、

if ($e->propagationIsStopped())

の条件に引っかかる事になります。引っかかると、ResponseCollectionのインスタンスのsetStoppedにtrueをセットし、break、つまり復数登録されているイベント処理のそれ以降の処理を行なわずにforeachを抜けます。つまり、復数の処理が登録されている場合でも、その中のある処理の結果が、それ以降の処理を行う必要がないとなるような場合に、$e->stopPropagation()とすることで以降の処理を行なわせずにすることが可能なわけです。


次の、

            if ($callback && call_user_func($callback, $responses->last())) {
                $responses->setStopped(true);
                break;
            }

の部分ですが、$callbackはtriggerListenersメソッド自体の引数で受けているもので、triggerメソッド自体を呼び出す際に渡された関数です。
例えばApplicationクラスでrouteイベントをtriggerする箇所は以下のようになっています。

vendor/ZF2/library/Zend/Mvc/Application.php
    public function run()
    {
        $events = $this->events;
        $event  = $this->event;

        $shortCircuit = function ($r) use ($event) {
            if ($r instanceof ResponseInterface) {
                return true;
            }
            if ($event->getError()) {
                return true;
            }
            return false;
        };

        $result = $events->trigger(MvcEvent::EVENT_ROUTE, $event, $shortCircuit);

このように例えばクロージャ関数などを渡す事ができ、こうすると、triggerListenersメソッドのforeach内の最後の部分でいちいちこの関数が呼びだされることになります。そして見ての通り、この関数がtrueを返すと、これまたResponseCollectionのインスタンスのsetStoppedにtrueをセットし、breakしています。つまり、そこでイベント処理ループがストップし、以降のイベント処理が行なわれずに終了することになります。このように、イベントループをEventManagerの外側から制御することが可能なわけです。

ループが終了するとResponseCollectionをreturnし、triggerListenersメソッドは終了です。
戻り先はtriggerメソッドですが、triggerListenersの戻り値をそのままreturnしてtriggerも終了します。