Zend Frameworkをどう使うか その3
Zend Framework(あるいはZend_View)と既存のテンプレートエンジンとの兼ね合いについては、MLのTemplate system, form handling and time scheduleから始まるスレッドを一読しておくといい。
Zend Frameworkのアプローチとしては、マニュアルに例として掲載されているような外部テンプレートシステムとの連携を基本と考えているのだろう。最初このマニュアルの例が何を意味しているのかよく分からなかったが、これはZend_Viewからrenderで呼び出されるスクリプト内で、PHPLIBスタイルのテンプレートを利用している、という意味のようだ。Zend_Viewからrenderで呼び出されるコードはテンプレートではなく、あくまでもHTML出力用のPHPコードであり、そこでさらに外部テンプレートシステムを使って出力するコードを書け、ということだ。さすがにその書き方はきっついなーと思わないでもないが、Zend Frameworkの構成要素自体は外部テンプレートシステムから独立するというポリシーを維持するためには、それなりにまっとうなアプローチだろう。
あと、SmartyのサイトからもリンクされているZend Framework: Using Smarty as template engineという記事で紹介されているアプローチがある。これは、SmartyのオブジェクトはZend::registerに突っ込んでおいて、Actionで直接Smartyを使ってHTMLを出力してしまい、Zend_Viewなんか使わない、というものになっている。この記事の作者は、
As you can see Smarty integration with Zend Framework is very simple task. Zend_View has ability to use your own filters and helper functions, but with Smarty you don’t need them because Smarty has its own plugins, filters, modifiers. Just forget about Zend_View and use best template engine for PHP in the world!
とまとめている。機能がかぶっているZend_Viewなんかのことは忘れて、Smartyをそのまま使った方が幸せになれるよ、って感じ。Smartyオブジェクトの置き場所がZend::registryってあたりはいまいちだし、Zend Framework全体のアプローチともきれいにかみ合わないけれども、機能としては問題ないし無駄も少なく実際的なアプローチだよな。
ちなみに上記記事のコメント欄では、Zend_View_Abstractを継承して、Smarty用のViewクラスを作ってそれを使うという、クラスライブラリ的にはかなりまっとうそうなアプローチを紹介している人がいるけれども、この記事の作者は、
I tried to create something similar but I don’t see advantages of this approach. (中略)You’ve extended Zend_View_Abstract but you don’t use almost all its functionality. I don’t think this is a good object-oriented practice - to extend but not to use methods of base class.
と答えている。確かにZend_View_Abstractを継承しているけれども、Zend_View_Abstractの機能なんてほとんど使ってないじゃん、そんなんじゃ継承している意味がないじゃん、って感じか。根性入れて、Zend_ViewのhelperとかfilterとかをSmartyのpluginとかと互換モードで動かすくらい気合いの入ったSmarty用Viewを書かないと、ほとんどの機能は互換性がないからなー。表面上のassign、render周りのインターフェースが同じになるだけでもそれなりに意味はあるとは思うけど、そんなのに大した意味はないと切り捨てる意見もわからないでもない。
ちなみに現状では実用的ではないけれども、アプローチとして一番面白かったのは、Smarty Plugin for viewというMLの投稿。これは、SmartyによるレンダリングをZend_Viewのフィルターとして実行してみよう、というもの。Zend_Viewはそのまま使い、そこでふつうにSmarty用のテンプレートファイルをrenderする。もちろんZend_ViewはSmarty用のテンプレート言語は知らないから、そのままスルーする。そしてrender後のフィルター処理でSmartyのレンダリング処理が行われる。残念なことにフィルターにはZend_Viewにassignされた値が渡されないので、現状では実用的ではないけれども(テンプレート変数が渡らないんじゃレンダリングしても意味がない)、何とかその辺を解決することができればなー。
そういやさっきのMLスレッドの最後の方に、
<disclaimer>Don't quote me and blame me if I'm wrong</disclaimer>
We are aiming to have something shippable in the 2nd half of 2006. However, this will very much depend on how the development goes and if we have enough proof points to be confident that it's in good shape design wise.
ってのがあった。2nd half of 2006って2006年下半期ってことだよね。要は2006年12月いっぱいまでってことだよね。まあそんな感じかなーとは思っていたけど、やっぱり遅そうだなー。
Zend Frameworkをどう使うか その2
さて、次はZend_View。Controllerが比較的薄くて差し替え可能なクラスを積み重ねている感じだったんで、Viewも似たような感じかなーと思ったら、Viewの方はZend_View自体が結構機能を持っているな。
基本機能としては、
$view = new Zend_View();
$view->setScriptPath($templateDir);
$view->assign('foo', 'FOO VALUE');
echo $view->render($templateFile);
みたいな感じか。ただしassignの代わりに__setと__getをつかって、
$view->foo = 'FOO VALUE';
みたいな記法もサポートしている(内部的にはassignするのと同じく、$this->_varsにセットされる)し、連想配列とかstdclassのプロパティとかを使ってassignしてもOK。まあこの辺はPHPのテンプレート展開処理で使われるいろんな方法は一通りサポートって感じか。
で、標準のテンプレート展開処理(=PHPのコードをincludeするだけ)では、
<?php echo $this->foo; ?>
で、セットした'FOO VALUE'が出力される(エスケープされない)。
で、escapeは特別なメソッドとして用意されていて、デフォルトでは
$this->_escape = 'htmlspecialchars';
というコールバックメソッドが登録されていて、
<?php echo $this->escape($this->foo); ?>
とすると、$this->escape()の実体としてhtmlspecialcharsが呼ばれて、エスケープ出力される。これは差し替え可能なコールバックメソッドなんで、
$view->setEscape('another_escape_function');
とかすれば、$this->escape()で別のエスケープ処理が使われるようになる。けど、なんでescapeを特別扱いしているのか、いまいち理由が分からない。
というのは、Zend_Viewでは汎用的な機能拡張方法として、helperってプラグインみたいな口が用意されていて、addHelperPathとかで指定したhelper用ディレクトリに、独自のhelperクラス(たとえばMyHelper.phpとか)を用意しておけば、 $view->myHelper()なんて感じでその処理を呼び出すことができるようになっている。escapeもそれで十分表現できるんだし、それでいいんじゃないのかなー(ただ、現状のZend_View_Abstract::_loadClassでthrow new Zend_View_Exceptionしている場所がおかしいんで、helperの検索が正しく働かない気がする。と思ったらMLで報告されていた)。
ちなみに標準のhelperとしては、FormButtonとか主にフォーム部品用のhelperが用意されている模様。ZFormとは違って、こっちはちゃんと出力は必要に応じてエスケープされるようになってるな。
あと、出力結果全体にかかるfilterもセットできるようになっていて、helperと同じようにaddFilterPathとかでfilter用ディレクトリをセットしてから、$view->addFilter('filter_name')とかすると、filterが追加される。filterは複数セットすることができ、renderの最後でまとめてかけられるようになっている。
と書きながらZend_View_Abstract::renderを読んでいたら、単純にレンダリングしているだけでなく、なんか変なスタックを積んでいるな。これって特にマニュアルには記述がないみたいだけど、renderの中でさらにrenderできるようになっているっぽい。
$view->render('foo.php');
とかやっておいて、foo.phpの中で、
<?php $this->render('bar.php'); ?>
<?php $this->render('baz.php'); ?>
とかやった場合に、正しくレンダリングされる(最初のfoo.phpのrenderが終わった段階で、まとめて出力が返される)ようになっている(気がする。動作確認してないけど)。一応テンプレートを複数のパーツに分割して管理することを意識しているのね。
まあ標準のZend_View(=テンプレートはViewオブジェクトのスコープで動作するPHPのコードをそのまま書く)で使う場合は、それはそれで結構便利そう。ただ、今まで他のテンプレートエンジン(というかSmarty)を使っていた人はどうするべきだろう。いくらZend_Viewがそれなりに高機能で拡張性があっても、今更<?php echo $this->escape($this->foo); ?>なんて記述はしたくないなー。
といったところで、また調査に戻る。
Zend Frameworkをどう使うか
PHP5への移行を決意したのはZend Frameworkの存在も大きいわけだが、Zend Frameworkがどのくらいで実用レベルに達するのか、現時点では全然見通しが立っていない。少なくとも半年程度は待たなければならないんじゃないかと思われるんで、ここ一、二ヶ月程度のスパンではZend Frameworkの存在を意識しつつも、Zend Frameworkにあまり依存しないようなアプローチでPHP5対応を行う必要があるだろう。
というわけで、いつのまにかPreview 0.1.2まで出ていたZend Frameworkを真面目にチェック。フレームワークのコアとなるControllerとViewまわりがどんな感じなのかについては、php architectのtutorialが一番わかりやすいドキュメントだろう。チュートリアルなんで内部構造にはほとんど触れず、使い方のレベルの説明だけど。
で、内部構造の方は、マニュアルのControllerパートとソースを読む限りでは、結構柔軟に差し替え可能な模様。
$controller = Zend_Controller_Front::getInstance(); $controller->setRouter(new MyRouter()); $controller->setDispatcher(new MyDispatcher()); $controller->setControllerDirectory($controllerDirectory);
みたいな感じで、Router(URIからAction名を解決する)やDispatcher(Routerで解決されたAction名から、Actionオブジェクトを生成し、実行する)は差し替えることができる。
あと、
$controller->registerPlugin(new MyPlugin());
とかやると、プラグインが登録できるけど、マニュアルにはプラグインに関する記述がない。ただ、Zend_Controller_Front::dispatchと、Zend_Controller_Plugin_Interfaceを見ればだいたい挙動がわかる。各プラグインには、$controller->dispatch()中に発生する下記のイベントに対応するハンドラーメソッドを書いておき、対応したハンドラーがあればそのプラグインが実行される(なければスルー)、って感じだろう。
- routeStartup() - $controller->getRouter()->route()前。1リクエストごとの前処理用プラグインで使うんだろう。認証とかもこの辺にかませるのが基本かな。
- routeShutdown($action) - $controller->getRouter()->route()後。デフォルトのrouteで解決したActionを、後付けで書き換えたりセットアップ処理を追加したりするときに使うのかな?
- $action = dispatchLoopStartup($action) - Actionのdispatch処理は、戻り値として次のActionを返すことにより、複数のActionの連鎖実行(dispatchLoop)できる。そのdispatchLoop前。最初に実行するActionが解決したあとの前処理用か。
- $action = preDispatch($action) - Actionのdispatch前。dispatchLoop内で実行される一つのActionごとに呼ばれる。最初に実行するかどうかに限らず、特定のActionに対する前処理。
- $action = postDispatch($action) - Actionのdispatch後。dispatchLoop内で実行される一つのActionごとに呼ばれる。preDispatchに対して、特定のActionに対する後処理用かと思いきや、この段階では$actionは次のActionに書き換えられているはずだから、plugin内で状態を保持していない限りは、次のActionの前処理と大して変わらないことしかできないな。ってことは、特定のActionに対する前処理後処理を書きたかったら、一つのプラグインでpreDispatchとpostDispatchを処理しつつ、プラグイン側で状態を保持する必要がありそう。
- dispatchLoopShutdown() - dispatchLoop後。全体の後処理。ただし、途中でdieしたりするパターンは(少なくとも正常ルートでは)ないんだろうか? あったら使える状況は限られてしまいそうだけど。
基本的な処理パターンを、Controller、Router、Dispatcher、各Actionに記述しつつ、パターンをまたがった処理はプラグインで書いていく感じかな。なんかプラグインって名前がちょっと違和感を感じるけれども、使い勝手と柔軟性はなかなか高そう。認証とかのクラスを基本的な処理パターンに密結合させると、一気に柔軟性が失われたりするものだけど、そのへんはプラグインに出してしまおうってことなんだろう。
ただ、そうなると認証とかの状態維持はZend::registerあたりを使ってねって話なのかなー。あれはちょっと汎用的な入れ物すぎるけど、$controllerが保持するくらいだったらZend::registerでも大して変わらないしなー。かといってActionのインスタンスは実行する瞬間しか存在しないから、Actionをまたがった入れ物としては使えないし。プラグインが状態を保持するというアプローチはありかもしれないけど、プラグインのインスタンスへの直接アクセス方法がないから、いまいち使い勝手が良くない気もする。
というところで疲れてきたので休憩。
