ビューその1(bootstrap~dispatch)
Zend2でのビューの処理の流れ
Zend2ではApplicationというオブジェクトのもと、一連のMVCの処理が流れていきます。その中でビューは、コントローラーから受け取ったデータをもとにレスポンスを形成します。コントローラーでは基本的にZend\View\Model\ViewModelクラスのインスタンスを生成してそこにデータをセットし、リターンしますが、このビューモデルという概念のオブジェクトを橋渡しに、データを間接的にビューに引き渡しています。ViewModelは、MVCで言うところのViewそのものではありません。ですが、アーキテクチャ的にはViewに属すと言えますが、コントローラーから見ればビューそのものにデータを引き渡しているようなイメージに見えますし、そう考えても普通は問題はありません。
ApplicationクラスをMVCの処理フローの柱に、EventManagerがイベントを管理し、イベントへの処理の割り当てとイベント発火が順次行なわれることでMVCが流れていきますが、ビューの処理も同様です。
最初にbootstrapイベントでビュー関連の初期設定を行います。ここで行う事は、dispatchイベントへのビュー関連の準備処理の登録や、必要なインスタンスの生成などです。
dispatchイベントではアクションメソッドの実行がメインですが、同時にビューの準備処理も行なわれるわけです。機能編の「テンプレートエンジンの変更」のところでも解説している通り、ビューを構成するコンポーネントとして、Renderer、Strategy、Resolverなどがありますが、これらの必要なインスタンスを生成して必要な処理をrenderイベントへアタッチしています。
そしてdispatchイベントが終了した後、renderイベントが発火され、ビューの処理が一斉に動き出します。そして最終的にテンプレートをもとにHTMLがレスポンスされます。
このあたりの一連の処理を見て行きましょう。
ビュー関連コンポーネント
Zend2のビュー関連のクラスは、いくつかのコンポーネント群に分類されます。それぞれの役割が組み合わさることにより、ビューとしての動作が成り立っており、また疎結合性を担保しているとも言えます。
ビュー関連のクラスはZend\View名前空間に存在し、各コンポーネントはその下にあります。
各コンポーネント別に役割を説明します。
Renderer
Rendererはテンプレートエンジンそのものです。テンプレートを読み込み、データを出力し、最終的なレスポンスHTMLを生成する役目を持っています。
RendererはZend\View\Renderer名前空間に属し、必ずRendererInterfaceを実装します。Rendererクラスはいくつか存在しますが、標準としてデフォルトで利用されるのはPhpRendererです。Zend2の標準のテンプレートはただのPHPを埋め込んだHTMLファイルですので、PhpRendererという名前になっています。
その他、JsonRendererやConsoleRendererなど、用途別に使い分けられるRendererがいくつか存在します。
RendererInterfaceを実装していることでrenderというメソッドが必ず存在し、このrenderメソッドがHTMLを生成して返すメソッドになります。
Strategy
Strategyはビューにおけるイベントリスナです。Strategyには大きく2種類あります。
1つはZend\View\Strategy名前空間に存在するStrategyクラスで、これはrendererイベント時にRendererクラスのインスタンスを返す役割を持っています。つまり、Rendererのインスタンスは必要な時に直接newされるのではなく、このStrategyを通して取得するわけです。この種類のStrategyクラスはいくつか存在しますが、それぞれ返すRendererクラスが決まっており、例えばZend2標準のPhpRendererを返すStrategyはPhpRendererStrategyというように、Rendererごとに対応したStrategyがあります。そのため、用途に応じてStrategyを切り替えることで、Rendererを使い分ける事が出来るわけです。
もう一つはZend\Mvc\View\Http名前空間に存在するStrategyクラスで、これはMVC処理としてのビュー出力に直接関連する処理のイベントリスナクラスです。
ViewModel
ViewModelは、コントローラーを介してモデルから渡ってきたビュー表示用のデータを保持するクラスです。
コントローラーのアクションメソッドの典型的な処理は、ViewModelのインスタンスを生成し、そこにモデルから得たデータを引き渡してリターンするというものですが、このViewModelがそのデータを持ったままRendererまで運び、Rendererはそのデータをテンプレートに出力します。
ViewModelという概念は、ウインドウズのUIアプリケーションにおいて、モデルから得たデータを持ってUIに表示すると同時に、UIの状態を監視し、ユーザーからの入力があれば逆にそれをモデルまで通知する役目を持つ、いわばモデルとビューの間の双方向の橋渡し、もっと言えばビューとモデルの同期をとる役割を果たすもので、そのようなアーキテクチャをMVCとは区別してMVVMと呼ばれたりします。ただPHPの場合はクライアントとサーバーの間をHTTPというプロトコルで、クライアントからの要求に応じてサーバーがレスポンスを返してプロセスが完結するという世界であるため、ビューとモデルの間に同期という概念は存在しません。サーバーから常時クライアントのUIを監視することは不可能だからです。なので、PHPの場合はMVVMはありえないわけですが、双方向とまでは行かないまでも、モデルからビューへデータを運ぶ片方向の橋渡しを行っているという事は言えます。
Resolver
Resolverは、コンフィグやリクエスト情報をもとにテンプレートを選択する役目をもちます。
Zend2では基本的にはリクエスト情報をもとにルーティング処理が行なわれ、それによりコントローラー・アクションが決定されるわけですが、そのコントローラー・アクションの階層パス通りにテンプレートのディクトリとファイルを作成すれば、それが選択されます。また、特定のコントローラー・アクションのパスに対応するテンプレートファイルをコンフィグで指定することもでき、それが設定されている場合はそちらが優先されます。
このような情報をもとにテンプレートを決めるのがResolverです。
Helper
Helperはテンプレートから利用できる、HTML出力を補助する機能を備えたクラスです。
テンプレートには基本的にHTMLを生で記述しますが、例えばHTMLヘッダを自動的に生成したり、配列データをもとにリストタグを出力したりというような、多くの機能を備えており、それらが一つ一つクラスとして存在します。これらを必要に応じて利用することにより、HTMLを記述する労力を省けたりするわけです。
bootstrapイベントでのイベントリスナの登録
bootstrapイベントで実行されるビュー関連のイベント処理はたった一つです。
- Zend\Mvc\View\Http\ViewManager::onBootstrap
まずはViewManagerのインスタンスがどのように生成され、onBootstrapメソッドがどこでbootstrapイベントに登録されているかを見てみましょう。
ViewManagerはZend\Mvc\Service\ServiceListenerFactoryにサービスとして定義されています。
class ServiceListenerFactory implements FactoryInterface { protected $defaultServiceConfig = array( 'factories' => array( 'HttpViewManager' => 'Zend\Mvc\Service\HttpViewManagerFactory', 'ViewManager' => 'Zend\Mvc\Service\ViewManagerFactory', ), ),
見たとおり、ViewManagerはfactoriesとして設定されており、そのクラスはZend\Mvc\Service\ViewManagerFactoryです。なのでServiceManagerのget('ViewManager')とした時には、ViewManagerFactoryでViewManagerのインスタンスが生成されることになります。
ViewManagerFactoryを見てみましょう。
class ViewManagerFactory implements FactoryInterface { public function createService(ServiceLocatorInterface $serviceLocator) { if (Console::isConsole()) { return $serviceLocator->get('ConsoleViewManager'); } return $serviceLocator->get('HttpViewManager'); }
Console::isConsole()の結果によって返すものが異なるようですが、通常はHttpViewManagerの方です。"HttpViewManager"という名前のサービスは、上記のServiceListenerFactoryを見れば分かる通り、Zend\Mvc\Service\HttpViewManagerFactoryであることが分かります。
HttpViewManagerFactoryを見てみましょう。
use Zend\Mvc\View\Http\ViewManager as HttpViewManager; class HttpViewManagerFactory implements FactoryInterface { public function createService(ServiceLocatorInterface $serviceLocator) { return new HttpViewManager(); }
単純にZend\Mvc\View\Http\ViewManagerのインスタンスを生成しているだけでした。
ServiceManagerからViewManagerのインスタンスが取得できることは分かりました。
ViewManagerクラスの中身を見てみましょう。
class ViewManager extends AbstractListenerAggregate { public function attach(EventManagerInterface $events) { $this->listeners[] = $events->attach(MvcEvent::EVENT_BOOTSTRAP, array($this, 'onBootstrap'), 10000); } public function onBootstrap($event) {
見たとおり、ViewManagerはAbstractListenerAggregateを継承しており、イベントリスナとして機能するクラスであることが分かります。しかしイベントリスナはどこかでEventManagerに登録されて初めて機能します。どこで登録されているでしょうか。
ViewManagerをイベントに登録しているのはApplicationクラスのbootstrapメソッドです。
Applicationクラスの$defaultListenersというフィールドを見てみると、'ViewManager'があらかじめ含まれています。
「ルーティング・ディスパッチ」で詳しく説明していますが、この$defaultListenersは全てEventManagerに登録されます。
class Application implements ApplicationInterface, EventManagerAwareInterface { 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)); // イベントリスナを全てEventManagerに登録 } ・ ・ ・ $events->trigger(MvcEvent::EVENT_BOOTSTRAP, $event); // bootstrapイベント発火
この時点でViewManagerのonBootstrapがbootstrapイベントに登録され、すぐにbootstrapイベントが発火されます。
これによりViewManagerのonBootstrapメソッドが実行されるわけですが、ここではかなりいろいろなことが行なわれています。順に見て行きましょう。
class ViewManager extends AbstractListenerAggregate { public function attach(EventManagerInterface $events) { $this->listeners[] = $events->attach(MvcEvent::EVENT_BOOTSTRAP, array($this, 'onBootstrap'), 10000); } public function onBootstrap($event) { $application = $event->getApplication(); $services = $application->getServiceManager(); $config = $services->get('Config'); $events = $application->getEventManager(); $sharedEvents = $events->getSharedManager(); $this->config = isset($config['view_manager']) && (is_array($config['view_manager']) || $config['view_manager'] instanceof ArrayAccess) ? $config['view_manager'] : array(); $this->services = $services; $this->event = $event; $routeNotFoundStrategy = $this->getRouteNotFoundStrategy(); $exceptionStrategy = $this->getExceptionStrategy(); $mvcRenderingStrategy = $this->getMvcRenderingStrategy(); $injectTemplateListener = $this->getInjectTemplateListener(); $createViewModelListener = new CreateViewModelListener(); $injectViewModelListener = new InjectViewModelListener(); $this->registerMvcRenderingStrategies($events); $this->registerViewStrategies(); $events->attach($routeNotFoundStrategy); $events->attach($exceptionStrategy); $events->attach(MvcEvent::EVENT_DISPATCH_ERROR, array($injectViewModelListener, 'injectViewModel'), -100); $events->attach(MvcEvent::EVENT_RENDER_ERROR, array($injectViewModelListener, 'injectViewModel'), -100); $events->attach($mvcRenderingStrategy); $sharedEvents->attach('Zend\Stdlib\DispatchableInterface', MvcEvent::EVENT_DISPATCH, array($createViewModelListener, 'createViewModelFromArray'), -80); $sharedEvents->attach('Zend\Stdlib\DispatchableInterface', MvcEvent::EVENT_DISPATCH, array($routeNotFoundStrategy, 'prepareNotFoundViewModel'), -90); $sharedEvents->attach('Zend\Stdlib\DispatchableInterface', MvcEvent::EVENT_DISPATCH, array($createViewModelListener, 'createViewModelFromNull'), -80); $sharedEvents->attach('Zend\Stdlib\DispatchableInterface', MvcEvent::EVENT_DISPATCH, array($injectTemplateListener, 'injectTemplate'), -90); $sharedEvents->attach('Zend\Stdlib\DispatchableInterface', MvcEvent::EVENT_DISPATCH, array($injectViewModelListener, 'injectViewModel'), -100); }
最初の方で$this->configにmodule.config.phpの'view_manager'以下の設定を格納しています。この'view_manager'の部分の設定は、こうしてbootstrapイベント時に行う初期設定で適用されるわけです。
ルーティング失敗時の処理登録
まず、
$routeNotFoundStrategy = $this->getRouteNotFoundStrategy();
という部分と、その数行先にある、
$events->attach($routeNotFoundStrategy);
の部分は、メソッド名からも分かる通り、ルーティング処理でルーティング先が特定出来なかった場合に404 Not Foundとしてのレスポンスを生成する処理を定義した、Zend\Mvc\View\Http\RouteNotFoundStrategyというイベントリスナクラスのインスタンスを生成し、イベントに割り当てています。
このメソッドの中身は詳しくは見ませんが、単純にRouteNotFoundStrategyをnewし、そこに対してmodule.config.phpの設定を反映しています。設定とは'display_exceptions'、'display_not_found_reason'、'not_found_template'の3つです。
例外エラー発生時の処理登録
次に、
$exceptionStrategy = $this->getExceptionStrategy();
という部分と、その数行先にある、
$events->attach($exceptionStrategy);
の部分は、なんらかの例外エラー発生時のレスポンスを構成する処理を定義した、Zend\Mvc\View\Http\ExceptionStrategyというイベントリスナクラスのインスタンスを生成し、イベントに割り当てています。
このメソッドの中身もRouteNotFoundStrategyと同様、単純にExceptionStrategyをnewし、そこに対してmodule.config.phpの設定を反映しています。設定とは'display_exceptions'、'exception_template'の2つです。
HTML出力処理登録(デフォルト)
$mvcRenderingStrategy = $this->getMvcRenderingStrategy();
の部分は、ルーティングが成功した時の、通常のレスポンス処理を登録するStrategyクラスのインスタンスを生成し、イベントに登録しています。
重要な処理なのでgetMvcRenderingStrategyの中身を見てみましょう。
class ViewManager extends AbstractListenerAggregate { public function onBootstrap($event) { ・ ・ $mvcRenderingStrategy = $this->getMvcRenderingStrategy(); //getMvcRenderingStrategyメソッドを呼び出し ・ ・ $events->attach($mvcRenderingStrategy); ・ ・ } public function getMvcRenderingStrategy() { if ($this->mvcRenderingStrategy) { return $this->mvcRenderingStrategy; } $this->mvcRenderingStrategy = new DefaultRenderingStrategy($this->getView()); //getViewメソッドを呼び出し、DefaultRenderingStrategyをnew $this->mvcRenderingStrategy->setLayoutTemplate($this->getLayoutTemplate()); $this->services->setService('DefaultRenderingStrategy', $this->mvcRenderingStrategy); $this->services->setAlias('Zend\Mvc\View\DefaultRenderingStrategy', 'DefaultRenderingStrategy'); $this->services->setAlias('Zend\Mvc\View\Http\DefaultRenderingStrategy', 'DefaultRenderingStrategy'); return $this->mvcRenderingStrategy; } public function getView() { if ($this->view) { return $this->view; } $this->view = new View(); $this->view->setEventManager($this->services->get('EventManager')); $this->view->getEventManager()->attach($this->getRendererStrategy()); $this->services->setService('View', $this->view); $this->services->setAlias('Zend\View\View', 'View'); return $this->view; } public function getRendererStrategy() { if ($this->rendererStrategy) { return $this->rendererStrategy; } $this->rendererStrategy = new PhpRendererStrategy( $this->getRenderer() ); $this->services->setService('ViewPhpRendererStrategy', $this->rendererStrategy); $this->services->setAlias('Zend\View\Strategy\PhpRendererStrategy', 'ViewPhpRendererStrategy'); return $this->rendererStrategy; } }
DefaultRenderingStrategyはその名の通り、デフォルトのHTML生成処理イベントリスナクラスです。これをnewしてインスタンスを生成しますが、コンストラクタ引数として、$this->getView()の戻り値を渡しています。
getViewメソッドはZend\View\Viewクラスのインスタンスを生成しています。
生成したViewのインスタンスは独自のEventManagerのインスタンスを持ちます。このEventManagerに対して、イベント処理の登録が行なわれています。
$this->view->getEventManager()->attach($this->getRendererStrategy());
の部分です。
$this->getRendererStrategy()で得られたものはイベントリスナクラスであることが推測できますが、getRendererStrategyメソッドを見てみると、Zend\View\Strategy\PhpRendererStrategyというクラスを生成して返しているのがわかります。PhpRendererStrategyの中身を見てみましょう。
class PhpRendererStrategy extends AbstractListenerAggregate { public function __construct(PhpRenderer $renderer) { $this->renderer = $renderer; } public function attach(EventManagerInterface $events, $priority = 1) { $this->listeners[] = $events->attach(ViewEvent::EVENT_RENDERER, array($this, 'selectRenderer'), $priority); $this->listeners[] = $events->attach(ViewEvent::EVENT_RESPONSE, array($this, 'injectResponse'), $priority); } public function selectRenderer(ViewEvent $e) { return $this->renderer; }
確かに、AbstractListenerAggregateを継承しており、イベントリスナです。attachメソッドではrendererというイベントとresponseというイベントに処理を割り当てています。それぞれに付いては後ほど詳しく見ていきますが、最初の方でビューのコンポーネントの一つとしてRendererについて解説していますが、rendererイベントとは、このRendererクラスを決定するイベントです。rendererイベント発火時にはこのPhpRendererStrategyクラスのselectRendererメソッドが呼び出され、Zend\View\Renderer\PhpRendererのインスタンスが返されるわけです。
話を戻し、getViewメソッドはZend\View\Viewのインスタンスを返すことになります、ということで、DefaultRenderingStrategyのコンストラクタにはこのViewのインスタンスが渡されています。
DefaultRenderingStrategyのコンストラクタでは、渡ってきたViewクラスのインスタンスを自身のフィールドに保持しています。これはこの後の処理で生かされます。
生成されたDefaultRenderingStrategyのインスタンスに対し、setLayoutTemplateというメソッドでレイアウトテンプレートをセットしています。
引数としては$this->getLayoutTemplate()の戻り値です。getLayoutTemplateが返すのはmodule.config.phpの'view_manager'の下の'layout'の設定値です。ここに設定されるのはレイアウトテンプレートのパスです。もし、この設定がなされていない場合は、デフォルトとして'layout/layout'というパスが設定されます。
ここまで見て分かる通り、ここではレイアウトテンプレートについての処理を行っています。Zend2では、全てのレスポンスに共通のレイアウト部分のテンプレートと、アクションごとの個別のコンテンツのテンプレートを組合せて最終的なHTMLが生成されます。Zend2のRendererは階層化することができる仕組みになっていて、Rendererに対して子Rendererを設定することができます。Rendererと子Rendererはクラス的には同じものとなるので、理論上、階層は無限に深くしていくことも可能です。
で、ViewManagerの処理としては、そのトップレベルの階層部分をレイアウトと呼び、このレイアウトの処理をイベントリスナとして登録することを行っているわけです。
こうしてgetMvcRenderingStrategyで得られたDefaultRenderingStrategyはイベントリスナです。
なので、数行先でイベントへ登録されています。$events->attach($mvcRenderingStrategy);の部分です。
DefaultRenderingStrategyクラスのattachメソッドを見てみると、renderイベントと、render.errorイベントにrenderメソッドが登録されているのがわかります。
namespace Zend\Mvc\View\Http; use Zend\View\View; class DefaultRenderingStrategy extends AbstractListenerAggregate { public function __construct(View $view) { $this->view = $view; } public function attach(EventManagerInterface $events) { $this->listeners[] = $events->attach(MvcEvent::EVENT_RENDER, array($this, 'render'), -10000); $this->listeners[] = $events->attach(MvcEvent::EVENT_RENDER_ERROR, array($this, 'render'), -10000); }
HTML出力処理登録(カスタム)
DefaultRenderingStrategyは、'Default'という名前がついているとおり、あくまでデフォルトのStrategyです。これ以外に好きなStrategyを利用することも可能な仕組みが組み込まれています。
それは、
$this->registerMvcRenderingStrategies($events);
の部分です。
registerMvcRenderingStrategiesメソッドの中身を見てみましょう。
class ViewManager extends AbstractListenerAggregate { public function onBootstrap($event) { ・ ・ $this->registerMvcRenderingStrategies($events); ・ ・ } protected function registerMvcRenderingStrategies(EventManagerInterface $events) { if (!isset($this->config['mvc_strategies'])) { return; } $mvcStrategies = $this->config['mvc_strategies']; if (is_string($mvcStrategies)) { $mvcStrategies = array($mvcStrategies); } if (!is_array($mvcStrategies) && !$mvcStrategies instanceof Traversable) { return; } foreach ($mvcStrategies as $mvcStrategy) { if (!is_string($mvcStrategy)) { continue; } $listener = $this->services->get($mvcStrategy); if ($listener instanceof ListenerAggregateInterface) { $events->attach($listener, 100); } } }
ここで何をやっているかですが、まず、module.config.phpの'view_manager'の設定から'mvc_strategies'というキーで設定が存在しないか確認します。この設定は配列である必要があります。ちなみにスケルトンアプリケーションのデフォルトのmodule.config.phpにはこの設定はありません。この設定が存在したら、それをforeachで順番に参照し、サービスとして登録がないかを検索します。あれば、そのインスタンスを取得し、EventManagerにイベントリスナとして処理登録しています。つまり、'mvc_strategies'に設定するのは、'service_manager'に定義されたサービスである必要があり、取得できるサービスのインスタンスはListenerAggregateInterfaceを実装したイベントリスナでなければなりません。
以下、これに則り、自作のStrategyを設定する例を示します。
'service_manager' => array( 'factories' => array( 'MyStrategy' => 'My\View\MyStrategyFactory', ), ), 'view_manager' => array( 'mvc_strategies' => array( 'MyStrategy' ), ),
class MyStrategy extends AbstractListenerAggregate { public function attach(EventManagerInterface $events, $priority) { $this->listeners[] = $events->attach(MvcEvent::EVENT_RENDER, array($this, 'render'), $priority); } public function render(MvcEvent $e) { }
で、注目すべきなのは、registerMvcRenderingStrategiesメソッドでは、mvc_strategiesで取得したイベントリスナを優先度100で登録しているということです。DefaultRenderingStrategy の方はというと、attachメソッド内で登録されている処理は優先度-10000になっています。つまりこれより大きい数値でrenderイベントに対して処理を登録すると、そちらがかならず優先して実行されるということです。優先して実行されるとどうなるか。renderイベントで実行されるDefaultRenderingStrategyのrenderメソッドを見てみるとわかります。
class DefaultRenderingStrategy extends AbstractListenerAggregate { public function render(MvcEvent $e) { $result = $e->getResult(); if ($result instanceof Response) { return $result; }
メソッドの頭でMvcEventのインスタンスからgetResultで得られたものがResponseのインスタンスであるかを検査しています。もしそうであればそのままreturnで処理終了となります。通常はここで引っかかることはないですが、DefaultRenderingStrategyより優先度が高い処理が先にMvcEventにsetResultしていたらどうなるでしょう。この検査の部分で引っかかり、結局はDefaultRenderingStrategyを無効化しているのと同じことになります。
つまり、mvc_strategiesに自作のStrategyを設定し、そのStrategyでは最終的に生成したHTMLをMvcEvent::setResultする事で、DefaultRenderingStrategyの代わりとなることが出来るというわけです。mvc_strategiesを設定しない場合だと、DefaultRenderingStrategyのrenderメソッドで最終的に生成されたHTMLをMvcEvent::setResultしているわけですが、これと同じ事をする自作Strategyを作ればよいのです。
テンプレート決定処理登録
onBootstrapメソッドに戻り、
以下の部分で、Zend\Mvc\View\Http\InjectTemplateListenerクラスのインスタンスを取得し、そのinjectTemplateメソッドをdispatchイベントに登録しています。
class ViewManager extends AbstractListenerAggregate { public function onBootstrap($event) { ・ ・ $injectTemplateListener = $this->getInjectTemplateListener(); ・ ・ $sharedEvents->attach('Zend\Stdlib\DispatchableInterface', MvcEvent::EVENT_DISPATCH, array($injectTemplateListener, 'injectTemplate'), -90); }
イベントといっても、ここではただのEventManagerではなく、SharedEventManagerへの登録になっています。SharedEventManagerについてはここでは詳しく説明しません(詳しくは「イベント」を参照)が、簡単に説明すると以下の様なことです。
ViewManagerが持っているEventManagerはApplicationクラスで生成されたEventManagerです。このEventManagerはZend2のMVCの一連の処理を通して共通で利用されるわけではなく、これはとは別に生成されるEventManagerも存在し、イベント名が同じだとしても、それぞれが別々にイベント処理を持ちます。なので、Applicationクラスの持つEventManagerのdispatchイベントを発火させても、その他のEventManagerのdispatchイベントの処理に伝播することありません。実際、AbstractController抽象クラスが独自に持つEventManagerもdispatchイベントに処理がアタッチされていますが、これはApplicationクラスのEventManagerのdispatchイベントが発火された時点では動き出さず、別のタイミングで遅れて明示的に発火されます。
それに対し、SharedEventManagerと言うのは、MVCの一連の処理を通して全体で共有されるEventManagerです。これはシングルトンとしてインスタンスが唯一のものとなっています。で、このSharedEventManagerでイベントに処理を登録すると、個別に存在するEventManagerで特定のイベントが発火された際、SharedEventManagerの同じイベント処理も連動して同時に発火されます。しかしそれでは上記のような、例えばdispatchイベントが2箇所のEventManagerで登録されている場合、それぞれでイベント発火された時に、SharedEventManagerのdispatchイベントリスナは同じものが2回動いてしまう事になります。そこで、SharedEventManagerは、その連動発火の元となるEventManageについて、特定のものの場合だけに連動するように設定することが出来るようになっています。そのキーとなるのがSharedEventManagerのattachメソッドの一つ目の引数です。上記に示したソースでは'Zend\Stdlib\DispatchableInterface'が指定されています。一方、EventManagerにはidentifierというものが設定できます。EventManagerでイベントが発火されたら、そのEventManagerに設定されたidentifierを参照し、SharedEventManagerに対して、一つ目の引数がこれと一致するものを指定して登録された処理のみが連動して動くわけです。
上のソースでは'Zend\Stdlib\DispatchableInterface'が指定されているわけですが、これがidentifierとして設定されているのはAbstractControllerが持つEventManagerです。つまりAbstractControllerのEventManagerがtrigger('dispatch')とやった場合にのみ、上記の処理が動くというわけです。
詳しくは後ほど、dispatchイベント時の処理ということで解説しますが、要はルーティング情報からテンプレートを決定しています。
このInjectTemplateListener::injectTemplateはdispatchイベントの優先度-90で登録されています。
かなり優先度低めですが、なぜでしょうか。これは、アクションメソッドの実行よりも後のタイミングとするためです。アクションメソッドで何かエラーが発生した場合はエラーテンプレートを表示しなければなりませんし、いざアクションメソッド実行というタイミングで、呼びだそうとするアクションメソッドが存在しない場合などは404 Not Foundとして処理しなければなりません。ということで、優先度0で実行されるアクションメソッド実行周りの処理よりも後のタイミングになるようにしているわけです。
ViewModelの自動生成処理の登録
onBootstrapメソッドに戻り、
以下の部分で、Zend\Mvc\View\Http\CreateViewModelListenerクラスのインスタンスを取得し、そのcreateViewModelFromArrayメソッドとcreateViewModelFromNullメソッドをdispatchイベントに登録しています。
class ViewManager extends AbstractListenerAggregate { public function onBootstrap($event) { ・ ・ $createViewModelListener = new CreateViewModelListener(); ・ ・ $sharedEvents->attach('Zend\Stdlib\DispatchableInterface', MvcEvent::EVENT_DISPATCH, array($createViewModelListener, 'createViewModelFromArray'), -80); ・ $sharedEvents->attach('Zend\Stdlib\DispatchableInterface', MvcEvent::EVENT_DISPATCH, array($createViewModelListener, 'createViewModelFromNull'), -80); }
この処理も、優先度-80のため、アクションメソッドより後に実行されます。
アクションメソッドの基本は、最終的にViewModelのインスタンスを生成してreturnすることですが、CreateViewModelListenerがやっていることは、アクションメソッドがViewModelのインスタンスを返さなかった場合に、代わりにViewModelのインスタンスを生成することです。詳しくは後ほど、dispatchイベント処理のところで解説します。
長いサブタイトルになりましたが、以下の部分はまさにこのサブタイトル通りの処理を行うイベントリスナの登録を行っています。
class ViewManager extends AbstractListenerAggregate { public function onBootstrap($event) { ・ ・ $injectViewModelListener = new InjectViewModelListener(); ・ ・ $events->attach(MvcEvent::EVENT_DISPATCH_ERROR, array($injectViewModelListener, 'injectViewModel'), -100); $events->attach(MvcEvent::EVENT_RENDER_ERROR, array($injectViewModelListener, 'injectViewModel'), -100); ・ ・ $sharedEvents->attach('Zend\Stdlib\DispatchableInterface', MvcEvent::EVENT_DISPATCH, array($injectViewModelListener, 'injectViewModel'), -100); } }
InjectViewModelListener::injectViewModelメソッドが3箇所で登録されていますが、このイベントリスナで行われる処理は、レイアウト用のViewModelとレイアウトの中に埋め込む個別テンプレートを組み合わせる処理です。ViewModelは子ViewModelを持つことができますが、レイアウトのViewModelの子供として個別テンプレートのViewModelをセットしています。これも、詳しくは後ほどdispatchイベント処理のところで解説します。
dispatchイベントでのビュー関連処理
ここまでの解説のように、bootstrap時の処理でdispatchイベントへの処理登録が行なわれますが、あとはdispatchイベント発火時に優先度に従い、各処理が行なわれていくだけです。
これらは全てアクションメソッドの実行よりタイミングが後になります。
- Zend\Mvc\View\Http\CreateViewModelListener::createViewModelFromArray
- Zend\Mvc\View\Http\CreateViewModelListener::createViewModelFromNull
- Zend\Mvc\View\Http\RouteNotFoundStrategy::prepareNotFoundViewModel
- Zend\Mvc\View\Http\InjectTemplateListener::injectTemplate
- Zend\Mvc\View\Http\InjectViewModelListener::injectViewModel
CreateViewModelListener::createViewModelFromArray、createViewModelFromNull
createViewModelFromArrayやcreateViewModelFromNullが実行されるのは、タイミング的にはアクションメソッドの実行の直後になります。
これらのメソッドが何を行っているか見てみましょう。
class CreateViewModelListener extends AbstractListenerAggregate { public function createViewModelFromArray(MvcEvent $e) { $result = $e->getResult(); if (!ArrayUtils::hasStringKeys($result, true)) { return; } $model = new ViewModel($result); $e->setResult($model); } public function createViewModelFromNull(MvcEvent $e) { $result = $e->getResult(); if (null !== $result) { return; } $model = new ViewModel; $e->setResult($model); } }
アクションメソッドの実装は実装者に委ねられているわけですが、アクションメソッドの目的の基本は、ViewModelのインスタンスを生成してreturnすることです。returnされた値は裏でMvcEventのインスタンスにsetResultメソッドで注入されます。その後、上記のメソッドが実行されるます。
それを踏まえ、まずcreateViewModelFromArrayの方を見ると、MvcEventのインスタンスのgetResult()メソッドを実行し、得られたものが配列であるか否かを判定しています。配列ではない場合は何もせず終了。配列であった場合はViewModelのインスタンスを生成してsetResultしなおしています。つまり、本当はアクションメソッドからViewModelのインスタンスが欲しかったんだけど、意図せずただの配列が返された場合に、ちゃんとビューの処理が成り立つようにここで担保しているわけです。
同じように、createViewModelFromNullの方は、アクションメソッドで何もreturnされなかった場合に、ViewModelのインスタンスを生成します。
なので、アクションメソッドでちゃんとViewModelのインスタンスを返していれば、これらの処理はあまり意味が無いことになりますし、このイベントリスナすら不要とも言えます。
試しに、アクションメソッドでViewModelをreturnさせる状態にし、このメソッドをイベントにアタッチしている箇所をコメントアウトしてアクセスしてみると、何も動きが変わらないことがわかります。
逆に言うと、実はアクションメソッドではなにもViewModelをnewして配列データを注入したあとreturnしないでも、配列データをそのままreturnするだけでもいいって言うことも言えてしまうわけです。
RouteNotFoundStrategy::prepareNotFoundViewModel
クラス名、メソッド名からだいたいイメージ出来ますが、404 Not Foundの場合のビュー処理を行っているように推測できます。
実際に見てみましょう。
class RouteNotFoundStrategy extends AbstractListenerAggregate { public function prepareNotFoundViewModel(MvcEvent $e) { $vars = $e->getResult(); if ($vars instanceof Response) { return; } $response = $e->getResponse(); if ($response->getStatusCode() != 404) { return; } if (!$vars instanceof ViewModel) { $model = new ViewModel(); if (is_string($vars)) { $model->setVariable('message', $vars); } else { $model->setVariable('message', 'Page not found.'); } } else { $model = $vars; if ($model->getVariable('message') === null) { $model->setVariable('message', 'Page not found.'); } } $model->setTemplate($this->getNotFoundTemplate()); $this->injectNotFoundReason($model); $this->injectException($model, $e); $this->injectController($model, $e); $e->setResult($model); } }
最初の方でステータスコードをチェックし、404ではない場合は何もせず抜けています。通常はこちらになるわけですが、いざアクションメソッドを実行しようとしたがアクションメソッドが未定義だった場合には404として処理され、レスポンスのインスタンスにステータス404がセットされます。ちなみにそれを行っている箇所が以下です。
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; } public function notFoundAction() { $response = $this->response; $event = $this->getEvent(); $routeMatch = $event->getRouteMatch(); $routeMatch->setParam('action', 'not-found'); if ($response instanceof HttpResponse) { return $this->createHttpNotFoundModel($response); } return $this->createConsoleNotFoundModel($response); } protected function createHttpNotFoundModel(HttpResponse $response) { $response->setStatusCode(404); return new ViewModel(array( 'content' => 'Page not found', )); }
AbstractActionControllerのonDispatchがdispatchイベントで呼び出されますが、method_existsでアクションメソッドが見つからない場合はnotFoundActionメソッドが代わりに実行され、最終的には $response->setStatusCode(404);の部分で404をセットしています。
RouteNotFoundStrategyの処理に話を戻して、404の場合はViewModelのインスタンスを生成し、各種情報を注入した後、MvcEventにsetResultしています。これで、Not Foundのテンプレートが表示されることになります。
ということで、404の場合の処理を定義しているわけです。
InjectTemplateListener::injectTemplate
名前からして、テンプレート注入、です。テンプレートに関する何かしらの処理をしているように推測できます。
メソッドの中身を見てみましょう。
class InjectTemplateListener extends AbstractListenerAggregate { public function injectTemplate(MvcEvent $e) { $model = $e->getResult(); if (!$model instanceof ViewModel) { return; } $template = $model->getTemplate(); if (!empty($template)) { return; } $routeMatch = $e->getRouteMatch(); $controller = $e->getTarget(); if (is_object($controller)) { $controller = get_class($controller); } if (!$controller) { $controller = $routeMatch->getParam('controller', ''); } $template = $this->mapController($controller); if (!$template) { $module = $this->deriveModuleNamespace($controller); if ($namespace = $routeMatch->getParam(ModuleRouteListener::MODULE_NAMESPACE)) { $controllerSubNs = $this->deriveControllerSubNamespace($namespace); if (!empty($controllerSubNs)) { if (!empty($module)) { $module .= '/' . $controllerSubNs; } else { $module = $controllerSubNs; } } } $controller = $this->deriveControllerClass($controller); $template = $this->inflectName($module); if (!empty($template)) { $template .= '/'; } $template .= $this->inflectName($controller); } $action = $routeMatch->getParam('action'); if (null !== $action) { $template .= '/' . $this->inflectName($action); } $model->setTemplate($template); } }
何をしているかというと、要はルーティング情報からテンプレートを決定しています。
routeイベント時のルーティング処理により決定したルーティング情報をもとに、"{モジュール名}/{コントローラー名}/{アクション名}"というのが基本的なテンプレート名になります。ちなみにここで言うテンプレート名とは、=テンプレートのファイル名ではありません。これを実際のテンプレートファイル名に変換する処理が後のタイミングで別に存在します。
最初の方で、ViewModelよりgetTemplateメソッドを実行し、既にテンプレート名が設定済みでないかチェックしています。設定済みならそのまま抜けるので、アクションメソッド内でsetTemplateで任意のテンプレートを設定していた場合はそちらが生かされます。未指定の場合は、コントローラー、アクション名と一致する階層でテンプレートを配置すると、自動的にそれが利用される仕組みになっているわけです。
InjectViewModelListener::injectViewModel
ビューモデルの注入、という名前ですが、何をしているでしょうか。
class InjectViewModelListener extends AbstractListenerAggregate { public function injectViewModel(MvcEvent $e) { $result = $e->getResult(); if (!$result instanceof ViewModel) { return; } $model = $e->getViewModel(); if ($result->terminate()) { $e->setViewModel($result); return; } if ($e->getError() && $model instanceof ClearableModelInterface) { $model->clearChildren(); } $model->addChild($result); } }
アクションメソッドでViewModelをreturnしていた場合、最初の、
$result = $e->getResult();
で、個別テンプレートのビューモデルとしてのViewModelのインスタンスが取得できることになります。
もしアクションメソッドではViewModelのインスタンスではなく、単なる配列データなどがreturnされていた場合でも、このメソッドよりも先に実行が済んでいるはずの、CreateViewModelListener::createViewModelFromArrayでViewModelが生成され、setResultされるため、$e->getResult()では必ずViewModelのインスタンスが返るはずです。
そして、
$model = $e->getViewModel();
で、レイアウトのビューモデルとしてのViewModelのインスタンスが取得できます。
そして最後の、
$model->addChild($result);
の部分で、レイアウトのViewModelの子ViewModelとして、個別テンプレートのViewModelがセットされます。つまり、ここで初めてレイアウトと個別テンプレートが1つに合体されるわけです。
dispatchイベントの最終処理
dispatchイベントの中で行なわれる一連のビュー関連の処理は、直接的にはAbstractActionControllerのEventManagerが発火するdispatchイベントで動きます。これはAbstractControllerのdispatchメソッド内で、EventManagerのtriggerを実行する事で起きることですが、このdispatchメソッドはZend\Mvc\DispatchListenerのonDispatchメソッド内で呼び出されています。
class DispatchListener implements ListenerAggregateInterface { public function onDispatch(MvcEvent $e) { ・ ・ ・ try { $return = $controller->dispatch($request, $response); // この中でdispatchイベントが発火 } 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); }
このAbstractControllerのdispatchメソッドは、戻り値としてMvcEventのgetResultで得られるものを返しています。
abstract class AbstractController implements Dispatchable, EventManagerAwareInterface, InjectApplicationEventInterface, ServiceLocatorAwareInterface { public function dispatch(Request $request, Response $response = null) { ・ ・ ・ $result = $this->getEventManager()->trigger(MvcEvent::EVENT_DISPATCH, $e, function ($test) { return ($test instanceof Response); }); if ($result->stopped()) { return $result->last(); } return $e->getResult(); }
これまでの説明を呼んでいただければわかりますが、MvcEvent::getResult()が返すのはほとんどの場合はアクションメソッドの戻り値であるViewModelのインスタンスです。
なので、
$return = $controller->dispatch($request, $response);
の、$returnの部分に入ってくるのはViewModelのインスタンスということになります。
そしてDispatchListener::onDispatchの最終行は、
return $this->complete($return, $e);
となっている通り、$returnに入っているViewModelのインスタンスをcompleteメソッドを通して返ってきたものをreturnしています。
completeで行っていることですが、
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; }
MvcEventにsetResultで注入しています。
前半部分は$returnがもしただの配列であったら、ArrayObjectに変換しているだけですが、普通はここには入らないと思います。
以上でdispatchイベントでの処理が終了します。
続きは「ビューその2」で。