Zend_Layoutで多層レイアウト -Zend_Dojo レイアウト機能を生かす-

Zend_Dojoを使ったレイアウトをこなすには、ViewRendererがビュースクリプトレンダリングした後、かつレイアウトスクリプトでヘッダを吐く前に、Dojoベースのレイアウトはすべて準備しておく必要があり、それを解決する方法は・・・ってことで。

この解決をするのに、アクションコントローラーのpostDispatchでDojo周りのレイアウトを処理できないかとも考えたが、これは処理順の問題で難しかった。
Django風にビュースクリプトから上位レイアウトを継承するような機構をしたらどうかとも思った。これは可能だが、継承の機構や制約を用意する方法がすぐには思いつかなかったので後回しにした。
レイアウトファイルの冒頭に条件付きでインクルード(partialやrender)を利用してもよいが、これはコントローラーからの制御が難しいかもしくはデータの中に条件を組み込む必要があって、積極的に採用できなかった。

Zend_Layoutを多層レイアウト化して解決

Zend_Layoutは標準で一つのレイアウトファイルだけで処理するように作られている。
これを拡張して、最終のレンダリング前に別のファイルをレイアウトとしてレンダリングするような機構を用意し、コントローラーで設定を注入しておくようにすれば、ページクラスに合わせて必要なレイアウトをスイッチできるようにしてみた。
たとえば、

  1. ベースレイアウト (html head bodyの基本構造)
  2. ヘッダー、フッターなどサイト全体で使うであろう基本レイアウト
  3. コンテンツを表示するゾーン用のレイアウト

という3層レイアウトにしてみた。
このうち、1層目はDojoでもプレーンなXHTMLでも使える共同のレイアウトファイルとし、2層目と3層目はコントローラー側でDojo用もしくはXHTMLの部分的なレイアウトファイルを利用をスイッチできるようにした。
これにより、サービス層のコントローラーはデータモデルを提供し、ページクラスでレイアウトを提供すれば、DojoXHTML用の切り替えが非常にスムーズにでき、前記事の問題も解消できる。

具体的には

  1. Zend_Layoutのサブクラスを作成
  2. mvcInstanceにサブクラスを指定
  3. コントローラーで多層設定を注入

これらの処理を行うことになる。

Zend_Layoutで多層レイアウトを可能にする

<?php
require_once ('Zend/Layout.php');
class Flower_Layout extends Zend_Layout
{
    protected $_layoutScripts = array();
    public function setLayouts(array $layoutScripts)
    {
        $this->_layoutScripts = $layoutScripts;
    }
    public function popScript()
    {
        return array_pop($this->_layoutScripts);
    }
    public function pushScript($name)
    {
        if (is_array($name) || is_string($name)) {
            $this->_layoutScripts[] = $name;
        }
		return $this;
    }
    protected function _render()
    {
        while($next = $this->popScript()) {
            if (is_array($next)) {
                $target = key($next);
                $name = $next[$target];
            }
            elseif(is_string($next)) {
                $target = $this->getContentKey();
                $name = $next;
            }
            else {
                throw new Zend_Exception('レイアウトスクリプト配列の指定に問題があります。');
            }
            $this->$target = parent::render($name);
        }
    }
    public function render($name = null)
    {
        if (is_null($name)) {
            $this->_render();
        }
        return parent::render($name);
    }
    /**
     * Static method for initialization with MVC support
     *
     * @param  string|array|Zend_Config $options
     * @return Zend_Layout
     */
    public static function startMvc($options = null)
    {
        if (null === self::$_mvcInstance) {
            self::$_mvcInstance = new self($options, true);
        } elseif (is_string($options)) {
            self::$_mvcInstance->setLayoutPath($options);
        } else {
            self::$_mvcInstance->setOptions($options);
        }

        return self::$_mvcInstance;
    }
}

renderが引数なしで呼ばれたら、スタックしてあるレイアウトファイルをレンダリングして再投入する、というだけの拡張をしたもの。

mvcInstanceにサブクラスを指定

startMvcをそのまま継承しているので、サブクラスのスタティックメソッドでstartMvcすればそのまま動作する。

コントローラーで多層設定を注入

<?php
$layout = Zend_Layout::getMvcInstance();
$layout->setLayout('layout')
       ->pushScript(array('content' => 'boarder'))
       ->pushScript(array('content' => 'tab'))
	;

連想配列のキーでレイアウトオブジェクトのフィールドに代入するようになっているので、sidebarなどに設定するためのスクリプトでもOK。
これで、レイアウト判定をビュースクリプトからもコントローラーからも分離してページクラスに隠ぺいすることができるようになった。
レイアウトスクリプトは下記のビュースクリプト用のシンプルなレイアウトスクリプトを使っている。
http://d.hatena.ne.jp/noopable/20090203/1233612076
dojoの基本的な設定はこんな感じ
http://d.hatena.ne.jp/noopable/20090203/1233621436

多重レイアウトの効用

多重レイアウトが使えるようになると、共通のヘッダーフッタを使いながら、その内部をユーザーの好みのレイアウトでレンダリングといった動的なレイアウトをスムーズに実装できる。カスタマイズポイント用にレイヤーを切ってやれば、ifなしにコントローラーでの設定の注入のみで汎用的なレイアウトが可能になる。
しかし、乱用は禁物。

問題はパフォーマンス

レイアウトをレンダリングするのに、数枚のレイアウトファイルを利用するということは、
リクエスト毎にディスクのseek数が増えてパフォーマンスが落ちる可能性があるのが気になる。apc等である程度回避できるとして、パフォーマンスにどの程度の影響がでるのか実環境に近い環境でベンチを取る必要がありそう。
本質的に解消するには、レイアウトファイルが何枚あってもその中に動的コンテンツが割り込まないように注意して、1枚にキャッシュして利用できるようにすること。
が、現在のビューの仕様だと、コンテンツは文字列で返されるのみなので、動的コンテンツとの部分的な切り分けを行うのは非常に難しい。
となれば、レスポンスオブジェクトの拡張とビューの仕様変更を行ってキャッシュしやすい構造に転換する必要があるかもしれない。
その場合でも他の部分に影響しないような実装を心掛けたいところ。