ZendFramework2ガイド

解析編

起動~アクション実行・その1(bootstrap)

リクエスト受付からアクションメソッド実行まで

クライアントよりのリクエストは全てindex.phpに集約されるので、全ての処理の基点がindex.phpということになります。
このindex.phpでやっていることは、Zend\Mvc\Applicationクラスのrunメソッドを実行するだけ。ApplicationクラスはZend2のMVCを司る、柱となるクラスです。ここから全ての処理が枝分かれ的に呼び出され、MVCアプリケーションとしての動作を形作っています。

Zend2を利用して開発を行う開発者にしてみれば、アクションメソッドが処理の起点というイメージになります。しかし、index.phpからアクションメソッドに至るまでにはフレームワークの処理過程があります。それらを知らなくても開発は可能ですが、ある程度知ることで、いろいろな拡張機能が利用出来たりして、Zend2の機能やメリットをフルに活かした開発も可能になると思います。

Zend2はイベント駆動型が取り入れられています。ほとんどの処理はいくつかのイベントに対してイベントハンドラとして登録される形となっており、イベントを順番に呼び出して各イベントに登録されている処理が連鎖的に動く仕組みになっています。
アクションメソッドに至るまでのイベントは主にboostrap、route、dispatchがあり、順番に呼び出されます。
boostrapはアプリケーションの初期設定。
routeとはリクエストURLから実行するコントローラー・アクションを決定する処理。
dispatchは決定したコントローラーに該当するコントローラークラスのインスタンスの生成と、決定したアクションに該当するアクションメソッドの実行、およびその周辺処理を指します。
これらは一連の流れと捉えることが出来ますが、ここではそれらを追ってみたいと思います。

アクションメソッドの実行に至るまでに関連する主要なクラスは以下になります。

たくさんありますが、これでも主要なものだけ挙げていますので、少しでも絡んでいるクラスを全て挙げるとこの数倍にはなると思います。

アプリケーションの起動

全てのリクエストはpublicディレクトリ直下の.htaccessで設定されたmod_rewriteの働きによってindex.phpへ振られます。
まずはindex.phpを見てみましょう。

public/index.php
chdir(dirname(__DIR__));

if (php_sapi_name() === 'cli-server' && is_file(__DIR__ . parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH))) {
    return false;
}

require 'init_autoloader.php';

Zend\Mvc\Application::init(require 'config/application.config.php')->run();

まずinit_autoloader.phpです。
Zend2はディレクトリ階層と名前空間階層が一致することで動作するようになっており、これに従っていれば特にreqireなどの処理は不要でクラスはオートロードしてくれる仕組みになっています。
init_autoloader.phpは、Zend2自体をモジュールとして読み込み、このオートロードの仕組に乗せる処理を行っています。

次の行では、Applicationクラスのinitメソッドを呼び出していますが、引数としてrequire関数の実行結果がそのまま渡されるような形になっています。requireは言うまでもなく別のPHPを読み込む関数ですが、読み込んだファイルがreturnで何か値を返していると、戻り値としてreturnしたものが返ってきます。config/application.config.phpの中を見てみると、配列をそのままreturnしています。つまり、config/application.config.php内で配列で定義されている設定値がそのままrequireの戻り値として返ってくることになり、結局Application::initの引数としてその配列を指定していることになります。

それではApplication::initを見てみましょう。

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

class Application implements
    ApplicationInterface,
    EventManagerAwareInterface
{

    public static function init($configuration = array())
    {
        $smConfig = isset($configuration['service_manager']) ? $configuration['service_manager'] : array();
        $serviceManager = new ServiceManager(new Service\ServiceManagerConfig($smConfig));
        $serviceManager->setService('ApplicationConfig', $configuration);
        $serviceManager->get('ModuleManager')->loadModules();

        $listenersFromAppConfig     = isset($configuration['listeners']) ? $configuration['listeners'] : array();
        $config                     = $serviceManager->get('Config');
        $listenersFromConfigService = isset($config['listeners']) ? $config['listeners'] : array();

        $listeners = array_unique(array_merge($listenersFromConfigService, $listenersFromAppConfig));

        return $serviceManager->get('Application')->bootstrap($listeners);
    }

ServiceManagerへの各種サービスの登録

まずServiceManagerのインスタンスを生成しています。ServiceManagerはアプリケーションを通して利用できるサービスとしてのクラスのインスタンスを保持するクラスで、サービスロケーターパターンに則ったものです(詳しくは解析編の「サービスロケーター、機能編の「サービスマネージャー」を参照して下さい)。ServiceManagerをnewする際に引数としてZend\Mvc\Service\ServiceManagerConfigのインスタンスを渡しています。このクラスはMVC処理の核となるサービスの情報を保持しているクラスです。このクラスの一部を見てみましょう。

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

class ServiceManagerConfig implements ConfigInterface
{
    protected $invokables = array(
        'SharedEventManager' => 'Zend\EventManager\SharedEventManager',
    );

    protected $factories = array(
        'EventManager'  => 'Zend\Mvc\Service\EventManagerFactory',
        'ModuleManager' => 'Zend\Mvc\Service\ModuleManagerFactory',
    );

    protected $abstractFactories = array();

    protected $aliases = array(
        'Zend\EventManager\EventManagerInterface' => 'EventManager',
    );

    protected $shared = array(
        'EventManager' => false,
    );

    public function __construct(array $configuration = array())
    {
        if (isset($configuration['invokables'])) {
            $this->invokables = array_merge($this->invokables, $configuration['invokables']);
        }

        if (isset($configuration['factories'])) {
            $this->factories = array_merge($this->factories, $configuration['factories']);
        }

        if (isset($configuration['abstract_factories'])) {
            $this->abstractFactories = array_merge($this->abstractFactories, $configuration['abstract_factories']);
        }

        if (isset($configuration['aliases'])) {
            $this->aliases = array_merge($this->aliases, $configuration['aliases']);
        }

        if (isset($configuration['shared'])) {
            $this->shared = array_merge($this->shared, $configuration['shared']);
        }
    }

$invokables、$factoriesなどのフィールドとしてサービスが定義されていますが、4つのクラスが既に定義されています。そしてコンストラクタでは、受け取ったサービスのリスト情報をこのサービスの定義に加える処理が行なわれています。

Application::initに戻って、引数の$configurationには上述のとおり、application.config.phpの内容がそのまま来ています。
$configuration['service_manager']を引数にServiceManagerConfigがnewされているので、application.config.phpの'service_manager'の設定内容がServiceManagerConfigのコンストラクタの処理でサービスリストに加えられることになります。
で、その状態のServiceManagerConfigのインスタンスを引数に、ServiceManagerのインスタンスが生成されることになります。
ServiceManagerのコンストラクタを見てみましょう。

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

class ServiceManager implements ServiceLocatorInterface
{
    public function __construct(ConfigInterface $config = null)
    {
        if ($config) {
            $config->configureServiceManager($this);
        }
    }

ServiceManagerConfigのインスタンスを引数で受けていますが、そのServiceManagerConfig::configureServiceManagerメソッドが実行されています。その引数にはServiceManager自身のインスタンスが渡されています。

ServiceManagerConfig::configureServiceManagerを見てみましょう。

vendor/ZF2/library/Zend/Mvc/Service/ServiceManagerConfig.php
class ServiceManagerConfig implements ConfigInterface
{

    public function configureServiceManager(ServiceManager $serviceManager)
    {
        foreach ($this->invokables as $name => $class) {
            $serviceManager->setInvokableClass($name, $class);
        }

        foreach ($this->factories as $name => $factoryClass) {
            $serviceManager->setFactory($name, $factoryClass);
        }

        foreach ($this->abstractFactories as $factoryClass) {
            $serviceManager->addAbstractFactory($factoryClass);
        }

        foreach ($this->aliases as $name => $service) {
            $serviceManager->setAlias($name, $service);
        }

        foreach ($this->shared as $name => $value) {
            $serviceManager->setShared($name, $value);
        }

        $serviceManager->addInitializer(function ($instance) use ($serviceManager) {
            if ($instance instanceof EventManagerAwareInterface) {
                if ($instance->getEventManager() instanceof EventManagerInterface) {
                    $instance->getEventManager()->setSharedManager(
                        $serviceManager->get('SharedEventManager')
                    );
                } else {
                    $instance->setEventManager($serviceManager->get('EventManager'));
                }
            }
        });

        $serviceManager->addInitializer(function ($instance) use ($serviceManager) {
            if ($instance instanceof ServiceManagerAwareInterface) {
                $instance->setServiceManager($serviceManager);
            }
        });

        $serviceManager->addInitializer(function ($instance) use ($serviceManager) {
            if ($instance instanceof ServiceLocatorAwareInterface) {
                $instance->setServiceLocator($serviceManager);
            }
        });

        $serviceManager->setService('ServiceManager', $serviceManager);
        $serviceManager->setAlias('Zend\ServiceManager\ServiceLocatorInterface', 'ServiceManager');
        $serviceManager->setAlias('Zend\ServiceManager\ServiceManager', 'ServiceManager');
    }

まず、上で説明したように、コンストラクタでの処理によってapplication.config.phpの設定内容がServiceManagerConfigのフィールドとして定義されているサービスリストのに加えられている状態です。configureServiceManagerメソッドでは、これらのサービスリストをServiceManagerに登録する処理が行なわれています。最後の方を見ると、ServiceManagerのインスタンス自身も、サービスとして登録されているのがわかります。

そしてApplication::initに戻り、直後に、

$serviceManager->setService('ApplicationConfig', $configuration);

という処理があるとおり、application.config.phpの設定配列もサービスとして登録されています。

モジュールの読み込み

一連のサービスの登録の処理の次に行なわれているのはモジュールの読み込みです。

$serviceManager->get('ModuleManager')->loadModules();

の部分です。
ModuleManagerはServiceManagerConfigクラスに最初から定義されているサービスリストに含まれているため、ここまでのサービス登録処理により、既にServiceManagerに登録されています。そのインスタンスをServiceManagerのgetメソッドで早速取得し、loadModulesメソッドを実行しています。

ここで、モジュールの読み込みが行なわれるわけですが、読み込むモジュールはapplication.config.phpの"modules"で定義されているモジュールです。そして、application.config.phpの中の'module_listener_options'の'module_paths'に設定されているディレクトリの直下から定義されているモジュールを探し、存在すれば読み込んでいます。

config/application.config.php
return array(

    'modules' => array(
        'Hoge',
    ),

    'module_listener_options' => array(

        'module_paths' => array(
            './module',
            './vendor',
        ),

イベントリスナの登録

Application::initでモジュールの読み込みの次の処理です。
もう一度、initメソッドを見てみましょう。

namespace Zend\Mvc;

class Application implements
    ApplicationInterface,
    EventManagerAwareInterface
{

    public static function init($configuration = array())
    {
                ・
                ・
        $listenersFromAppConfig     = isset($configuration['listeners']) ? $configuration['listeners'] : array();
        $config                     = $serviceManager->get('Config');
        $listenersFromConfigService = isset($config['listeners']) ? $config['listeners'] : array();

        $listeners = array_unique(array_merge($listenersFromConfigService, $listenersFromAppConfig));

        return $serviceManager->get('Application')->bootstrap($listeners);
    }

$listenersFromAppConfig = isset($configuration['listeners']) ? $configuration['listeners'] : array();

の部分は、application.config.phpの'listeners'の設定を取得しています。
そして次の、

$config = $serviceManager->get('Config');
$listenersFromConfigService = isset($config['listeners']) ? $config['listeners'] : array();

の部分ですが、'Config'というサービスには、モジュールのmodule.config.phpの設定内容が格納されています。モジュールが複数ある場合、全てのモジュールのmodule.config.phpがマージされた状態のものが得られます。そのため、復数モジュールを管理する場合に気をつけなければならないのは、復数モジュールの各module.config.phpに、同じキーの設定が存在した場合、後ろのモジュールもので上書きされていくため、上の方のモジュールの設定は無意味ということになります。

そして、

$listeners = array_unique(array_merge($listenersFromConfigService, $listenersFromAppConfig));

ここでapplication.config.phpのlistenersとmodule.config.phpのlistenersがマージされます。
initメソッドの最後は、

return $serviceManager->get('Application')->bootstrap($listeners);

となっていますが、$serviceManager->get('Application')ではApplicationクラスのインスタンスが取得できます。今処理しているのはApplicationクラスのinitメソッドのはずですが、initメソッドはスタティックメソッドです。ここまでにApplicationクラスのインスタンスは一度も生成していません。そしてここで初めてApplicationのインスタンスが生成されるわけです。で、すぐにbootstrapメソッドが実行されています。引数はapplication.config.phpのlistenersとmodule.config.phpのlistenersをマージした配列です。

Application::bootstrapメソッドを見てみましょう。

vendor/ZF2/library/Zend/Mvc/Application.php
    protected $defaultListeners = array(
        'RouteListener',
        'DispatchListener',
        'ViewManager',
        'SendResponseListener',
    );

    public function bootstrap(array $listeners = array())
    {
        $serviceManager = $this->serviceManager;
        $events         = $this->events;

        $listeners = array_unique(array_merge($this->defaultListeners, $listeners));
        foreach ($listeners as $listener) {
            $events->attach($serviceManager->get($listener));
        }

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

まず、引数で受け取った配列、つまりapplication.config.phpのlistenersとmodule.config.phpのlistenersをマージしたものですが、更にこれと$this->defaultListeners、つまりApplicationクラス自身のフィールドに設定されているイベントリスナ設定もマージします。

そしてそれらのイベントリスナを全てEventManagerのattachメソッドによってイベントに登録しています(EventManagerについては機能編の「イベント」で詳しく解説しています)。

その後、Zend\Mvc\MvcEventクラスのインスタンスを生成し、各種情報をセットしています。そして、EventManagerによるイベントの発火時、呼び出されるメソッドには必ずこのMvcEventのインスタンスが引き回され、それらのイベントハンドラとして動作するメソッドの中からはアプリケーションの各種情報が参照できるようになっているわけです。

そしてbootstrapイベントが発火されます。

bootstrapイベント

Applicationのbootstrapメソッドの最後にboostrapイベントが発火され、そこからbootstrapイベントの処理に入っていきます。
この時実行されるのは、事前にEventManagerに対して、bootstrapイベントにattachされたメソッド群です。
その中の主な物は以下です。

ViewManager::onBootstrapは、ビュー関連のイベント処理の登録などを行います。これについてはここでは詳しく解説しません。詳しくは解析編の「ビューその1」で解説していますのでそちらを見てください。

Module::onBootstrapは、任意に作成したモジュールのModule.php内のonBootstrapです。
moduleディレクトリ、またはvendorディレクトリなどにモジュールが複数存在する場合は全てのモジュールのModuleクラスのonBootstrapが実行されます。
スケルトンアプリケーションでは、以下の処理が定義されています。

module/Hoge/Module.php
namespace Hoge;

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

イベントリスナクラスであるZend\Mvc\ModuleRouteListenerをイベントに登録しています。
ModuleRouteListenerの中身を見てみましょう。

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

class ModuleRouteListener implements ListenerAggregateInterface
{
    const MODULE_NAMESPACE    = '__NAMESPACE__';
    const ORIGINAL_CONTROLLER = '__CONTROLLER__';

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

    public function onRoute(MvcEvent $e)
    {
        $matches = $e->getRouteMatch();
        if (!$matches instanceof Router\RouteMatch) {
            return;
        }

        $module = $matches->getParam(self::MODULE_NAMESPACE, false);
        if (!$module) {
            return;
        }

        $controller = $matches->getParam('controller', false);
        if (!$controller) {
            return;
        }

        if (0 === strpos($controller, $module)) {
            return;
        }

        $matches->setParam(self::ORIGINAL_CONTROLLER, $controller);

        $controller = $module . '\\' . str_replace(' ', '', ucwords(str_replace('-', ' ', $controller)));
        $matches->setParam('controller', $controller);
    }

routeイベントにonRouteメソッドを登録しています。
このonRouteでやっていることはあとで解説します。

これでApplicationクラスのbootstrapメソッドは終了となり、Applicationクラスのインスタンス自身を返します。
そして、このbootstrapメソッドを呼び出していたのはApplicationクラスの静的メソッドであるinitメソッドでした。もう一度この部分を見てみましょう。

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

class Application implements
    ApplicationInterface,
    EventManagerAwareInterface
{

    public static function init($configuration = array())
    {
                ・                
                ・
                ・
        return $serviceManager->get('Application')->bootstrap($listeners);
    }

initメソッドはbootstrapの戻り値をそのままreturnして終了しています。
bootstrapメソッドの戻り値はApplicationのインスタンスだったので、initメソッドの戻り値もそのままApplicationのインスタンスを返すことになります。
そしてこのinitメソッドを呼び出していたのはどこだったかというとindex.phpでしたね。もう一度見てみましょう。

public/index.php
        ・
        ・
        ・
Zend\Mvc\Application::init(require 'config/application.config.php')->run();

initメソッドはスタティックメソッドとしてコールされ、結論としてApplicationのインスタンスが返ってくることになります。
言ってみれば、Applicationクラスが自身のインスタンスを生成し、各種初期処理を行った状態で返す、というのがinitメソッドの役目ということになります。
そして返ってきたApplicationのインスタンスに対してメソッドチェーンでそのままrunメソッドを実行します。メソッド名のとおり、アプリケーションがここで初めて動き出すというイメージです。