Viewの挙動に、もやっと

Zend_View周辺(ViewRenderer、Zend_Layout、Zend_Controller_Action)の仕様にもやっとしてます。

ViewRenderer

http://d.hatena.ne.jp/noopable/20090211/1234300771
ここでも書きましたが、ViewRendererを有効にすると、基本的にViewRendererのビューオブジェクトが優先されてActionControllerに投入されます。ViewRenderer使用時はビューオブジェクトは共有されるということです。
一方で、各コントローラーに対応するビュースクリプトをパス検索しなければならないので、パス関係は毎コントローラー毎に"追加"されます。これがZF-3828のバグの原因になっています。いっそ、コントローラー毎にビューオブジェクトを使い捨てた方が安全かと思います。その分、コストは高くなります。特に、ビューヘルパーのコンテナーに保存されたデータはビューオブジェクトが切り替わると使えなくなるので、Zend_Layout等にビューヘルパー経由でのデータ受け渡しができなくなります。
しかし、できないと割り切ってしまうのも一考かとは思います。
そこで、パス検索部分を切り出そうかと思ったのですが、これがなかなか、ViewRenderer相当のコードを書くのはちょっと骨が折れます。コピペじゃ、なんかアレですし。多少のカスタマイズはしたにしてもViewRendererは使いたいところです。なんだかんだよくできているって事ですね。

Zend_Layout

Zend_Layoutを使う時もビューオブジェクトは共用です。となると、ビューオブジェクトの個別のカスタマイズはかなり難しくなります。逆にそこに穴を開けようとすると、いろんなところでバギーな状態になってしまいそうです。たとえば、コントローラー毎に違うビューオブジェクトを使った場合、Zend_Layoutのビューオブジェクトはどうなるか・・・Zend_LayoutはViewRendererに格納されたビューオブジェクトを読み込むのがデフォルトになっているので、最後に使用されたビューオブジェクトの設定を引き継ぐことになりそう。

パス解決方法の分散

Zend_Viewは基本的に指定したファイルをインクルードしてPHPとしてパースさせます。そうでないビューを使いたいときはZend_Viewを継承した派生クラスを投入することになります。(ってどこに?)
対象がファイルなので、パス解決がキモになるわけですが、View関連のクラスでばらばらに指定されていたりデフォルト値がハードコーディングされていたりするので、この部分は改善の余地がありそうです。実際そのうちのいくつかはissueとして上がっているようです。

ビューの本質をどちらととらえるべきなのか。

Zend_Viewのパースロジックはincludeでオブジェクトメソッド内スコープでphpをパースするところにあります。Zend_Viewオブジェクトにビュースクリプトから$thisでアクセスできるところがミソです。つまり、Zend_Viewは値のコンテナかつパースエンジンを軽量に実装してあるわけです。それだけに分離がしにくい。
ところが、ビューヘルパーはビューヘルパーで独自のコンテナを持っています。
この部分が一体であるために、永続化したいニーズと分離したいニーズ、および個々の設定が衝突しやすくなっているような気がします。

getEngineをうまく使いたい

パースエンジンを分離するなら、Zend_ViewにはgetEngineという内部エンジンを返すメソッドがありますので、これを利用する手はないでしょうか。setEngineがあるわけではなく、Zend_View本体にsetEngineできるわけではありません。実際に行いたいのはsetEngineなので、setEngine対応のビュークラスを書けばいいかな・・・

Zend_Viewの縛り

Zend_Viewの縛りはZend_Viewインターフェースと、他のコンポーネントとの兼ね合いで決まります。ZendでMVCを利用する場合、Zend_ViewのInterfaceの中でもrenderによって"ファイル名"を与えるという部分は決定的です。

Zend_View_Abstractの謎

Zend_View_AbstractはZend_Viewの変化を作成したい場合に利用します。Zend_View_Interfaceで1から作ってもいいのですが、Abstractを継承した方が何かと便利なようです。Zend_View_Abstractには_run()というabstract functionがあり、_runを実装することで多様なレンダリングエンジンを実装できるとのことです。_runはファイルをincludeするわけですが、そのまま出力するというちょっと困った実装なんですね。returnしてくれればそれなりに拡張できると思うのですが・・・。その_runの出力をrender()のobハンドラーで取得するのですが、この部分を変更しようとするとrenderを再実装しなければなりません。ところが、_filterがprivateなので呼べないのです・・・うぅ。そこで、filterについては疑似機能をpublicで実装するわけですが・・・。
こうしてみるとなぜ_runがabstractなんでしょうか・・・renderがabstractになっていたほうがいいと思います。_runを使うかどうかなど、内部メソッドは具象クラス任せの方が自然のような。パース機能を取り出して、getEngineに処理させようとすると、protectedになった_runは呼べないので、結局renderも再実装せざるをえません。
残念ながらそこまで手を入れると、ベース仕様となるZend_Viewと離れすぎていて独自仕様バリバリになってしまいます。

たとえばrenderを書いてみる

<?php
    /**
     * Processes a view script and returns the output.
     *
     * @param string $name The script script name to process.
     * @return string The script output.
     */
    public function render($name)
    {
        try {
            $this->_file = $this->_script($name);
        } catch (Zend_View_Exception $e) {
            if (isset(self::$_errorScript)) {
                $this->message = $e->getMessage();
                $this->_file = $this->_script(self::$_errorScript);    
            } else {
                throw $e;
            }
        }
        // find the script file name using the parent private method
        $this->getEngine()->setView($this);
        $unit = call_user_func_array(array($this->getEngine(), 'run'), $this->_file);
        return $this->filter($unit);
    }

オリジナルとほぼ同機能のrenderを実装しているわけですが、違うのは、

  • 指定されたビュースクリプトが見つからなかった場合にエラー用スクリプトもしくはデフォルトのスクリプトを適用させる
  • 処理エンジンを切り離して交換可能に
  • $this->filterは$this->_filterがprivateなので派生クラスから呼べないために代替メソッドです。protectedでいいですよね・・・

という2点ですが、このようにrenderはZend_Viewの派生クラスで変更したいケースが多いと思いますが、一方、abstract指定された_runは使わない(使えない)形となっています。_runは残しますが使いません。

リソースローダーでビュー用のコンテナを分離

エンジンはそれぞれのビュークラスに付属するようにして、データコンテナを分離してはどうか。
そうすれば、必要なところで必要なリソースをロードできる。リソースに対するアクセス制御も可能になるような気がする。
Zend_Applicationはそういうことも可能なのだろうか。

印象なんですが・・・

それにしても、ビューオブジェクトの寿命が長すぎる気がしています。コントローラーで設定した内容を最後まで保持できるとか、ビュースクリプトパスを冗長にできるとか、便利なこともあると思いますが、コントローラーを跨いでデータを保持するためならリクエストかレスポンスオブジェクトがいいような気がします。ビュースクリプトパスが冗長になるのはいいんですが、結局最初に設定したデフォルトパスと、現在のコントローラーが指定したパスの間に別のコントローラーが指定したパスが混入してしまうのは使い方がまずいからだろうか。。まぁ、毎度初期化すればいいのかもしれませんけど。
そのあたり、どこかで議論になっていたのだろうか・・・