Zend Frameworkをどう使うか その8
さっきのテンプレート展開の例で<?=h($foo); ?>みたいな記述を実現するために、
function showTemplate($_templateFile, $_templateVars)
{
extract($_templateVars);
include $_templateFile;
}
みたいな関数を用意していた。同じようなことがZend_Viewでもできれば、
<?php echo $this->escape($this->foo); ?>
は、
<?php echo $this->escape($foo); ?>
と書けるようになる。$this->の連続がなくなってずいぶん見通しが良くなった気がする。この程度ならば、Zend_View_Abstractを継承した、
class MyView extends Zend_View_Abstract
{
protected function _run()
{
extract($this->_vars);
include func_get_arg(0);
}
}
とか作れば簡単に実現できるじゃんと思ったんだけど、ダメだった。というのは、$this->_varsってZend_View_Abstractでprivate宣言されているのね。せめてprotectedにならんかなー。あるいはZend_View_Abstractに、
public function getAllVars()
{
return $this->_vars;
}
なんてメソッドを追加してもらえないだろうか。っつーかなんでDmytro Shteflyuk’s Home » Zend Framework: Using Smarty as template engineのコメント欄で紹介されていたSmarty用のViewがわざわざassignをoverrideしていたのか、ようやくわかったよ。
この仕様ってどうなんだろうなー。継承されたクラスからも、テンプレート変数一覧には直接触らせないようにしなければならないだろうか? 単に「protectedにする理由がないからprivate」にしただけならば、protectedにして欲しいなー。そうすれば、もっとまっとうなSmarty用のViewを書くこともできそうだし。
まあ最悪、Zend_View_Abstractの$this->_varsおよびその関連メソッドを丸ごとMyViewで上書きしちゃえば、俺のやりたいことはできるようになるだろうけど、できればそういうアプローチ自体を公式に意識した作りになっていて欲しい。という話はZend FrameworkのMLに投げなきゃだめなのか。英語で議論するのめんどいなー。
MLの敷居の高さは
英語云々と言うよりは、ML投稿者の誰が誰やらさっぱり分からないことだな。Zend Frameworkのソースには開発者の名前が書かれていないし、MLに発言している人も特に名乗ったりしていない。また、関係者だと分かるような署名をつけている人も見あたらない。
この状態だと、どの発言がどういう立場でのものか分からないんで、どういうアプローチで発言していいのかさっぱり分からない。たとえば、Re: [fw-general] Smarty Plugin for viewで書かれているZend_Viewをもっと抽象化して他のテンプレートエンジンに対応できるようにしよう、なんて案には賛同する*1わけだけど、人間関係的な文脈が読めないんで、どうアプローチしていいのかさっぱりわからない。ひとまず各投稿者の名前とかでググって、どういう人がどういう立場でどういう発言をしているのか把握するところからはじめないとダメなのかな。
*1 けど、俺の場合ははそこまできれいなアプローチにしなくても、単に$_varsをprotectedしてくれるだけで十分なんだけどね
Zend Frameworkをどう使うか その7
ひとまず、その他のテンプレートエンジンを使わず、お作法に則ってZend_Viewを使うアプローチについて考えてみる。Zend Frameworkの設計は筋がいいんで、アプリケーションの処理の流れに関するコードはきれいに記述できるし、PHP言語自体テンプレート記法みたいなもんだと考えれば、HTMLテンプレート(Zend_Viewでrenderするファイル)もきちんとロジックと分離して管理できる。
けど、やっぱり<?php echo $this->escape($this->foo); ?>という記法は我慢できない。書籍でも上記のような記法は我慢できなくて、ショートタグを有効にして、<?=h($foo) ?>*1みたいな記述を採用したし、ショートタグが使えない場合の代替手段としても、<?php eh($foo); ?>*2を採用した。単にテンプレート変数を展開(エスケープ付き)するだけなら、この程度の入力しやすさ&可読性の良さを期待したい。
では、どうやってZend_Viewをベースに上記のような入力しやすさ&可読性の良さを獲得するか。アプローチはいろいろ考えつくけれども、ひとまずZend_Viewを拡張するまっとうな手段であるhelperを使って書いてみることにする。
といっても、helperは単にZend_Viewの中から$this->[helperName]で呼び出すことができるという以上の機能は持ち合わせていないんで、escapeのショートカット関数を定義するくらいにしか使えそうにない。
class Zend_View_Helper_E
{
public function E($value)
{
return htmlspecialchars($value);
}
}
なんてhelperを書いて、$view->setHelperPath('/path/to/helpers')してから、
<?php echo $this->e($this->foo); ?>
する程度が関の山か。ちょっとは短くなったけど、可読性が高いとはとても言えない。あるいは、
class Zend_View_Helper_E
{
public function E($value)
{
echo htmlspecialchars($value);
}
}
としちゃって、
<?php $this->e($this->foo); ?>
とすればもっと短くなるけど、これだとテンプレート変数を出力しているように見えなくなっちゃうんで、短くなっても可読性は落ちてる気がするしなー。まあ慣れればこれでもいいかもしれないけど。
と試してみてやっぱり、Zend_Viewがescapeを特別扱いしている意味が感じられないよなーと再確認した。escape用のhelperを標準で用意しておけばそれでいいじゃん。helperの書き換えもaddHelperPathして上書きすればいいわけだし*3。Zend_Viewがescapeを特別扱いしているのは、「escape重要!」というポリシーを知らしめるためとか、helperよりも汎用性の低い実装にすることで多少のパフォーマンス上の利点があること、くらいしか思いつかない。
Zend Frameworkをどう使うか その6
さて続いては、一番いじりがいがあって、しかも今後インターフェースが変わりそうな気配が濃厚なZend_View周りをいじってみよう。まずは、 Zend Frameworkをどう使うか その2で予想した、Zend_View::render内で$this->renderした場合に、正しくレンダリングされるかどうかのテスト。
$view = new Zend_View();
$view->setScriptPath('/path/to/views');
$view->foo = 'FOO';
$view->render('index.php');
なんて感じで呼び出し、index.phpの方で、
<p>header</p>
<?php $this->render('body.php'); ?>
<p>footer</p>
なんて書いて、body.phpでは、
<p>foo value is <?php echo $this->escape($this->foo); ?></p>
と書くと、
<p>header</p> <p>foo value is FOO</p> <p>footer</p>
と出力が得られた。ちゃんと入れ子のrenderも実行してくれるみたいだね。ちなみにindex.phpでは、
<p>header</p>
<?php echo $this->render('body.php'); ?>
<p>footer</p>
とrenderの戻り値をechoするようにしてもしなくても結果は同じ。というのは、Zend_View::renderは入れ子の中で呼ばれた場合は、出力結果を文字列で返さず、親のrenderでob_startされた出力バッファリングにくっつけて出力し、親のrenderが終わった段階でまとめて結果を返すようになっているから。というわけで利用者レベルではechoをつけてもつけなくてもいいわけだけど、開発者はどっちの記述法を基本にするつもりなんだろうな。まあ大した問題ではないけど。
ちなみに上記サンプルにもあるとおり、Viewにセットした値は入れ子の中のrenderで呼ばれたテンプレート(PHPコード)の中でもふつうに使える。というか、renderが入れ子になっていても、呼び出すViewオブジェクト自体は変わらない=renderされるPHPコードのスコープは変わらないから、まあ当たり前。もちろん、
<?php $view = new Zend_View(); $view->render('foo.php'); ?>
とか明示的に別のViewオブジェクトを作ったら、別のスコープでレンダリングされるけどね。パーツをレンダリングするときに、使える(見える)値を制限したい場合は、そういう使い方もあるかも。
[Zend Framework: Zend Frameworkをどう使うか その5
プラグインが考えたとおりに動くかどうかテスト。
<?php
Zend::loadClass('Zend_Controller_Plugin_Abstract');
class AuthPlugin extends Zend_Controller_Plugin_Abstract
{
public function routeShutdown($action)
{
if (!session_id()) {session_start();}
if ($this->_isLogin()) {return $action;}
$action->setControllerName('login');
$action->setActionName('index');
return $action;
}
private function _isLogin()
{
if (isset($_SESSION['login'])) {return true;}
if (($_POST['id'] == 'testid') && ($_POST['pwd'] == 'testpwd')) {
$_SESSION['login'] = true;
return true;
}
return false;
}
}
?>
な感じで認証プラグインもどきを作る。routeStartupではActionが書き換えられないんで、routeShutdownの方をフック。上のコードでは、元のActionを活かして、Controller名とAction名を認証ページに書き換えているけど、こうやって全然違うActionにマップし直す場合は、
return new Zend_Controller_Dispatcher_Token([CONTROLLER], [ACTION]);
という風に、本当はToken自体を作り直した方がいいよね。でもまあそのままにしておこう。で、FrontControllerを作るときに、
Zend::loadClass('Zend_Controller_Front');
$controller = Zend_Controller_Front::getInstance();
$controller->setControllerDirectory('/path/to/controllers');
$controller->registerPlugin(new AuthPlugin());
$controller->dispatch();
なんて感じで、プラグインを登録してからdispatchする。すると、どのActionが指定された場合も、必ず事前にAuthPluginが呼ばれるようになり、認証に通っていなかったら強制的にActionはLoginController::indexActionに差し替えられるようになる。LoginControllerは
Zend::loadClass('Zend_Controller_Action');
class LoginController extends Zend_Controller_Action
{
public function indexAction()
{
Zend::loadClass('Zend_View');
$view = new Zend_View();
$view->setScriptPath('/path/to/views');
echo $view->render('login.php');
}
public function noRouteAction()
{
echo get_class($this) . '->' . 'noRouteAction';
var_dump($this->_getAllParams());
}
}
な感じで、login.phpは、
<form method="post"> ID: <input type="text" name="id" /><br /> Password: <input type="password" name="pwd" /><br /> <input type="submit" value="login" /> </form>
な感じ。これでプラグインを使った認証処理が実現できる。ただ、ここでは単に認証を通すだけだけれども、ふつうのアプリケーションならば認証されたユーザー固有の状態を保持・参照できるようにする必要がある。そういう機能を受け持つものはZend Frameworkには見あたらないっぽいんで、自前でセッションにユーザークラスとかを入れておくとか、Zend::registryあたりで持ち回すとかする必要があるだろう。
Zend Frameworkをどう使うか その4
Zend Frameworkの感触だけ確かめるつもりだったんだけど、思ったよりも深追いしすぎている気がするな。でもまあいずれやらなきゃならないことだから、今やっておいても無駄にはなるまい。
で、さすがにそろそろコードを追うだけじゃなくて、実際に動かしてみようってことで、サンプルを作って動かしてみた。ただ、標準のmod_rewriteを前提としたRouterだと気軽にテストできないんで、
<?php
Zend::loadInterface('Zend_Controller_Router_Interface');
class MyRouter implements Zend_Controller_Router_Interface
{
public function route(Zend_Controller_Dispatcher_Interface $dispatcher)
{
$path = $_SERVER['PATH_INFO'];
$path = explode('/', trim($path, '/'));
$controller = $path[0];
$action = isset($path[1]) ? $path[1] : null;
if (!strlen($controller)) {
$controller = 'index';
$action = 'index';
}
$params = array();
for ($i=2; $i<sizeof($path); $i=$i+2) {
$params[$path[$i]] = isset($path[$i+1]) ? $path[$i+1] : null;
}
$actionObj = new Zend_Controller_Dispatcher_Token($controller, $action, $params);
if (!$dispatcher->isDispatchable($actionObj)) {
throw new Zend_Controller_Router_Exception('Request could not be mapped to a route.');
} else {
return $actionObj;
}
}
}
?>
みたいにPATH_INFOからActionを解決するようにしたRouter(上記ソースは、REQUEST_URIの代わりにPATH_INFOを使うようにした以外は、Zend_Controller_Routerとほとんど同じ)を用意しておいて、
Zend::loadClass('Zend_Controller_Front');
$controller = Zend_Controller_Front::getInstance();
$controller->setRouter(new MyRouter());
$controller->setControllerDirectory('/path/to/app');
$controller->dispatch();
という風にRouterを差し替えて動かす。すると、
http://example.com/path/to/sample.php/[CONTROLLER]/[ACTION]
なんて感じでアクセスできるようになる。あるいはaction=[CONTROLLER]/[ACTION]とか、module=[CONTROLLER]&action=[ACTION]とかのQUERY_STRINGから解決する方がテスト用にはいいのかな。
まあそんな風にして動作させたところ、思ったような感じで動作してくれているんで、今までソースとマニュアルを読んだだけで理解してきたことは、特に大きくは外してはいない模様。
というわけで、しばらく実際にいろいろ動かして試してみることにする。