PEAR::HTML_Template_Flexy with Zend_View に関するメモ

PEAR::HTML_Template_FlexyはDOMコンパイル型のシンプルなテンプレートエンジンです。 Zend FrameworkMVCではZend_ViewのビュースクリプトPHPで書かれますが、いわゆるテンプレートというよりは、昔風なPHPソースを想起するような書き方になりがちです。
Zend_Viewは容易に他のテンプレートエンジンを組み込めますが、FlexyはビュースクリプトからPHPコードを追い出し、見通しのよいテンプレートを書くのにちょうどいい感じです。
注目したのは下記の点です

  • テンプレート記法がシンプルで覚えることが少ない
  • 出来上がったテンプレートが見やすくメンテナンスしやすい
  • テンプレート機能の拡張が容易であること
  • テンプレートエンジンに余計な機能を積んでいないこと
  • PHPの実行を禁止でき、内部を保護できること

PHPTALもテンプレート記法的には非常に気に入っているエンジンなのですが、GPLであることと、機能が豊富すぎて個人的にはむしろ機能を抑えたいぐらいでしたので、メンテナンス性を考えるとFlexyかなと思ったので調べてみました。

その他のリソース

Flexyに関しては、思ったより日本語のリソースが豊富でした。ただ、昨年以前の情報が多く、最近ではあまり注目されないんでしょうか・・・

Zend_Viewとの連携について書かれており、とても参考になりました。
また、問題点として指摘されている

  1. オプションstrictを指定していないとヘルパーが呼べない
  2. ヘルパー等でメソッドチェーンが使えない

私もテスト実装してすぐにこの問題に出会いました。

こちらのサイトには、FlexyのTokenizerの拡張方法が出ていて、非常に参考になります。時間ができたら、少し拡張してみたいと思います。現状では備忘的に押さえておこうかなといった感じです。その他の説明もわかりやすかったです。

関連で、maru_ccさんの一連の記事です。Flexyに限らず、字句解析の参考になりそうな感じがします。

ViewRendererを使わない形にしてFlexyの機能を全面に押し出した使い方としてシンプルな設計になっています。

それから、マニュアルにはやっぱりお世話になります。DLして使ってますが、一応リンクしときます。

Flexy with Zend_Viewの基本的な使い方

こちらで紹介されている方法が分かりやすいと思います。少し工夫してMy_Viewに渡すFlexyはサブクラスを作っておくのと、PEARはPHP5のE_STRICTが出ますので、_runで抑止するようにしてもいいかもしれません。また、Windows環境でスクリプトパスにドライブ文字が含まれる場合はフルパスでのテンプレート指定ではファイルが見つからないので、ディレクトリ名とファイル名を分解して設定しておいた方がよさそうです。

<?php
    protected function _run()
    {
        $errorReporting = error_reporting();
        error_reporting($errorReporting & ~E_STRICT);
        $path = func_get_arg(0);
        $dir  = dirname($path);
        $this->_flexy->options['templateDir'] = array($dir);
        $this->_flexy->options['compileDir']  = $dir . '/_compiled';
        $this->_flexy->compile(basename($path));
        $this->_flexy->outputObject($this);
        error_reporting($errorReporting);
    }

また、必要なオプションについては、PEAR用のiniと互換性がありそうなので、同じように書いてBootstrapから供給されるようにすることで、PEAR::getStaticPropertyで投入する必要がなくなると思います。

一時的にstrictオプションをつけてヘルパーを実行可能にする

たとえば、テンプレート内で

{headTitle(#title#):h}

と書いた場合、

<?php if ($this->options['strict'] || (isset($t) && method_exists($t, 'headTitle')))
 echo $t->headTitle('title');?>

コンパイルされます。$tはZend_Viewオブジェクトなのでヘルパーがコールされてもいいのですが、method_existsが入っているため、マジックメソッド__callで呼ばれるZend_Viewヘルパーは起動されません。
strictオプションをtrueにすればいいわけですが、せっかくあるチェックを外してしまうのは、あまり面白くありません。
ところで、マニュアルになかったのですが、

{this.method()}

と書くと、

<?php if ($this->options['strict'] || (isset($this) && method_exists($this, 'method')))
 echo htmlspecialchars($this->method());?>

コンパイルされます。
echoがちょっとうざいですが、そこは工夫でうまくやれます。前者の$tはZend_Viewオブジェクトですが$thisはFlexyオブジェクトです。Zend_Viewの常識的ビュースクリプトとは逆転しているので要注意です。
これを応用すると、$tもしくは$thisを拡張すればいろいろな使い方ができそうです。前出したときに、Flexyのサブクラスを作っておくと書いたのはここで調整が効くためです。
では、strictオプションを変更するメソッドをflexyのサブクラスに追加してみます。

<?php
    public function strict($flag)
    {
        $this->options['strict'] = (bool) $flag;
    }

flexyのサブクラスにこのように追加しておくと、

{this.strict(1)}{headTitle(#title#):h}{this.strict(0)}

これで、headTitleをコールした部分でだけ、strictが1になるので、無事headTitleがコールされます。
ここでは、flexyもしくはviewを拡張することで簡単に機能拡張できる。それでいて、無差別にテンプレートから実行できるわけではないというところがメリットかと思います。
ただ、ヘルパー部分を挟むというのはあまりスマートではありませんね。

ヘルパー用メソッドを実装する

Flexy用のビュークラスに下記のメソッドを追加してみます

<?php
    public function h()
    {
        $args = func_get_args();
        $helperName = array_shift($args);
        if (strlen($helperName) > 0) {
            return $this->__call($helperName, $args);
        }
    }

これで、下記のようにテンプレートを書くと

{h(#headTitle#,#title#)}

このようにコンパイルされます

<?php if ($this->options['strict'] || (isset($t) && method_exists($t, 'h'))) echo htmlspecialchars($t->h("headTitle","title"));?>

メソッドhは、何も返さないので静かにheadTitleヘルパーを実行します。echoが必要な時はehメソッドでも実装するとよいと思います。
無事、ヘルパーをスマートに実行することができました。

配列を引数に取るヘルパー

ヘルパーの中には、配列やオブジェクトをとるものもあります。これについても、flexyのサブクラスかviewのサブクラスかどちらかに配列を育成してメンバーにするメソッドを定義しておけば間接的に呼ぶことができそうですが、そこまでバッドノウハウをためる必要はない気がするので、やろうと思えばできるけど・・・というあたりでやめておきます。

レイアウトスクリプトとの連携

レイアウトスクリプトphpを含めたいかどうかはちょっとわかりませんが、個人的にはレイアウトスクリプトは事前に用意しておけば後から何度も修正するものではないので、仕様が確定した段階でPHPファイル化しておいてよいと考えています。
そのため、Flexyを利用する必要がないと考えています。
Zend_Viewのサブクラスを利用する場合、Zend_Layoutが呼ばれる前に、Flexyエンジンから通常のPHPエンジンに切り替えたいところです。残念ながらZend_View単独ではその機能はないので、オレオレFWでは、コンテナーとしてのビューオブジェクトとビュースクリプトを解析するエンジンを分離して、エンジンだけ切り替えられるようにしています。
こうすると、実は、ビューオブジェクトを汚染させずに、flexyを継承したエンジンを書くことができるので、個人的には合理的と考えているのですが、その実装過程でビューオブジェクトにpublicメソッドが増えてしまっており、ビューオブジェクトに実装するpublicメソッドは少なければ少ないほどよいので、この部分にジレンマがあります。

Flexyのドキュメント化されていないオプションと拡張ポイント

Flexy記法で変数の出力等で使うオプション修飾子があります。

{headTitle(#title#):h}

たとえば、:hを指定するとhtmlspecialcharsされずに出力できます。
デフォルトではhtmlspecialcharsされます。
r: preタグで囲みprint_rする配列のデバッグ
n: numberフォーマット
b: nl2brとhtmlspecialcharsをかける
などなど。これらは、ドキュメント化されていませんが、PEAR::HTML_Template_Flexy_Compiler_Flexyで実装されています。
これらの修飾子を追加したり動作を変更するには、一時的には直接書き換えてもよいのですが、ZFとの連携を考えれば、Compilerを別実装しておきたい感じがします。
FlexyにはオプションcompilerでHTML_Template_Flexy_Compilerのサブクラスインスタンスを与えることでコンパイラーを切り替えられます。Flexyコンパイラを継承してHTML_Template_Flexy_Compiler_Flexy::getModifierWrapperを修正すると修飾子の動作を変更できます。個人的には、htmlspecialcharsに引数をつけておきたいのと、出力を抑制する修飾子もあった方がよいだろうと思います。
この部分をうまく拡張しておけば、上記した$tや$thisで無駄なメソッドを増やすことなく自然な動作を確保できそうです。
コンパイラ実装を超えたFlexyの機能拡張は字句解析に踏み込んで処理する必要がありそうですが、いまのところそこに工数をさけそうにないので、指をくわえてます。やりたいなぁ。。。

Zend_ViewとMVCの小考

若干、話がずれますが、Zend_Viewを拡張してFlexyで解析するという方法だけではなく、じつは、ビュースクリプトからFlexyであれなんであれ、必要な"テンプレート"を解析するようにしようかと考えています。
シンプルなMVCではビュースクリプトファイルはテンプレートとビュー用のスクリプトを兼ねているわけですが、どうもビューを取り扱うスクリプトと表示形式を決定するテンプレートは別に取り扱った方がいいのではないかと感じることがよくあるためです。
Zend_Viewは変数のコンテナでもあり処理エンジンでもある。ビュースクリプトはテンプレートとテンプレート制御を兼ねている現状、それでもいいんですが、エンジンの切り離し、テンプレートの切り離しも比較的容易にできそうなので、オレオレFWではその方向で行こうと思っています。