ZendFramework2ガイド

機能編

イベント

Zend2のMVCはイベント駆動

Zend2のMVCはイベントという機構のもとに成り立っています。
イベントといっても、javascriptのような、ユーザーの入力に反応するようなイベント駆動ということではありません。MVCの一連の処理フローを、処理タイミングでいくつかに分類したものをイベントと呼んでいるようなイメージです。たとえば、アプリケーション起動直後の初期処理であるbootstrapイベント、リクエストURLからコントローラーを振り分けるrouteイベント、コントローラーを起動してアクションを実行するdispatchイベントなどです。
それぞれのイベントに対してはあらかじめ、そのタイミングで行う必要のある処理をメソッドとして登録しておきます。
そしてアプリケーションの軸となるメイン処理クラスが存在し、そのメイン処理クラスがイベントを順番に発火させていくわけです。これにより、個々の処理同士は直接依存しておらず、メイン処理クラスに全てがぶら下がるようなイメージで、Zend2の重要な思想の一つである疎結合を実現する一つの仕組みがイベントとも言えます。

イベントの種類

まず、イベントには何があるか見てみましょう。
MVCのイベントとしては主に以下の5つです。

Zend2のMVCの柱となるメインクラスがZend\Mvc\Applicationです。
上で紹介したような各イベントに対してはいろいろなところで処理がメソッドとして登録されていきます。そしてApplicationクラスから順番に各イベントが発火されていき、発火されたイベントに登録された処理メソッドが呼び出されます。

イベントを管理するクラスがZend\EventManager\EventManagerです。このクラスにイベント処理を登録していく感じです。
スケルトンアプリケーションなどをもとに基本的な実装をおこなっている限り、このクラス自体はあまり意識することはありません。イベントという概念自体、知らなくてもZend2でのアプリ開発は可能です。しかし、これを知り、ある程度使うことができれば、できることの幅は格段に広がってきます。

イベントへの処理の登録

イベントへの処理の登録方法を見てみましょう。
イベントへの処理の登録は、上で挙げているようなイベントのどれかにメソッドを登録するようなイメージです。で、あるイベントに対して処理を登録するということは、当然そのイベントが発火されるより前のタイミングで登録処理が必要ということになります。なので、イベントの前後関係は重要になります。基本的に全ての処理はイベントの発火がトリガーとなり、動作します。例えばdispatchイベントに何かの処理を登録するのであれば、bootstrap、またはrouteのイベントで呼び出されるメソッドのどこかで登録する必要があります。

最もお手軽にイベントの登録が出来そうなのは、ModuleクラスのonBootstrapメソッド内です。これはbootstrapイベント時に呼び出されるメソッドの一つで、bootstrapといえば最も最初に発火されるイベントであるため、それ以降のイベントへの処理登録には適していると言えます。

EventManagerから直接呼び出されるメソッドには必ず引数としてZend\Mvc\MvcEventクラスのインスタンスが渡ってきます。このクラスにはMVCアプリケーションとしての情報がいろいろと詰まっていますが、ここからEventManagerのインスタンスも得ることが出来ます。まずはEventManagerのインスタンスの取得方法から見てみましょう。

module/Hoge/Module.php
namespace Application;

use Zend\Mvc\ModuleRouteListener;
use Zend\Mvc\MvcEvent;

class Module
{
    public function onBootstrap(MvcEvent $e)
    {
        $eventManager        = $e->getApplication()->getEventManager();
        $moduleRouteListener = new ModuleRouteListener();
        $moduleRouteListener->attach($eventManager);
    }

スケルトンアプリケーションのModule.phpには最初からこのようなコードが入っています。
見れば分かる通り、実は最初からEventManagerのインスタンスを取得するコードが1行目にあります。今のところは2行目以降は気にしないで下さい。で、このEventManagerのインスタンスに対してイベント処理を登録していきます。

module/Hoge/Module.php
    public function onBootstrap(MvcEvent $e)
    {
        $eventManager        = $e->getApplication()->getEventManager();
        $moduleRouteListener = new ModuleRouteListener();
        $moduleRouteListener->attach($eventManager);

        $eventManager->attach(MvcEvent::EVENT_DISPATCH, array($this, 'onDispatch'));   // 追加
    }

    // 追加
    public function onDispatch(MvcEvent $e)
    {

    }

EventManagerのattachメソッドで登録します。
1番目の引数は登録先のイベント名です。イベント名はMvcEventクラスに定数として定義されているので、これを利用します。
2番目の引数は登録する処理です。クラスメソッドの場合は例のように配列でインスタンスとメソッド名を指定します。
これだけです。

そして呼び出すメソッドを実際に作れば、登録したイベントのタイミングで呼び出されることになります。
イベントは呼び出される順番がありますが、それでは同じイベントに登録された復数の処理はどういう順番で呼び出されるでしょうか。例えば今回登録したdispatchイベントは、コントローラークラスのインスタンスが生成され、アクションメソッドが呼び出されるイベントです。ここに処理を追加したことになりますが、アクションメソッドより前?後ろ?どちらでしょうか。試してみましょう。

Module.phpに作成したonDisptachメソッドと、コントローラーのアクションメソッド、2つのdispatchイベントで呼び出されるはずのメソッドの中で、自身のメソッド名を出力してみます。

module/Hoge/Module.php
    public function onBootstrap(MvcEvent $e)
    {
        $eventManager        = $e->getApplication()->getEventManager();
        $moduleRouteListener = new ModuleRouteListener();
        $moduleRouteListener->attach($eventManager);

        $eventManager->attach(MvcEvent::EVENT_DISPATCH, array($this, 'onDispatch'));
    }

    public function onDispatch(MvcEvent $e)
    {
        var_dump(__METHOD__);
    }
module/Hoge/src/Hoge/Controller/IndexController.php
class IndexController extends AbstractActionController
{
    public function indexAction()
    {
        var_dump(__METHOD__);
    }
}
string(42) "Hoge\Controller\IndexController::indexAction"
string(21) "Hoge\Module::onDispatch"

登録したonDispatchの方が後になりました。
コントローラーのアクションメソッドを呼び出す処理をdispatchイベントに登録する処理は、アプリケーション起動直後で、ModuleクラスのonDispatchメソッドが呼び出されるよりも前になります。なので単純に考えて後から追加したのだから後になるというのは不思議なことではありませんし、実際にそういうことです。
ではアクションメソッドより前のタイミングで呼び出される処理を挟むことは出来ないのでしょうか?dispatchよりも前のrouteイベントに登録するという手もありますが、イベントへの登録は、その処理がどのイベントで行われるべきかという基準で考えるべきです。routeイベントはリクエストURLをもとに振り分け先のコントローラー・アクションを特定するイベントです。なのでそれに関連する処理はrouteイベントに登録するべきだし、そうではなくて、今回のようにアクションメソッドの前処理としてアクションメソッドの直前に実行させたい処理があるとしたら、意味合い的にはdispatchイベントに登録すべきとなります。

ではどうするかですが、実は同じイベント内に登録された処理は実行の優先度というものが存在します。上の例ではattachでの登録の際に優先度を示すような情報は何も指定していませんが、実は3番目の引数が優先度の指定になります。

module/Hoge/Module.php
    public function onBootstrap(MvcEvent $e)
    {
        $eventManager        = $e->getApplication()->getEventManager();
        $moduleRouteListener = new ModuleRouteListener();
        $moduleRouteListener->attach($eventManager);

        $eventManager->attach(MvcEvent::EVENT_DISPATCH, array($this, 'onDispatch'), 2);   // 優先度2で指定
    }

    public function onDispatch(MvcEvent $e)
    {
        var_dump(__METHOD__);
    }

優先度は数値で指定し、数値が大きいほど優先度が高くなります。
先ほどのように優先度の指定を省略したらどうなるか。その場合は優先度1での登録となります。
コントローラーのアクションメソッドを呼び出す処理は優先度1で登録されています。先ほどの例では優先度の指定なしで結局1で登録されているわけですから、優先度1が2つ登録された事になりますが、この場合は先に登録されたものほど優先されます。
優先度1で登録されているアクションメソッドを呼び出す処理よりも先に呼び出したい処理の場合には優先度として1よりも大きい数値を指定すれば良いことになります。例えば上の例のように2で登録します。この状態で実行結果を見てみましょう。

string(21) "Hoge\Module::onDispatch"
string(42) "Hoge\Controller\IndexController::indexAction"

希望通りの順番に入れ替わっています。

このような感じでイベントの順序と、同じイベント内の処理順序を意識して登録していけば、任意のタイミングで任意の処理を実行させる事が可能なわけです。
例えばコントローラーの種類に関わらず、アクションメソッドの直前、直後に必ず呼び出される処理がある場合、イベントへの登録で対応することが出来ます。

module/Hoge/Module.php
    public function onBootstrap(MvcEvent $e)
    {
        $eventManager        = $e->getApplication()->getEventManager();
        $moduleRouteListener = new ModuleRouteListener();
        $moduleRouteListener->attach($eventManager);

        $eventManager->attach(MvcEvent::EVENT_DISPATCH, array($this, 'preDispatch'), 2);
        $eventManager->attach(MvcEvent::EVENT_DISPATCH, array($this, 'postDispatch'), 0);
    }

    // アクションメソッド前処理
    public function preDispatch(MvcEvent $e)
    {
        var_dump(__METHOD__);
    }

    // アクションメソッド後処理
    public function postDispatch(MvcEvent $e)
    {
        var_dump(__METHOD__);
    }
string(22) "Ec\Module::preDispatch"
string(42) "Ec\Controller\IndexController::indexAction"
string(23) "Ec\Module::postDispatch"

イベントリスナを利用したイベントの登録

EventManagerのattachメソッドによって直接EventManagerへ処理を登録する方法もありますが、Moduleクラスにいろいろな処理がごちゃまぜになり、あまり綺麗ではありません。例えば上の例のような、コントローラーのアクションメソッドの前後処理メソッドがModuleクラスにかかれているのもなんか変な感じがします。

もう一つのイベント登録の仕組みとしてイベントリスナを利用した登録方法があります。
Zend2におけるイベントリスナとは、イベントに登録する処理を定義したクラスの事です。大雑把に言ってしまえば、メソッドをちまちま登録するのではなく、一つ、または復数のメソッドを定義したクラスをまるごと登録してしまうようなイメージです。とはいっても実際にはクラスの中に定義されたメソッドが登録されるわけですが。

イベントリスナとして機能させるクラスは、Zend\EventManager\AbstractListenerAggregateという抽象クラスを継承する必要があります。

module/Hoge/src/Hoge/Controller/ControllerCommonListener.php
use Zend\EventManager\AbstractListenerAggregate;
use Zend\EventManager\EventManagerInterface;
use Zend\Mvc\MvcEvent;

class ControllerCommonListener extends AbstractListenerAggregate
{
    public function attach(EventManagerInterface $events)
    {
    }
}

AbstractListenerAggregateを継承すると、attachとメソッドの実装が強制されますので、最低限、例のような状態である必要があります。

今回作成したのは、dispatchイベントでコントローラーのアクションメソッドの前後で共通的に行う処理を登録するためのイベントリスナということで、ControllerCommonListenerという名前にしました。
まず、このクラスの中に、イベントに登録するメソッドを作成します。その上で、attachメソッド内でそのメソッドをEventManagerに登録する1行を書きます。attachメソッドの引数にEventManagerのインスタンスが来るので、後は上で紹介した、EventManagerのattachメソッドで同様に登録するだけです。

module/Hoge/src/Hoge/Controller/ControllerCommonListener.php
use Zend\EventManager\AbstractListenerAggregate;
use Zend\EventManager\EventManagerInterface;
use Zend\Mvc\MvcEvent;

class ControllerCommonListener implements AbstractListenerAggregate
{	
    public function attach(EventManagerInterface $events)
    {
        $events->attach(MvcEvent::EVENT_DISPATCH, array($this, 'preDispatch'), 2);
        $events->attach(MvcEvent::EVENT_DISPATCH, array($this, 'postDispatch'), 0);
    }

    public function preDispatch(MvcEvent $e)
    {
        var_dump(__METHOD__);
    }

    public function postDispatch(MvcEvent $e)
    {
        var_dump(__METHOD__);
    }
}

今回はdispatchの前処理として処理を登録したいために、dispatchの前後処理ということでpreDispatchとpostDispatchというメソッドを作成し、これをそれぞれdispatchイベントの優先2と0で登録しています。
detachメソッドはお決まりで例のように書くものと思って下さい。コピペでかまいません。

この状態ではただクラスを作成しただけで何も起こりません。これをイベントに登録するには、やはりEventManagerのattachメソッドです。

module/Hoge/Module.php
use Zend\Mvc\ModuleRouteListener;
use Zend\Mvc\MvcEvent;
use Hoge\Controller\ControllerCommonListener;

class Module
{
    public function onBootstrap(MvcEvent $e)
    {
        $eventManager        = $e->getApplication()->getEventManager();
        $moduleRouteListener = new ModuleRouteListener();
        $moduleRouteListener->attach($eventManager);

        // 追加
        $commonListener = new ControllerCommonListener();
        $eventManager->attach($commonListener);
    }

ModuleクラスのonBootstrapでイベントリスナの登録を行います。
まずは作成したイベントリスナクラスのインスタンスを生成します。そしてEventManagerのattachメソッドの1番目の引数にそのインスタンスを指定して実行します。
上で紹介している、メソッドの個別登録と同じEventManagerのattachで登録していますが、引数の渡し方が違います。EventManagerのattachメソッドにイベントリスナクラスのインスタンスを渡すと、内部でイベントリスナクラスのattachメソッドが呼び出され、実行されます。その結果、イベントリスナクラスのメソッドがEventManagerに登録されることになります。

この例ではコントローラーのアクションメソッドの前後処理ということで、クラス名をControllerCommonListenerにし、名前空間もControllerとして、クラスファイルもコントローラーディレクトリに置くことも可能になるので、コントローラーの共通処理を行うクラスであるという事がすぐにわかるし、置き場所としても適切でわかりやすいと言えます。
このように、任意の名前のクラスを任意の名前空間に置くことが可能なため、処理の種類ごとにイベントリスナを分類して作成し、名前空間も適切にすることで、Moduleクラスにやたらといろいろな種類のメソッドが乱立することを防ぐ事もできるし、全体に整理されるとも言えると思います。

また、必要な処理はイベントへの登録によって動作させることにより、クラス同士の依存度を下げることにも貢献していると言えます。
というのは、これまでは、例えばコントローラーの前後共通処理を行いたいとなった場合、コントローラーの親クラスに前後処理メソッドを作成し、アクションを呼び出す前後で固定的にフレームワークから呼び出させるという手法がありました。しかしこの場合、前後処理のために継承を利用することで、その親クラスと継承クラスは切っても切れない関係になっています。クラス定義としても、extends {クラス名}と固定的に記述してしまい、その共通処理が必要ない場合でもを切り離すことはそうそう簡単なことではありません。

そこでイベントという仕組みを利用すると、アクションメソッドを実行する処理や、その前後処理など、全て含めて個々にEventManagerというクラスに処理リストとして管理されているイメージとなり、それぞれの処理どうしは直接の関わりがありません。そのため、ある処理が一時的に必要ない場合、EventManager登録している部分のただ1行をコメントアウトするだけで対応が出来てしまいます。この仕組は理解してしまうと、従来の全てが絡みついているような構造と比較すると非常にシンプルで軽快なイメージとなり、変更にも強いといえるのです。

で、そのようなメリットを更にレベルアップさせるイベントリスナの登録方法があります。
上記ではModule.phpなどでattachする処理が発生しています。処理が増えるほど、このattachする行が増えていくわけですが、一切これが必要なくなる方法があります。
それは、サービスマネージャーを組み合わせる方法です。

module/Board/config/module.config.php
return array(

    'service_manager' => array(
        'invokables' => array(
            'ControllerCommonListener' => 'Hoge\Controller\ControllerCommonListener',
        ),
    ),

    'listeners' => array(
        'ControllerCommonListener'
    ),

やることはmodule.config.phpに上記のように設定を入れるだけです。
イベントリスナをServiceManagerに自動的に登録されるように'service_manager'の'invokables'に指定します。このへんの詳しいことはサービスマネージャーのところの解説を見て下さい。この状態では$serviceManager->get('ControllerCommonListener')でインスタンスが取得できるに過ぎませんが、'listeners'にこのサービス名を指定するのがミソです。これで、起動直後に自動的にEventManagerに登録されます。こうするとModuleクラスなどでのEventManagerへの登録処理すら必要なくなります。

イベントリスナの機能を一時的に切りたい場合はmodule.config.phpのlistenersの該当箇所を1行コメントにするだけです。