ZendFramework2ガイド

機能編

テンプレートエンジンの変更(Smarty連携)

Zend2のビューの構造

ビューを構成するコンポーネント

Zend2のViewはテンプレートエンジンとしての働きを持ち、HTMLテンプレートのファイルと、それをもとに出力するHTMLを組み上げるビュークラス群で成り立っています。
ビューを制御するクラス群はZend\Viewや、Zend\Mvc\Viewを見ればわかるようにたくさん存在し、結構複雑になっています。Zend\View以下はビューとしての働きそのもののクラス、Zend\Mvc\ViewはMVCの処理フローにZend\Viewの機能を乗せるための連携部分のクラスが集まっています。

Zend\View以下のクラス群は主に以下のようなコンポーネントに分かれます。

Zend\Mvc\View以下には、イベントリスナ系のクラスが存在します。中でもZend\Mvc\View\Http\ViewManagerというクラスはZend2のMVCにおけるビュー処理の中核をなしています。このViewManagerは、上記のZend\View以下のビューコンポーネントやZend\Mvc\View以下のイベントリスナのインスタンスを生成し、これらを組み合わせてイベントにアタッチしており、これにより、イベントフローにビュー処理が乗ることになります。
そしてZend\View\Viewというクラスはビュー系のイベントを制御しており、Zend\Mvc\Applicationクラスからの処理支持を受けて、ビュー処理の起点となります。

モジュールの作成

Smarty連携を実現するには、各ビューコンポーネントのSmarty対応版を作成する必要があります。
ここでは、vendorディレクトリにSmarty対応用のモジュールとしてZendSmartyというモジュールを作成します。
まずはvendorの下にZendSmartyフォルダを作成し、Module.phpを作成します。

vendor
  ∟ZendSmarty
    ∟Module.php

vendor/ZendSmarty/Module.php
namespace ZendSmarty;

class Module
{	
    public function getAutoloaderConfig()
    {
        return array(
            'Zend\Loader\StandardAutoloader' => array(
                'namespaces' => array(
                    __NAMESPACE__ => __DIR__,
                ),
            ),
        );
    }
}

そして、Moduleの存在をapplication.config.phpに設定します。

config/application.config.php
return array(

    'modules' => array(
        'ZendSmarty',       // 追加
        'Application',
    ),

Smartyに対応したコンポーネントクラス群の作成

SmartyRendererクラスの作成

デフォルトのRendererはZend\View\Renderer\PhpRendererです。デフォルトのテンプレートは、見ればわかるようにただのPHPを埋め込んだだけのテンプレートです。変数部分は普通にPHPタグになっており、<?php echo $hoge; ?>みたいな感じになっています。なので、このテンプレート用のRendererは"PhpRenderer"という名前なわけです。
今回はこのPhpRendererの代わりにSmartyを利用し、Smarty形式のテンプレートに対応したRendererとしたいので、SmartyRendererという名前のRendererを作成します。

まず、RendererクラスがRendererとして機能するにはZend\View\Renderer\RendererInterfaceを実装していなければなりません。
それを踏まえ、SmartyRendererの作成方法として2つの方法が考えられます。

一つはPhpRendererを継承してSmartyRendererを作成する方法です。PhpRendererは当然RendererInterfaceを継承しています。そして標準のRendererとして必要な機能が当然入っているわけで、これを継承して、テンプレートに対して変数を割り当てる処理部分のみをSmartyのやり方に変えればいいことになります。具体的にはrenderメソッドです。これはRendererInterfaceのメンバですが、テンプレートへ変数を割り当て、HTMLの構築を実際に行うメソッドです。これをオーバーライドし、PhpRendererのrenderメソッドでやっていることを、Smarty風に書き換えたメソッドとすればいいわけです。この場合、肝心のSmartyのインスタンスはSmartyRendererのメンバとして内包し、それを利用する形にする必要があります。

もう一つの方法として、Smartyを継承してSmartyRendererを作成する方法も考えられます。この場合、Rendererクラス自体がSmartyの機能を全てそのまま備えていることになり、Smartyのいろいろな機能を駆使する場合はいいかもしれません。しかし今度はPhpRendererが持っている機能でSmartyRendererにも必要と思われる機能はPhpRendererからコピーする必要があります。またこの場合、RendererInterfaceを実装する必要があります。

どちらも一長一短はありますが、総合的に見てPhpRendererを継承する方法の方が作成しなければならないメソッドも減り、いろいろと都合がいい気がしますので、そちらとして進めます。

vendor/ZendSmarty/SmartyRenderer.php
namespace ZendSmarty;

use Zend\View\Model\ViewModel;
use Zend\View\Renderer\PhpRenderer;

class SmartyRenderer extends PhpRenderer
{
    protected $smarty;
    
    public function __construct($smarty)
    {
        $this->smarty = $smarty;
        parent::__construct();
    }

    public function getEngine()
    {
        return $this->smarty;
    }

    public function render($nameOrModel, $values = null)
    {
        $vars = array();
        if ($nameOrModel instanceof ViewModel) {            
            $model = $nameOrModel;
            $name = $model->getTemplate();            
            $vars = $model->getVariables();
            
            $helper = $this->plugin('view_model');
            $helper->setCurrent($model);            
        } else {
            $name = $nameOrModel;
            $vars = $values;
        }

        foreach ($vars as $key => $value) {
            $this->smarty->assign($key, $value);
        }
        
        $templateFile = $this->resolver($name);            
        $content = $this->smarty->fetch($templateFile);

        return $this->getFilterChain()->filter($content);
    }
}

PhpRendererから変わっていることはrenderメソッドくらいという感じです。
PhpRendererのrenderメソッドを解析し、最低限必要と考えられる部分のみをSmarty用に置き換えていったらこんな感じではないかと思います。Smartyのインスタンスはコンストラクタで外部から受け取るようにしています。コンストラクタ内でnewしてもかまいませんが、依存性を弱めるためには受け取るほうがいいでしょう。

SmartyRendererStrategyクラスの作成

rendererイベント時に、標準のRendererであるPhpRendererを生成してくれるクラスがZend\View\Strategy\PhpRendererStrategyクラスです。
Strategyはイベントリスナである必要があり、そのためZend\EventManager\ListenerAggregateInterfaceを実装している必要があります。
そして、attachメソッドではrendererイベントに対してメソッドを割り当て、そのメソッドはRendererのインスタンスを返す必要があります。
更に、responseイベントに対してもメソッドを割り当て、そのメソッドではResponseのインスタンスに、出力するHTMLをセットする必要があります。
PhpRendererStrategyではコンストラクタでPhpRendererのインスタンスを受け取って自身のフィールドとして保持し、rendererイベント発火時に、それを返すようになっています。

これにならい、SmartyRendererを返すSmartyRendererStrategyを作成します。

vendor/ZendSmarty/SmartyRendererStrategy.php
namespace ZendSmarty;

use Zend\EventManager\EventManagerInterface;
use Zend\EventManager\AbstractListenerAggregate;
use Zend\View\ViewEvent;

class SmartyRendererStrategy extends AbstractListenerAggregate
{
    protected $renderer;
    
    public function __construct(SmartyRenderer $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)
    {
        // アクションメソッドが返したのがSmartyModel出なければ何も返さない
        if (!$e->getModel() instanceof SmartyModel) {
            return;
        }
        return $this->renderer;
    }

    public function injectResponse(ViewEvent $e)
    {
        $renderer = $e->getRenderer();
        if ($renderer !== $this->renderer) {
            return;
        }
        $result = $e->getResult();
        $response = $e->getResponse();
        $response->setContent($result);
    }
}

PhpRendererStrategyとの違いは入出力するRendererのクラスが異なる事ですが、少し違うのは、selectRendererメソッドで、$e->getModel()で得られるViewModelがSmartyModelのインスタンスであるかどうかの判定を行っていることです。SmartyModelではない場合は何も返さないようにしています。この理由ですが、それにはまずRenderer選択の仕組について説明しなくてはなりません。

dispatchイベント後、レスポンス生成のイベントとしてrenderイベントが発火されるわけですが、このrenderイベントの処理内で2次的に発火されるイベントとしてrendererイベントがあります。このrendererイベントは、レスポンス生成を実際に行う役目を持つRendererクラスを決定するイベントです。Rendererはいくつかの種類が存在し、その中から実際に使うものを選択するわけです。このrendererイベントには、Rendererのインスタンスを返すメソッドを登録する前提になっています。登録された複数のメソッドは順番に処理されるのですが、最初に実行されたメソッドほど優位となり、最初にRendererのインスタンスが返った時点でそのRendererに決定するわけです。なので、標準のPhpRendererを返すPhpRendererStrategyのように、単にRendererのインスタンスを無条件に返すようなメソッドの場合、そのメソッドが呼び出された時点で無条件にそのRendererで決定してしまうわけです。デフォルトではPhpRenderer以外は想定されていないため、無条件にPhpRendererが返されているのです。

で、PhpRendererStrategyのselectRendererは優先度1でrendererイベントに登録されます。上述の通り、PhpRendererStrategyのselectRendererは無条件にPhpRendererを返してしまうため、今回作成したSmartyRendererが選択されるには、PhpRendererStrategyより先にSmartyRendererStrategyのselectRendererメソッドが呼び出される必要がある事になります。そのため、SmartyRendererStrategyは優先度を1より大きい数値で登録すれば良いことになります。ということは、SmartyRendererStrategyのattachメソッド内のEventManagerへのattachで指定する優先度を1より大きい数値にすればいいことになりますが、実はViewManagerにはもともと拡張Strategyを想定した仕組が組み込まれており、それを利用した場合には優先度100で登録されるようになっています。

しかし、SmartyRendererStrategyのselectRendererメソッドでも、PhpRendererStrategyと同じように無条件にSmartyRendererを返してしまうと、今度は逆に、どのような場合でもSmartyRendererが選択されてしまう事になります。そのような前提のアプリケーションなら良いですが、例えばAjaxを利用したアプリであった場合には、JSONを出力したいとなると思います。このような場合にはJsonRendererなどを利用するでしょうし、そうすると何がなんでもSmartyRendererだと不都合が生じます。そのため、アクションメソッドが返したViewModelの種類によって選択されるRendererが切り替わるようにするには、selectRendererで無条件にRendererを返すのではなく、アクションメソッドが返したViewModelを見て、SmartyModelであればSmartyRendererを返し、そうでない場合には何も返さないようにします。そうすると、次の順番のStrategyに処理が移ってそちらが選択されるようになり、結果的にアクションメソッドで何をreturnするかによってRendererを切り替えることができるようになるというわけです。

SmartyModelクラスの作成

Smartyになったからといってビューモデルとしての処理は何も変わりません。コントローラーからRendererまでデータを運ぶという役割に関してはSmartyだろうがPHPだろうが違いはないからです。なのでSmartyを利用するという目的を達成するためだけであれば特にSmartyModelというクラスは必要なく、普通のViewModelでもいいわけなのですが、上のSmartyRendererStrategyのところで説明している通り、SmartyRenderer以外も利用する可能性があるアプリの場合はRenderer切替を実現する為だけにSmartyModelを作成する必要があります。

なので、クラス自体はViewModelを継承しただけの空のクラスになります。
もちろん、これは最低限で、独自の処理が必要な場合は何かメソッドを作ればいいと思います。

vendor/ZendSmarty/SmartyModel.php
namespace ZendSmarty;

use Zend\View\Model\ViewModel;

class SmartyModel extends ViewModel
{
}

SmartyRendererStrategyFactoryクラスの作成

SmartyRendererStrategyをビュー処理のイベントリスナとして作成しましたが、このイベントリスナをイベントに登録するという肝心の処理がどこにもありません。この状態では当然Zend2のMVCの処理フローには乗りません。これらの存在をフレームワークに伝え、PhpRendererの代わりにSmartyRendererが利用されるように持っていく必要があります。

先述のとおり、Zend\Mvc\View\Http\ViewManagerクラスが、ビューのコンポーネントのインスタンスを生成し、イベント処理に割り当てを行っているわけですが、標準のRendererであるPhpRendererを返すPhpRendererStrategyについては、このViewManager内で直接インスタンスを生成し、イベントにattachしています

では今回のような拡張Rendererについてはどうやってイベントに乗せるかですが、単純にapplication.config.phpでlisteneresにStrategyを設定するという方法も考えられますが、ViewManagerには拡張Strategyを想定した処理が組み込まれています。標準のPhpRendererStrategyのインスタンスの生成はViewManager内で"new PhpRendererStrategy"とハードコーディングされていますが、拡張のRendererStrategyクラスの方はというと、ViewManagerにとっては当然クラス名なんて知る由もありません。そのため、ServiceManagerを介して拡張のRendererStrategyのインスタンスが生成されるようになっています(ServiceManagerについては「サービスマネージャー」で解説しています)。

具体的には、拡張のRendererStrategyをmodule.config.phpなどでサービスに登録します。そして更にmodule.config.phpの"view_manager"の設定の中に、"strategies"というキーを設け、拡張Strategyクラスに割り当てたサービス名を指定します。これでViewManagerは"strategies"の設定からサービス名をたどって、ServiceManagerからStrategyのインスタンスを生成してくれるようになります。

で、サービスに登録するのはStrategyクラスですが、Strategyクラスをinvokablesで直接ServiceManagerに登録すると、Strategyクラス内でRendererを生成する必要がでてきます。invokablesで登録されたクラスはコンストラクタに引数がない前提で動作する仕組みだからです。更にRendererに渡すSmartyのインスタンスもStrategy内で生成する必要があります。当然、Rendererに渡すのだからRendererをnewするよりも前のタイミングでSmartyのnewが必要です。
また、Smartyはある程度の初期設定が必要であるため、それらのコーディングも必要になります。

こう考えると、StrategyのコンストラクタはSmartyクラス、SmartyRendererクラスに直接依存する事になってしまいます。特に気にしないならば良いかもしれませんが、コンストラクタがあまり肥大化するのは考えものです。
そこでServiceManagerのinvokableではなく、factoriesとして登録し、SmartyRendererStrategyを生成するSmartyRendererStrategyFactoryクラスを作成し、それを介してStrategyのインスタンスを生成して返すようにします。

vendor/ZendSmarty/SmartyRendererStrategyFactory.php
namespace ZendSmarty;

use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use Zend\View\Resolver\TemplatePathStack;

class SmartyRendererStrategyFactory implements FactoryInterface
{
    public function createService(ServiceLocatorInterface $serviceLocator)
    {
    	// Smartyインスタンス生成
        $moduleConfig = $serviceLocator->get('Config');
        if (!isset($moduleConfig['view_manager']['smarty']['class_path'])) {
            throw new \Exception('Smarty class file path is not specified. Please set to module.config.php.');
        }        
        require_once(realpath($moduleConfig['view_manager']['smarty']['class_path']));
        $smarty = new \Smarty();
        
        // Smartyコンパイルテンプレート出力ディレクトリ設定
        if (!isset($moduleConfig['view_manager']['smarty']['compile_dir'])) {
            throw new \Exception('Smarty compile directory is not specified. please set on module.config.php.');
        }
        $smarty->setCompileDir(realpath($moduleConfig['view_manager']['smarty']['compile_dir']));

        // Smartyのテンプレートディレクトリ設定(テンプレート内でのincludeの基点)
        $templatePaths = $serviceLocator->get('ViewTemplatePathStack')->getPaths()->toArray();
        if (count($templatePaths) > 0) {
            $smarty->template_dir = $templatePaths[0];
        }

        // Renderer生成
        $renderer = new SmartyRenderer($smarty);
        $renderer->setHelperPluginManager($serviceLocator->get('ViewHelperManager'));

        // テンプレート拡張子設定
        $suffix = 'tpl';
        if (isset($moduleConfig['view_manager']['smarty']['template_suffix'])) {
            $suffix = $moduleConfig['view_manager']['smarty']['template_suffix'];
        }
        $resolver = $serviceLocator->get('ViewResolver');
        foreach($resolver->getIterator() as $listener) {
            if ($listener instanceof TemplatePathStack) {
                $listener->setDefaultSuffix('tpl');
            }
        }
        $renderer->setResolver($resolver);
        
        // Strategy生成
        $strategy = new SmartyRendererStrategy($renderer);
        return $strategy;
    }
}

見たとおり、Smartyの各種設定等はmodule.config.phpに持たせる前提にしました。
設定内容は下を見て下さい。

module.config.phpの設定

クラスが整ったら、module.config.phpでそれらの存在をフレームワークに伝えるだけです。
それに加え、Smartyの設定などもmodule.config.phpに持たせ、SmartyRendererStrategyFactoryから参照し、Smartyの初期設定を行うようにしました。
また、レイアウトテンプレートやエラーテンプレートの設定は拡張子がphtmlのものになっていますが、Smartyに合わせてtplに変更しておきましょう。もちろん、設定したテンプレートの中身はSmarty形式のテンプレートにする必要があります。

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

    'view_manager' => array(
        'strategies' => array(
            'SmartyRendererStrategy'
        ),
        'smarty' => array(
            'class_path'      => __DIR__ . '/../../../vendor/Smarty/libs/Smarty.class.php',
            'compile_dir'     => __DIR__ . '/../../../data/templates_c',
            'template_suffix' => 'tpl',
        ),
        'template_map' => array(
            'layout/layout' => __DIR__ . '/../view/layout/layout.tpl',
            'error/404'     => __DIR__ . '/../view/error/404.tpl',
            'error/index'   => __DIR__ . '/../view/error/index.tpl',
        ),
    ),

    'service_manager' => array(
        'factories' => array(
            'SmartyRendererStrategy' => 'ZendSmarty\SmartyRendererStrategyFactory', 
        ),
    ),

);

あとはアクションごとのテンプレートの拡張子もtplにし、テンプレートのタグをSmarty形式にすれば動作するはずです。


最終的に、ZendSmartyのモジュールは以下の様な構成になりました。

vendor
  ∟ZendSmarty
    ∟Module.php
    ∟SmartyModel.php
    ∟SmartyRenderer.php
    ∟SmartyRendererStrategy.php
    ∟SmartyRendererStrategyFactory.php