ZendFramework2ガイド

解析編

起動~アクション実行・その2(route)

routeイベント

まずはApplication::runメソッドの全体像です。

vendor/ZF2/library/Zend/Mvc/Application.php
class Application implements
    ApplicationInterface,
    EventManagerAwareInterface
{

    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);
        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;
            }
        }

        if ($event->getError()) {
            return $this->completeRequest($event);
        }

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

        $response = $result->last();
        if ($response instanceof ResponseInterface) {
            $event->setTarget($this);
            $event->setResponse($response);
            $events->trigger(MvcEvent::EVENT_FINISH, $event);
            $this->response = $response;
            return $this;
        }

        $response = $this->response;
        $event->setResponse($response);
        $this->completeRequest($event);

        return $this;
    }

順番に見て行きましょう。

    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;
        }

                ・
                ・
                ・

$eventsはEventManagerのインスタンス、$eventはMvcEventのインスタンスです。
最初に無名関数の定義をしています。この無名関数はその後の処理を見れば分かる通り、EventManagerのtriggerメソッドの3番目の引数で渡されています。これはどういうことか。
EventManagerでは、特定のイベントが発火されるとそのイベントに登録された処理が順番に実行されていきます。その一つ一つの処理後にこの3番目で渡された関数が実行されます。そしてこの関数がtrueを返した場合、イベント処理がそこでストップし、以降の処理は行われなくなります。

この無名関数が実行される際、引数としてイベント処理のreturn値が入ってきます。$rの部分ですね。そしてこれがResponseInterfaceを実装したインスタンスの場合、またはMvcEventのインスタンスにエラーがセットされている場合にtrueを返しています。なので結局イベントに登録された処理がResponseInterfaceを実装したインスタンスを返した場合、または、イベントに登録された処理の中で何かエラーが発生し、エラーメッセージがセットされた場合に、イベント処理がストップするようになっているわけです。

routeイベントの発火

まずrouteイベントが発火されます。
この時に、上で説明している無名関数が3番目の引数に渡されています。

    public function run()
    {
                ・
                ・
                ・

       $result = $events->trigger(MvcEvent::EVENT_ROUTE, $event, $shortCircuit);
        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;
            }
        }
                ・
                ・
                ・

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

ルーティング処理

まずはZend\Mvc\RouteListener::onRouteを見てみましょう。
このメソッドはリクエスト情報からコントローラー・アクションへのルーティングを行っています。

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

class RouteListener implements ListenerAggregateInterface
{

    public function onRoute($e)
    {
        $target     = $e->getTarget();
        $request    = $e->getRequest();
        $router     = $e->getRouter();
        $routeMatch = $router->match($request);
		
        if (!$routeMatch instanceof Router\RouteMatch) {
            $e->setError(Application::ERROR_ROUTER_NO_MATCH);

            $results = $target->getEventManager()->trigger(MvcEvent::EVENT_DISPATCH_ERROR, $e);
            if (count($results)) {
                $return  = $results->last();
            } else {
                $return = $e->getParams();
            }
            return $return;
        }

        $e->setRouteMatch($routeMatch);
        return $routeMatch;
    }

4行目の、

$routeMatch = $router->match($request);

の部分が実際にルーティング処理を行っている部分です。$routerは$e->getRouter()で取得していますが、Application::bootstrapの最後の方でMvcEventのインスタンスにRouterのインスタンスをセットしていましたね。それです。しかしそもそも、Application::bootstrapでセットしたRouterのインスタンスってなんだったのでしょうか。
もう一度見返してみると以下のようになっています。

vendor/ZF2/library/Zend/Mvc/Application.php
    public function bootstrap(array $listeners = array())
    {
            ・
            ・
            ・

        $this->event = $event  = new MvcEvent();
        $event->setTarget($this);
        $event->setApplication($this)
              ->setRequest($this->request)
              ->setResponse($this->response)
              ->setRouter($serviceManager->get('Router'));

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

        return $this;
    }

よく見るとServiceManagerから'Router'というサービスとして取得したものでした。Routerというサービスは何を返すのでしょうか。
Routerというサービスが定義されているのはZend\Mvc\Service\ServiceListenerFactoryクラスです。

vendor/ZF2/library/Zend/Mvc/Service/ServiceListenerFactory.php
class ServiceListenerFactory implements FactoryInterface
{

    protected $defaultServiceConfig = array(

        'factories' => array(

            'Router'                         => 'Zend\Mvc\Service\RouterFactory',

        ),

ServiceListenerFactoryクラスのフィールド定義に存在しています。factoriesサービスの定義なので、RouterというサービスはZend\Mvc\Service\RouterFactoryというファクトリクラスが返すインスタンスであることが分かりました。
RouterFactoryを見てみましょう。

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

class RouterFactory implements FactoryInterface
{

    public function createService(ServiceLocatorInterface $serviceLocator, $cName = null, $rName = null)
    {
        $config             = $serviceLocator->has('Config') ? $serviceLocator->get('Config') : array();

        $routerClass        = 'Zend\Mvc\Router\Http\TreeRouteStack';
        $routerConfig       = isset($config['router']) ? $config['router'] : array();

        if ($rName === 'ConsoleRouter'                       // force console router
            || ($cName === 'router' && Console::isConsole()) // auto detect console
        ) {
            $routerClass = 'Zend\Mvc\Router\Console\SimpleRouteStack';
            $routerConfig = isset($config['console']['router']) ? $config['console']['router'] : array();
        }

        if (isset($routerConfig['router_class']) && class_exists($routerConfig['router_class'])) {
            $routerClass = $routerConfig['router_class'];
        }

        if (!isset($routerConfig['route_plugins'])) {
            $routePluginManager = $serviceLocator->get('RoutePluginManager');
            $routerConfig['route_plugins'] = $routePluginManager;
        }

        $factory = sprintf('%s::factory', $routerClass);
        return call_user_func($factory, $routerConfig);
    }

デフォルトではZend\Mvc\Router\Http\TreeRouteStackクラス、module.config.phpでrouter_classが設定されていると、そこで設定されているクラスのインスタンスを生成していることがわかります。しかし普通は特にmodule.config.phpに設定の必要はなく、結局デフォルトのTreeRouteStackクラスになるわけです。
で、インスタンスの生成は素直にnewではなく、call_user_func関数を通してTreeRouteStack::factoryメソッドを実行しています。引数はmodule.config.phpの'router'以下の設定配列です。

TreeRouteStack::factoryを見てみましょう。

vendor/ZF2/library/Zend/Mvc/Router/Http/TreeRouteStack.php
namespace Zend\Mvc\Router\Http;

class TreeRouteStack extends SimpleRouteStack
{

    public static function factory($options = array())
    {
        if ($options instanceof Traversable) {
            $options = ArrayUtils::iteratorToArray($options);
        } elseif (!is_array($options)) {
            throw new Exception\InvalidArgumentException(__METHOD__ . ' expects an array or Traversable set of options');
        }

        $instance = parent::factory($options);

        if (isset($options['prototypes'])) {
            $instance->addPrototypes($options['prototypes']);
        }

        return $instance;
    }

$instance = parent::factory($options);

の部分でそのまま親クラスのfactoryメソッドを呼び出しています。
親クラスはSimpleRouteStackです。

vendor/ZF2/library/Zend/Mvc/Router/SimpleRouteStack.php
namespace Zend\Mvc\Router;

class SimpleRouteStack implements RouteStackInterface
{

   public static function factory($options = array())
    {
        if ($options instanceof Traversable) {
            $options = ArrayUtils::iteratorToArray($options);
        } elseif (!is_array($options)) {
            throw new Exception\InvalidArgumentException(__METHOD__ . ' expects an array or Traversable set of options');
        }

        $routePluginManager = null;
        if (isset($options['route_plugins'])) {
            $routePluginManager = $options['route_plugins'];
        }

        $instance = new static($routePluginManager);

        if (isset($options['routes'])) {
            $instance->addRoutes($options['routes']);
        }

        if (isset($options['default_params'])) {
            $instance->setDefaultParams($options['default_params']);
        }

        return $instance;
    }

    public function addRoutes($routes)
    {
        if (!is_array($routes) && !$routes instanceof Traversable) {
            throw new Exception\InvalidArgumentException('addRoutes expects an array or Traversable set of routes');
        }

        foreach ($routes as $name => $route) {
            $this->addRoute($name, $route);
        }

        return $this;
    }

$instance = new static($routePluginManager);

の部分で自身のインスタンスを生成しています。
そして、module.config.phpの'router'の'routes'の設定をまるまるインスタンスにセットしています。
この部分です。

$instance->addRoutes($options['routes']);

routesはmodule.config.phpに設定するルーティング情報です。この後行なわれるルーティング処理の元となる設定情報をRouterクラスに注入しているわけです。
addRoutesメソッドはSimpleRouteStackクラスに定義されており、TreeRouteStackではオーバーライドされていません。
一応、addRoutesメソッドが何をやっているかを見てみると、引数で来た配列を順に回してaddRouteメソッドに処理を呼び出しているだけです。
addRouteの方はTreeRouteStackでオーバーライドされているため、そちらを見ます。

vendor/ZF2/library/Zend/Mvc/Router/Http/TreeRouteStack.php
class TreeRouteStack extends SimpleRouteStack
{

    public function addRoute($name, $route, $priority = null)
    {
        if (!$route instanceof RouteInterface) {
            $route = $this->routeFromArray($route);
        }

        return parent::addRoute($name, $route, $priority);
    }

引数で来たものがRouteInterfaceを実装したクラスのインスタンスでなければrouteFromArrayメソッドを通しています。
来ているのはただの配列のはずなので、routeFromArrayが呼ばれます。ここでは配列情報をもとに、Routerクラスのインスタンスに変換しています。ここでいうRouterクラスとはつまり、module.config.phpのroutes設定の中の個々の設定の'type'で指定したクラスです。LiteralとかSegmentとかの、RouteInterfaceを実装するクラスです。そしてルーティング設定として、それらのリストをTreeRouteStack内に保持します。

addRouteしたら、生成したTreeRouteStackのインスタンスをreturnしています。
Application::bootstrapでは結局、ServiceManagerのget('Router')でTreeRouteStackのインスタンスを取得し、MvcEventにsetRouterメソッドでセットしています。


Zend\Mvc\RouteListener::onRouteに戻ります。
上記の通りなので、$e->getRouter()はTreeRouteStackのインスタンスが得られるわけですが、、

$routeMatch = $router->match($request);

この部分でそのTreeRouteStackのmatchメソッドを実行しています。引数はリクエスト情報を保持する、Zend\Http\PhpEnvironment\Requestクラスのインスタンスです。

vendor/ZF2/library/Zend/Mvc/Router/Http/TreeRouteStack.php
class TreeRouteStack extends SimpleRouteStack
{

    public function match(Request $request, $pathOffset = null, array $options = array())
    {
        if (!method_exists($request, 'getUri')) {
            return null;
        }

        if ($this->baseUrl === null && method_exists($request, 'getBaseUrl')) {
            $this->setBaseUrl($request->getBaseUrl());
        }

        $uri           = $request->getUri();
        $baseUrlLength = strlen($this->baseUrl) ?: null;

        if ($pathOffset !== null) {
            $baseUrlLength += $pathOffset;
        }

        if ($this->requestUri === null) {
            $this->setRequestUri($uri);
        }

        if ($baseUrlLength !== null) {
            $pathLength = strlen($uri->getPath()) - $baseUrlLength;
        } else {
            $pathLength = null;
        }
		
        foreach ($this->routes as $name => $route) {
            if (
                ($match = $route->match($request, $baseUrlLength, $options)) instanceof RouteMatch
                && ($pathLength === null || $match->getLength() === $pathLength)
            ) {
                $match->setMatchedRouteName($name);

                foreach ($this->defaultParams as $paramName => $value) {
                    if ($match->getParam($paramName) === null) {
                        $match->setParam($paramName, $value);
                    }
                }

                return $match;
            }
        }

        return null;
    }

ここで重要なのは最後のforeachのところです。
$this->routesというのはTreeRouteStackのfactoryメソッド内でセットした、ルーティング設定です。実際はZend/Mvc/Router/Http/Literal等のRouterクラスのインスタンスの配列です。これらを一つ一つ順に回し、リクエスト情報にマッチするかをチェックしていっています。それがif条件内にある、

$match = $route->match($request, $baseUrlLength, $options)

の部分です。
例えば、module.config.phpで以下のように設定されている場合、$this->routesの一つ目にはLiteralのインスタンスが入っており、'options'以下の設定を保持しています。

module/Hoge/config/module.config.php
        'routes' => array(

            'home' => array(
                'type' => 'Zend\Mvc\Router\Http\Literal',
                'options' => array(
                    'route'    => '/',
                    'defaults' => array(
                        '__NAMESPACE__' => '',
                        'controller' => 'Hoge\Controller\Index',
                        'action'     => 'index',
                    ),
                ),
            ),

で、そのLiteralのmatchメソッドの中身を見てみましょう。

vendor/ZF2/library/Zend/Mvc/Router/Http/Literal.php
namespace Zend\Mvc\Router\Http;

class Literal implements RouteInterface
{

    public function match(Request $request, $pathOffset = null)
    {
        if (!method_exists($request, 'getUri')) {
            return null;
        }

        $uri  = $request->getUri();
        $path = $uri->getPath();

        if ($pathOffset !== null) {
            if ($pathOffset >= 0 && strlen($path) >= $pathOffset && !empty($this->route)) {
                if (strpos($path, $this->route, $pathOffset) === $pathOffset) {
                    return new RouteMatch($this->defaults, strlen($this->route));
                }
            }

            return null;
        }

        if ($path === $this->route) {
            return new RouteMatch($this->defaults, strlen($this->route));
        }

        return null;
    }

$this->routeにはmodule.config.phpのroutes設定の’route'の部分が入っています。上の'home'というroutes設定であれば、'/'になります。
そしてリクエスト情報からリクエストURLのパス部分を取得しています。

$uri = $request->getUri();
$path = $uri->getPath();

の部分です。
これは例えばhttp://hogehoge.com/hoge/fugaだったとしたら、$pathには'/hoge/fuga'が入っています。
そしてこの$pathと、'route'の設定が一致したらマッチしたということになり、Zend/Mvc/Router/RouteMatchクラスのインスタンスが生成され、それがそのままreturnされてこのメソッドは終了です。このRouteMatchというクラスは、ルーティングが成功したときに、そのルーティング情報を格納するクラスです。コンストラクタで$this->defaultsを渡していますが、これはmodule.config.phpのroutes設定の'defaults'の部分の値が入っています。

そしてTreeRouteStackのmatchメソッドに戻り、戻り値がRouteMatchのインスタンスであったら、それをreturnしています。

RouteListenerのonRouteに戻り、戻り値がRouteMatchのインスタンスであったら、それをMvcEventのインスタンスにセットし、returnしています。

これで、routeイベントの一つ目の処理、Zend\Mvc\RouteListener::onRouteメソッドが終了です。

module/Hoge/config/module.config.php
                    // 名前空間とコントローラー名を別で設定
                    'defaults' => array(
                        '__NAMESPACE__' => 'Hoge\Controller',
                        'controller' => 'Index', //Controller名


                    // コントローラー名に名前空間を含めて設定
                    'defaults' => array(
                        'controller' => 'Hoge\Controller\Index', //Controller名

で、ルーティングの結果生成されるRouteMatchのインスタンスにはこのdefaultsの設定がそのまま保持されています。このRouteMatchのインスタンスから、getParam('controller')で、ルーティング先のコントローラー名が取得できるわけですが、そのままだと上の2つの設定方法の違いにより、前者では名前空間を含まない'Index'という値が取得されてしまいます。これではコントローラー名としては不完全なので、'__NAMESPACE__'で指定されている名前空間を、'controller'の設定値の前にくっつけて、結局、上の2パターンの設定のどちらでもgetParam('controller')で得られるコントローラー名の結果が同じになるようにしているだけです。
ここはあまりフレームワークの動作には影響がないので深く考える必要はないですが、コントローラーでこの値を参照することもあるため、処理としては重要です。


routeイベントが終了し、Applicationのrunメソッドへ戻ります。

vendor/ZF2/library/Zend/Mvc/Application.php
class Application implements
    ApplicationInterface,
    EventManagerAwareInterface
{

    public function run()
    {
            ・
            ・
            ・
        $result = $events->trigger(MvcEvent::EVENT_ROUTE, $event, $shortCircuit);
        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->stopped()という判定が入っていますが、これはイベント処理内で以降のイベントリスナの処理を行なわないという指定がされた場合、または、イベントを発火したtriggerメソッドの3番目の引数で指定したクロージャ関数の結果がtrueとなった場合(上で詳しく解説しています)に、ここの判定に引っかかることになります。個々に引っかかると、dispatchイベントやviewイベントなど、以降の処理は全て中断されてアプリケーションが終了します。