ZendFramework2ガイド

解析編

起動~アクション実行・その3(dispatch)

ルーティング情報をもとにアクションメソッドを実行

起動~アクション実行・その2」の続きです。
その2では、routeイベント関連の処理を見ていきましたが、このrouteイベントで、リクエストURLからコントローラークラス、アクションメソッドが決定しましたが、この時点では決定しただけで、まだ実行はされていません。それを行うのがdispatchイベントになります。以下、dispatchイベントまわりの処理を見ていきます。

dispatchイベント

dispatchイベントの発火

Applicationクラスのrunメソッドで発火されたrouteイベントが終了後、すぐにdispatchイベントが発火されます。

vendor/ZF2/library/Zend/Mvc/Application.php
    public function run()
    {
            ・
            ・
            ・
        $result = $events->trigger(MvcEvent::EVENT_ROUTE, $event, $shortCircuit);  // routeイベント発火
        if ($result->stopped()) {
            $response = $result->last();
            if ($response instanceof ResponseInterface) {
                $event->setTarget($this);
                $event->setResponse($response);
                $events->trigger(MvcEvent::EVENT_FINISH, $event);
                $this->response = $response;
                return $this;
            }
        }
        $result = $events->trigger(MvcEvent::EVENT_DISPATCH, $event, $shortCircuit);  // dispatchイベント発火

            ・
            ・
            ・
    }

この時点でdispatchイベントに登録されている処理は以下です。

たくさんありますね。しかし、3つ目以降はアクションメソッド後のビュー関連の処理ばかりです。
アクションメソッドの実行に係るのは最初の2つだけです。

まずはZend\Mvc\DispatchListener::onDispatchを見てみましょう。

vendor/ZF2/library/Zend/Mvc/DispatchListener.php
namespace Zend\Mvc;

class DispatchListener implements ListenerAggregateInterface
{

    public function onDispatch(MvcEvent $e)
    {
        $routeMatch       = $e->getRouteMatch();
        $controllerName   = $routeMatch->getParam('controller', 'not-found');
        $application      = $e->getApplication();
        $events           = $application->getEventManager();
        $controllerLoader = $application->getServiceManager()->get('ControllerManager');

        if (!$controllerLoader->has($controllerName)) {
            $return = $this->marshalControllerNotFoundEvent($application::ERROR_CONTROLLER_NOT_FOUND, $controllerName, $e, $application);
            return $this->complete($return, $e);
        }
		
        try {
            $controller = $controllerLoader->get($controllerName);
        } catch (InvalidControllerException $exception) {
            $return = $this->marshalControllerNotFoundEvent($application::ERROR_CONTROLLER_INVALID, $controllerName, $e, $application, $exception);
            return $this->complete($return, $e);
        } catch (\Exception $exception) {
            $return = $this->marshalBadControllerEvent($controllerName, $e, $application, $exception);
            return $this->complete($return, $e);
        }

        $request  = $e->getRequest();
        $response = $application->getResponse();

        if ($controller instanceof InjectApplicationEventInterface) {
            $controller->setEvent($e);
        }
		
        try {
            $return = $controller->dispatch($request, $response);
        } catch (\Exception $ex) {
            $e->setError($application::ERROR_EXCEPTION)
                  ->setController($controllerName)
                  ->setControllerClass(get_class($controller))
                  ->setParam('exception', $ex);
            $results = $events->trigger(MvcEvent::EVENT_DISPATCH_ERROR, $e);
            $return = $results->last();
            if (! $return) {
                $return = $e->getResult();
            }
        }

        return $this->complete($return, $e);
    }

順番に見て行きましょう。まずは最初の部分です。

    public function onDispatch(MvcEvent $e)
    {
        $routeMatch       = $e->getRouteMatch();
        $controllerName   = $routeMatch->getParam('controller', 'not-found');
        $application      = $e->getApplication();
        $events           = $application->getEventManager();
        $controllerLoader = $application->getServiceManager()->get('ControllerManager');
                ・
                ・
                ・

まず、RouteMatchのインスタンスからコントローラー名を取得しています。RouteMatchはrouteイベントでのルーティング処理で生成されたもので、ルーティング先のコントローラーやアクション等の情報を保持しています。
そしてServiceManagerを通してZend\Mvc\Controller\ControllerManagerクラスのインスタンスを取得します。
ControllerManagerのインスタンスが生成される過程を見てみましょう。

ControllerManagerのサービス定義は、Zend\Mvc\Service\ServiceListenerFactoryにあります。

vendor/ZF2/library/Zend/Mvc/Service/ServiceListenerFactory.php
namespace Zend\Mvc\Service;

class ServiceListenerFactory implements FactoryInterface
{

    protected $defaultServiceConfig = array(
        'factories' => array(
            'ControllerLoader'               => 'Zend\Mvc\Service\ControllerLoaderFactory',
        ),
        'aliases' => array(
            'ControllerManager'                      => 'ControllerLoader'
        ),

ControllerManagerのインスタンスはZend\Mvc\Service\ControllerLoaderFactoryによって生成されることがわかります。
この設定では、ControllerLoaderFactoryは'ControllerLoader'というサービスとして設定されていますが、ControllerLoaderのエイリアスとして'ControllerManager'というサービス名が設定されています。なので実際はget('ControllerLoader')でもget('ControllerManager')でも、どちらでもControllerManagerのインスタンスが得られます。

ControllerLoaderFactoryを見てみましょう。

vendor/ZF2/library/Zend/Mvc/Service/ControllerLoaderFactory.php
namespace Zend\Mvc\Service;

class ControllerLoaderFactory implements FactoryInterface
{
    public function createService(ServiceLocatorInterface $serviceLocator)
    {
        $controllerLoader = new ControllerManager();
        $controllerLoader->setServiceLocator($serviceLocator);
        $controllerLoader->addPeeringServiceManager($serviceLocator);

        $config = $serviceLocator->get('Config');

        if (isset($config['di']) && isset($config['di']['allowed_controllers']) && $serviceLocator->has('Di')) {
            $controllerLoader->addAbstractFactory($serviceLocator->get('DiStrictAbstractServiceFactory'));
        }

        return $controllerLoader;
    }
}

特に変わったことはなく、単にControllerManagerをnewしているだけです。そしてServiceManagerのインスタンスをセットしています。
後半のdiがどうのという部分は今回は触れません。DIという仕組みを利用した場合、ここを通りますが、普通はあまり気にする必要はありません。

そして生成されたインスタンスを返します。

DispatchListener::onDispatchに戻り、続きの処理を見て行きましょう。

vendor/ZF2/library/Zend/Mvc/DispatchListener.php
    public function onDispatch(MvcEvent $e)
    {
                ・
                ・
                ・
        if (!$controllerLoader->has($controllerName)) {
            $return = $this->marshalControllerNotFoundEvent($application::ERROR_CONTROLLER_NOT_FOUND, $controllerName, $e, $application);
            return $this->complete($return, $e);
        }
		
        try {
            $controller = $controllerLoader->get($controllerName);
        } catch (InvalidControllerException $exception) {
            $return = $this->marshalControllerNotFoundEvent($application::ERROR_CONTROLLER_INVALID, $controllerName, $e, $application, $exception);
            return $this->complete($return, $e);
        } catch (\Exception $exception) {
            $return = $this->marshalBadControllerEvent($controllerName, $e, $application, $exception);
            return $this->complete($return, $e);
        }
                ・
                ・
                ・

ControllerManagerのインスタンスは$controllerLoaderに格納され、以下の処理が進みます。

if (!$controllerLoader->has($controllerName))

の部分は、routeイベントで決定したコントローラー名に合致するコントローラークラスがサービスとして登録されているかをチェックしています。これは、module.config.phpの'controllers'にサービス名の定義がされているかどうかということです。実際に該当のコントローラークラスが定義されているかどうかではありません。ここで定義がない場合には404 NotFoundとして処理されます。

次に、ControllerManagerからコントローラーのインスタンスを取得します。

$controller = $controllerLoader->get($controllerName);

ControllerManagerも以下のとおり、もとを正せばServiceManagerを継承しています。

namespace Zend\Mvc\Controller;

class ControllerManager extends AbstractPluginManager
{

---------------------------------------------------------------------------------

namespace Zend\ServiceManager;

abstract class AbstractPluginManager extends ServiceManager implements ServiceLocatorAwareInterface
{

そのため、getメソッドはServiceManagerのメンバですし、基本的にはServiceManagerと同じ仕組でインスタンスを生成していることになります。ServiceManagerはあらかじめ設定されたサービス名と実際のクラス名のマッピング配列をもとにインスタンスを生成します。ということは、ControllerManagerもこの仕組にならい、あらかじめマッピングが設定されているはずです。それこそが、module.config.phpで設定している'controllers'の設定です。この設定がどのタイミングでControllerManagerにセットされているのかはここでは詳しくは追いません(解析編の「モジュール」で詳しく説明しています)。

そしてコントローラーのインスタンスを取得したら、そのdispatchメソッドを実行しています。

    public function onDispatch(MvcEvent $e)
    {
                ・
                ・
                ・
        $request  = $e->getRequest();
        $response = $application->getResponse();

        if ($controller instanceof InjectApplicationEventInterface) {
            $controller->setEvent($e);
        }
		
        try {
            $return = $controller->dispatch($request, $response);
        } catch (\Exception $ex) {
                ・
                ・
                ・

内部dispatchイベントの連鎖的な発火

Zend2でのルールとして、コントローラークラスはAbstractActionControllerを継承するルールになっていますが、dispatchメソッドは更にその継承元であるAbstractController クラスに定義されています。

vendor/ZF2/library/Zend/Mvc/Controller/AbstractController.php
namespace Zend\Mvc\Controller;

abstract class AbstractController implements
    Dispatchable,
    EventManagerAwareInterface,
    InjectApplicationEventInterface,
    ServiceLocatorAwareInterface
{

    public function dispatch(Request $request, Response $response = null)
    {
        $this->request = $request;
        if (!$response) {
            $response = new HttpResponse();
        }
        $this->response = $response;

        $e = $this->getEvent();
        $e->setRequest($request)
          ->setResponse($response)
          ->setTarget($this);

        $result = $this->getEventManager()->trigger(MvcEvent::EVENT_DISPATCH, $e, function ($test) {
            return ($test instanceof Response);
        });

        if ($result->stopped()) {
            return $result->last();
        }

        return $e->getResult();
    }

ここではEventManagerを使ってdispatchイベントを発火しています。
ここで、あれ?と思われるかもしれません。dispatchイベントの発火は既にApplication::runメソッド内で行なわれ、そのイベント処理の最中のはずです。dispatchイベント処理の中で更にdispatchイベントを発火するという、よくわからないことになっています。どういうことでしょうか。

よくよく追いかけてみると、AbstractControllerクラスの持つEventManagerは、Applicationクラスの持つEventManagerとは別インスタンスであることがわかります。コントローラーのインスタンスが生成されてからここまで、外側からEventManagerのインスタンスをセットしている箇所はありません。で、AbstractControllerのgetEventManagerメソッドを見てみると、

    public function getEventManager()
    {
        if (!$this->events) {
            $this->setEventManager(new EventManager());
        }

        return $this->events;
    }

という感じで、EventManagerを新たにnewしています。
イベントと、それにアタッチされている処理を管理しているのはEventManagerですが、同じEventManagerでもインスタンスが別であればイベントの管理は別です。構造的には、Applicationの持つEventManagerで発火されたdispatchによって実行された処理の中で、別のEventManagerのインスタンスを生成し、そのEventManagerに対して更にdispatchイベントの処理を追加、そして発火という事が行なわれているわけです。

ちなみにdispatchメソッドの中ではいきなりEventManagerのtriggerメソッドでdispatchイベントが発火されていますが、処理のattachはどこで行なわれているでしょう。
それはsetEventManagerメソッドを実行した瞬間になります。上の例の通り、getEventManagerの中でEventManagerのインスタンスを生成し、それを一旦、setEventManagerメソッドで自分自身にセットしています。そのsetEventManagerメソッドを見てみましょう。

    public function setEventManager(EventManagerInterface $events)
    {
        $events->setIdentifiers(array(
            'Zend\Stdlib\DispatchableInterface',
            __CLASS__,
            get_class($this),
            $this->eventIdentifier,
            substr(get_class($this), 0, strpos(get_class($this), '\\'))
        ));
        $this->events = $events;
        $this->attachDefaultListeners();

        return $this;
    }

    protected function attachDefaultListeners()
    {
        $events = $this->getEventManager();
        $events->attach(MvcEvent::EVENT_DISPATCH, array($this, 'onDispatch'));
    }

setEventManagerメソッドの最後にattachDefaultListenersというメソッドが呼び出されています。
そしてここでdispatchイベントへ処理をattachしています。
attachしているのは自身のonDispatchメソッドです。

ということでdispatchメソッドへ戻り、triggerによりdispatchイベントが発火されると、onDispatchメソッドが実行されるというわけです。
onDispatchはAbstractControllerクラスとしては抽象メソッドとなっており、AbstractControllerを継承するAbstractActionControllerで実装されています。

vendor/ZF2/library/Zend/Mvc/Controller/AbstractActionController.php
namespace Zend\Mvc\Controller;

abstract class AbstractActionController extends AbstractController
{

   public function onDispatch(MvcEvent $e)
    {
        $routeMatch = $e->getRouteMatch();
        if (!$routeMatch) {
            throw new Exception\DomainException('Missing route matches; unsure how to retrieve action');
        }

        $action = $routeMatch->getParam('action', 'not-found');
        $method = static::getMethodFromAction($action);

        if (!method_exists($this, $method)) {
            $method = 'notFoundAction';
        }

        $actionResponse = $this->$method();

        $e->setResult($actionResponse);

        return $actionResponse;
    }

MvcEventのインスタンスからルーティング結果情報を保持するRouteMatchのインスタンスを取得し、そこからアクション名を取得しています。
そしてそのアクション名から、フレームワークのルールに則ったアクションメソッド名に変換しているのがgetMethodFromActionメソッドです。そしてアクションメソッドを実行しているのが、

$actionResponse = $this->$method();

この部分です。
この直前の部分では、もしアクションメソッドがコントローラークラスに定義されていなければ、404 Not Foundとして処理していることがわかります。

多くの場合、アクションメソッドの戻り値はViewModelのインスタンスになります。
アクションメソッドの戻り値はMvcEventに対してsetResultメソッドによりアクションメソッドの実行結果として格納され、同時にアクションメソッドの戻り値をreturnしてonDispatchメソッドは終了です。

AbstractControllerのdispatchメソッドに戻ると、最終的にはonDispatchでMvcEvent::setResultでセットしたものをgetResultで取得し、そのままreturnしています。

処理はDispatchListenerのonDispatchに戻ります。

    public function onDispatch(MvcEvent $e)
    {
                ・
                ・
                ・
        try {
            $return = $controller->dispatch($request, $response);
        } catch (\Exception $ex) {
            $e->setError($application::ERROR_EXCEPTION)
                  ->setController($controllerName)
                  ->setControllerClass(get_class($controller))
                  ->setParam('exception', $ex);
            $results = $events->trigger(MvcEvent::EVENT_DISPATCH_ERROR, $e);
            $return = $results->last();
            if (! $return) {
                $return = $e->getResult();
            }
        }

        return $this->complete($return, $e);
    }

    protected function complete($return, MvcEvent $event)
    {
        if (!is_object($return)) {
            if (ArrayUtils::hasStringKeys($return)) {
                $return = new ArrayObject($return, ArrayObject::ARRAY_AS_PROPS);
            }
        }
        $event->setResult($return);
        return $return;
    }

コントローラークラスのdispatchメソッドの戻り値として$returnにはアクションメソッドの戻り値が入っていることになります。多くの場合はViewModelのインスタンスです。
そしてそれを引数に、DispatchListener自身のcompleteメソッドを実行し、その結果をそのままreturnしています。
ここの処理はあまり重要ではないですが、引数で受け取ったものがobject型ではなく、配列であったら、ArrayObjectに変換しています。

ここまででdispatchイベントの処理は終了です。
あとはその結果をもとにビューの処理に入っていきます。ビューに関しては「ビューその1」「ビューその2」で説明していますので、そちらも見てみて下さい。