2006-03-03 [長年日記]
_ 次世代のフォーム処理
Ajaxを利用することで、Webアプリのフォーム回りの処理もいろいろできるようになるわけだけど、その処理が主機能に関連するものの場合は、そう簡単にJavaScript必須にするわけにもいかない。
だから今後のフォーム回りの設計については、基本的にJavaScriptなしでもフル機能動作するフォーム設計を行った上で、もしもJavaScriptが利用できる場合はより快適に動作する、といった方針で作っていくことを考えていた。
基本的にはonSubmitをフックして、JavaScriptで対応できる処理の場合はJavaScript側で処理を代行した上で、return false;するようなアプローチね。
で、実際にそういう高機能なフォームを書いてみようと試してみたところ、すぐに手が止まってしまった。具体的にどう書くという方針が全然練れていなかったらしい。
というか、JavaScriptなしで完全に動作するフォームの上に、JavaScriptを使うとより快適に動作するフォーム機能を載せようというアプローチ自体に無理があるのかな。もちろんそういうアプローチが可能な場合もあるけれども、そういうアプローチが可能なケースはかなりレアな気もしてきた。
素直に、JavaScriptなし版のフォームとJavaScript必須なフォームを、それぞれ別に作った方がいいのかもしれない。この辺を追求してみた人っているのかな?
2006-03-07 [長年日記]
_ ちょっとましになったかな
先週の金曜日から昨日あたりまでなかなかすごい感じで、ちゃんと朝晩薬を飲んでいても、クスリが切れかかる時間帯(朝起きてすぐと夕方過ぎくらい)にはちょっとキていたんですが、今日はずいぶん楽になった気がする。このくらいなら(クスリが効いている間は)マスクなしでも大丈夫そう。
_ smarty_modifier_json
今後JSONを使う機会は多くなるだろうけど、JSONライブラリのどれを使うべきか決め手に欠ける。あと、「サーバーとクライアントでテンプレートを共有したい場合」なんてことも考えつつ、
function smarty_modifier_json($var)
{
require_once 'HTML/AJAX.php';
$jsonSerializer =& new HTML_AJAX_Serializer_JSON();
$json = $jsonSerializer->serialize($var);
return $json;
}
のようなSmartyプラグインを用意して、
{if $mode == 'json"}
{$data|json}
{else}
{!-- $dataをHTMLとして展開するコード --}
{/if}
なんて書くようにしておくと、JSONライブラリを差し替えるのも比較的楽かも。将来性を考えると、Zend Frameworkに入っているZend_Json_Encoderあたりが良さそうかなー。
あとsmarty_modifier_jsonの中で
header('content-type: text/javascript; charset=utf-8');
までやっちゃうと副作用が大きすぎるかな?
そういや
{if $mode == 'php'}
{$data|serialize}
とかも一応対応しておいた方がいいのかな? この前ちょっと話題になっていたよね。
2006-03-08 [長年日記]
_ 次世代のフォーム処理 その2
Zend Frameworkのプレビューリリースに同梱されているZFormっていうHTML_QuickFormの後継になりそうなフォーム処理クラス(まだ_HIGHLY EXPERIMENAL_だそうだ)が、Ajax対応を試みている模様。もともとHTML_QuickFormはサーバーとクライアント(JavaScript)両対応のバリデーション機能を持っていたけど、さらにAjaxにも透過的に対応させようとしているっぽい。
あいにく同梱されているZFormは、まともに動く状態じゃない(関連ライブラリのディレクトリ構成が現状とは違っているし、requireしている関連ファイルも存在していなかったりする)けど、アプローチ自体は何となく読み取れた。
- すべてのフォーム要素(form自身も)はZFormElementを継承する。
- ZFormElementのbehaviorとして、ZFormAjaxBehaviorを追加することによって、その要素に対してAjaxによる拡張を行う
- ZFormAjaxBehaviorでは、要素をレンダリングする際に、その要素専用のイベントハンドラー(JavaScript関数)を出力し、指定されたイベント(デフォルトではonclick)とイベントハンドラーを結びつける
- イベントハンドラーが呼ばれると、ZFormAjaxBehaviorによって指定されたXmlHttpRequestリクエストを呼び出す
- ZFormElementがフォーム自身だった場合は、フォームのすべての要素の値をパラメータとして使う
- ZFormElementがフォーム要素の一つだった場合は、その要素の値をパラメータとして使う
- XmlHttpRequestリクエストが正常終了したら、指定されたコールバック関数が呼ばれる
で、指定されたコールバック関数ってのは特に自動生成される対象ではなさそう(生成する処理を見つけられてないだけかもしれないけど)なんで、そこはJavaScriptレベルで独自に記述しておいてね、って感じなんだろう。
具体的な使い方の例がないんだけど、雰囲気的には、
<script type="text/javascript">
function myFormOnSubmit(req)
{
//XmlHttpRequestリクエストが成功したときの処理を書く
}
function myInputOnChange(req)
{
//XmlHttpRequestリクエストが成功したときの処理を書く
}
</script>
<?php
$form = ZFormFactory::loadElement('ZForm', 'MyForm'); //フォーム
$behavior = new ZFormAjaxBehavior(
$form,
'MyForm',
'/path/to/api', //XmlHTTPRequestの呼び出し先URL
'myFormOnSubmit', //callback
true, //非同期
'POST',
'submit' //submitイベントをフック
);
$form->addBehavior($behavior);
// 以下フォーム要素を追加
$element = ZFormFactory::loadElement('ZFormInputElement', 'MyInputElement');
$elementBehavior = new ZFormAjaxBehavior(
$element,
'MyInputElement',
'/path/to/api', //XmlHTTPRequestの呼び出し先URL
'myInputOnChange', //callback
true, //非同期
'POST',
'change' //changeイベントをフック
);
$element->addBehavior($elementBehavior);
$form->appendChild($element);
$form->render();
?>
みたいな感じになるのかなー。肝心のZFormElementBehaviorが同梱されていないんで、behaviorの登録の仕方がいまいちわからないけど、まあきっとこんな感じになるんだろう。ちなみにMyFormの方ではフォームのPOST処理自体をAjaxで代替するイメージで、MyInputElementの方は一文字ごとの文字チェックとか入力候補表示とかを行うイメージね。
確かにこのやり方だったら、フォームを構成するすべての要素のすべてのイベントに、独自のイベントハンドラーを用意できるし、JavaScriptレベルで、定型の呼び出し処理を自分で書かなくてすみ、コア機能を実装するためのイベントハンドラーの中身だけを書けばよくなる。方向性としては悪くはない。
ただ、ZAjaxとかいう独自のJavaScriptライブラリ(あまり完成度が高くない。というか、必要最低限しかまだ実装されていないっぽい)を使っているあたりは、大きなマイナスだ。この辺はもうprototype.jsあたりを採用して欲しいよなー。
あと、HTML_QuickFormもそうだったけど、こうやってコードでフォームを組み立てるやり方は、結構だるくて自由度が低かったりするんで、本格的にこれでいけるのかがちょっと微妙。HTML_QuickFormの場合はSmartyRendererとかを使えば、ある程度出力をいじれたけど、ZFormElementはrenderで直接echoしちゃってたりして、その辺の融通が利きにくそうだし。
「XmlHttpRequestリクエスト送信処理は自動生成し、送信処理後のコールバックだけを記述すればいい」という方針をパクった上で、prototype.jsと組み合わせたやり方を別途考えていった方がいいかなー。ZFormの今後に期待するという手もあるけど、方針が確定して安定していくまでまだまだ時間がかかりそうだしなー。
あと実際に使ってみないと分からないところだけど、XmlHttpRequestリクエストが成功した後のコールバックだけで、やりたいことは一通り表現できるんだろうか? なんとなく、XmlHttpRequestリクエストを送る前のコールバックも登録できるようにしておいた方が、良さそうな気がする。
function myFormOnSubmiting(params)
{
//XmlHttpRequestリクエスト送信前に実行する処理を記述する
}
とかやっておいて、この関数の戻り値によっては、XmlHttpRequestリクエストの送信自体をキャンセルしたりとか。まあこの辺は「あったほうがいい」とか言い出すと複雑になる一方なんで、切り捨てることのメリットの方が大きかったのかもしれないけど。
_ Zend Frameworkにはもうちょっと気を遣って欲しかった
なんかZend Frameworkの構成コードやサンプルなんかも、全然セキュリティには気を遣ってないっぽいなー。まだプレビューリリースってことを差し引いたとしても、根本的にセキュリティっつーか状況に応じたエスケープという概念を省略しまくりなのが、ちょっときつい。
どうせなら標準関数として、
function htmlprintf()
{
$args = func_get_args();
$format = array_shift($args);
foreach ($args as $index => $value) {
$args[$index] = htmlspecialchars($value);
}
array_unshift($args, $format);
return call_user_func_array('sprintf', $args);
}
function sqlprintf()
{
$args = func_get_args();
$format = array_shift($args);
foreach ($args as $index => $value) {
$args[$index] = addslashes($value);
}
array_unshift($args, $format);
return call_user_func_array('sprintf', $args);
}
$html = '<span>%s</span><span>%d</span>';
echo htmlprintf($html, '<>"&', '345.3') . "\n";
echo htmlprintf($html, '234', 'adsfdas') . "\n";
$sql = 'select * from test where string = \'%s\' and number = %d';
echo sqlprintf($sql, '\'foo', '345.3') . "\n";
echo sqlprintf($sql, '234', 'adsfdas') . "\n";
C:\php>cli\php test.php <span><>"&</span><span>345</span> <span>234</span><span>0</span> select * from test where string = '\'foo' and number = 345 select * from test where string = '234' and number = 0
みたいな関数を用意しておけば、ずいぶんましになるのかなー。sqlprintfの方はもっとちゃんと作る必要がある(DB関連関数の呼び出しをフックして、最後に呼び出したDB関数用のエスケープを実行するようにする、とか)けど、これでもないよりはずいぶんましって気がする。
2006-03-09 [長年日記]
_ 次世代のフォーム処理 その3
ZFormが使っていたアプローチをHTML_QuickFormに適用できないか、試しに検討。HTML_QuickForm関連クラス自体を書き直すのではなく、外付けで拡張する方法で。
外付けで拡張するとなると、HTML_QuickForm_Elementたちをいちいち拡張するわけにはいかない*1から、できるのはせいぜいHTML_QuickForm本体を拡張したクラスを作って、従来の各関係クラスの機能やインターフェースとの互換性を確保したまま、Ajax対応の仕組みを追加するアプローチだろう。
HTML_QuickForm_Rendererを拡張しちゃえば、出力部分はかなり大幅にいじれるけど、Rendererは基底クラスは単なるインターフェース定義で、実装は各派生クラスでやっちゃってるから、一カ所直せば済むって話にはならなそうだ。手を出さない方が無難だろう*2。
となると、HTML_QuickFormにAjax関連の設定を追加するためのメソッドを追加し、HTML_QuickForm::acceptあたりを互換性を持ったまま書き換えて、追加されたAjax関連の設定を見ながら、必要なJavaScriptコードを吐き出す感じかなー。その際には各elementがIDを持っていない場合は、IDを振る仕組みとかも必要そうだ。イベントハンドラー自体は直接element出力時に出力しなくても、IDさえ分かるなら後付けでEvent.observeできる。
というわけで、まとめてみる。
- HTML_QuickFormを拡張したHTML_QuickForm_Exを作る。$form =& new HTML_QuickForm()の部分を$form =& new HTML_QuickForm_Ex()に変えるだけで、従来のHTML_QuickFormを使っていたコードは完全に動作するようにする。
- HTML_QuickForm_Ex::addAjaxHandler(mixed $element, string $event, mixed $callback, array $ajaxOptions)を追加し、各element($element == __ALL__の場合はform自体)にAjax拡張用の情報を追加する。
- コールバックの実行をXmlHttpRequestがonCompleteな場合のみ対応するのならば、このようなメソッドでいいけど、コールバックのパターンを増やしたいならば、インターフェースをもっと練る必要がある
- HTML_QuickForm_Ex::removeAjaxHandler(mixed $element, string $event, mixed callback)も用意しておいた方がいいだろう
- HTML_QuickForm_Ex::accept(&$renderer)で、HTML_QuickForm::accept相当の機能を実行した後に、必要なJavaScriptコードを出力する機能を追加する
- Renderer_Defaultの場合は、$renderer->_hiddenHtmlにJavaScriptコードを追加すればいいだろう(JavaScriptの実行順序を考えると、$renderer->_htmlにコードを追加する必要があるかも。いや、$render->_hiddenHtmlにすべて出力した上で、onloadで初期化コードを呼ぶようにすれば問題ないかな?)。
- toArrayなRendererの場合は、$renderer->_ary['javascript']にJavaScriptコードを追加すればいいかな。初期化周りの問題は上と同じ。
- toObjectは使ったことないんで(Flexyは使ってない)よくわからないけど、多分toArrayな場合と同様に、$renderer->_obj->javascriptにJavaScriptコードを追加すればいいんだよね?
- その他のRendererは使わないんでパス。JavaScriptコードさえ生成できていれば、出力を追加するのは簡単だろう。
- その他懸案事項
- Rendererに割り込むあたりは、外部オブジェクトのプライベート変数を外からばりばり書き換えるアプローチなんで、将来の互換性は厳しい。
- javascript側のコードを書いて、考えていたような処理がJavaScriptでできるかどうか試してみないとな。まだ単純なprototype.jsのテストコードしか書いたことがない。
- HTML_QuickFormが出力するJavaScriptバリデーションコードと、主に実行順序的にバッティングしないかどうか検証が必要。確かあれはformのonsubmitに直接イベントハンドラーを書いていたはずだけど、prototype.jsでEvent.observeした場合は、どういう実行順序になるんだろう? あるいは上書き?
- そういやコールバックをどうやって記述するか全然考えてない。多分普段はSmartyと組み合わせて使うだろうから、Smartyテンプレート側に記述するのが無難? あるいはコールバック関数の内容自体もここで登録できるようにする方がいい?
- elementにIDがない場合にIDを振る処理は、acceptの最初にやるべきか。自動生成したUNIQUE PREFIX+element名とかにIDを固定しちゃった方が扱いは楽なんだけどな。
- どうせprototype.jsを使うんだったら、Ajax.Updaterとかも使いたくなりそうだけど、仕組みが複雑になりすぎるかな?
_ 次世代のフォーム処理 その4
いや、よく考えたらこの仕組みはフォームに限定する必要がまったくないな。基本的にDOM要素のIDとイベント名が分かれば、イベントハンドラーの登録はできるわけで、フォーム生成ライブラリと連携しなければならない必然性は非常に薄い。
対象のDOM要素がフォームまたはフォーム要素じゃない場合は、XmlHttpRequestで送るパラメータを何にするかが未確定だけど、逆に言うと、渡されたDOM要素の種類がフォームまたはフォーム要素ならば、送るパラメータを自動で特定できるわけだ。
となってくると、HTML_QuickFormを拡張するとか考えずに、独立したライブラリとしてHTML_AjaxHandlerとかを作った方がいいかもしれない。
class HTML_AjaxHandler
{
var $_handlers = array();
function addHandler($domId, $eventName, $callbacks, $ajaxOptions)
{}
function removeHandler($domId, $eventName, $callbacks)
{}
function toJavaScript()
{}
}
とかで十分いけるかな? この程度だったら、コールバックのタイミングを複数対応にしたりしても、全体としてはさほど複雑にならないだろうから、その辺までやっちゃえるか。
2006-03-10 [長年日記]
_ HTML_AjaxHandlerのプロトタイプ
昨日言っていたHTML_AjaxHandlerのプロトタイプ版を作ってみた。
デモのソースは複数のデモをまとめて書いてあって読みにくいんで、2行目に表示されている平方根の計算に関連する部分だけ抜き出して、解説してみる。
デモの内容としては、テキストボックスに数字を入れると、その右側にリアルタイムで平方根の計算結果が表示される、というもの。平方根の計算はサーバーサイドで行い、XmlHttpRequestでデータをやりとりしている。Ajaxのデモなんで、平方根なんてJavaScriptで計算すればいいじゃん、ってツッコミはなしね。
HTMLとして必要な要素は、以下。
<script src="prototype.js" type="text/javascript"></script> 平方根: <input type="text" name="rootnum" id="rootnum" value="" /> = <span id="rootnum_result"></span>
ここでは特にイベントハンドラー等は定義していないし、prototype.jsを読み込んでいる以外は、JavaScriptコードは書いていない。
続いて、PHPコード部分は、以下。
require_once 'HTML/AjaxHandler.php'; $ajax =& new HTML_AjaxHandler(); $ajax->addHandler( 'rootnum', 'keyup', array( 'url' => 'api.php', 'method' => 'get', 'parameters' => 'command=root', 'container' => 'rootnum_result', ) ); echo $ajax->toJavaScript();
これでイベントハンドラーの定義を行っている。
HTML_AjaxHandler::addHandlerの引数は、
- $elementId - 対象となる要素のID
- $eventName - ハンドリングするイベント(prototype.jsの仕様に合わせる)
- $ajaxOptions - その他オプションいろいろ
となっている。$ajaxOptionsに関しては、基本的にprototype.jsのAjax.Request、Ajax.Updater、Ajax.PeriodicalUpdaterのオプション(およびパラメータ)を受け付ける。それ以外にも、HTML_AjaxHandler独自のオプションをいくつか受け付ける。
上記の例だと、以下のような感じ。
- url - XmlHttpRequestするURL。
- method - リクエストメソッド。
- parameters - 初期パラメータ。対象の要素がフォームやフォーム要素の場合は、その内容も自動的に追加される。
- container - Ajax.Updaterで更新される要素のID。containerが指定されている場合、Ajax.Requestではなく、Ajax.Updaterが呼ばれることになる。containerに加えてfrequencyが指定されている場合は、Ajax.PeriodicalUpdaterが呼ばれることになる。
で、最後のecho $ajax->toJavaScript()で、実際のイベントハンドラー等のJavaScriptコードを自動生成して出力している。どういうJavaScriptコードかというと、以下のような感じ。
<script type="text/javascript">
<!--
function AjaxHandler_autoParameters(e, params)
{
if (params == undefined) {params = '';}
if ($(e).value != undefined) {
params += (params != '' ? '&' : '') + Form.Element.serialize($(e));
} else if ($(e).tagName.toLowerCase() == 'form') {
params += (params != '' ? '&' : '') + Form.serialize($(e));
}
return params;
}
function AjaxHandler_autocreated_rootnum_keyup(event)
{
var url = 'api.php';
var options = {
method: 'get',
parameters: 'command=root',
asynchronous: true,
container: 'rootnum_result',
};
options['parameters'] = AjaxHandler_autoParameters('rootnum', options['parameters']);
var ajaxRequest = new Ajax.Updater(
'rootnum_result',
url,
options
);
Event.stop(event);
}
function AjaxHandler_autocreated_441130d9062b1_onInit()
{
Event.observe('rootnum', 'keyup', AjaxHandler_autocreated_rootnum_keyup, false);
}
Event.observe(window, 'load', AjaxHandler_autocreated_441130d9062b1_onInit, false);
-->
</script>
その他のデモは、
- load時にAjax.PeriodicalUpdaterに登録して、現在時刻をAjaxで取得&表示する。loadの処理が微妙(DOM要素単位でもloadイベントがあると勘違いして作っていて、あとでそんなイベントはないことに気づいて、あわててごまかした産物)。
- 二つのテキストボックスに入力された数値を、フォームのsubmitをフックして、足し算した結果を表示する(フォーム投稿処理をAjaxで代替するイメージ)
- 二つのテキストボックスに入力された数値を、入力欄の更新をフックして、足し算した結果を表示する(パラメータの付け替えとか、複数のイベントハンドラで同じコールバックを使ってみたりとか)
なんて感じ。
まだ全然細かいところまで練れていないけれども、ひとまずこんな感じのアプローチで、Ajax対応が楽になるかどうかいろいろ使って試してみよう。一応HTML_AjaxHandlerのライセンスはLGPLにしてあるけど、まだプロトタイプレベルなんで、コードは全面的に変更される可能性がある。
ちなみに
なんで初期化(イベントハンドラの登録)をwindow.onload経由でやっているかというと、JavaScriptコードをHTMLヘッダ部に出力した場合、イベントハンドリングする対象のDOM要素がまだ生成(HTMLとして出力)されていない可能性があるから。
あと、XmlHttpRequestリクエストを実行する前に呼ばれる独自のコールバックオプションとして、parametersCallback、urlCallback、optionsCallbackが使える。それぞれ、Ajax.*を呼び出す直前に、JavaScriptコードでparameters、url、optionsを書き換えたいときに使う。
本当はもう一個、XmlHttpRequestを行うかどうかを選択するためのコールバックも必要かと思っていたんだけど、基本的な処理の流れ自体を分岐するオプションに関しては、それ以外にもいろいろなパターンがありそうだから、もっといろいろ具体的なパターンを考えてから実装した方が良さそうに思えたんで、今のところつけていない。
2006-03-11 [長年日記]
_ HTML_AjaxHandlerのサンプルその2
一番基本となる、Ajax.Requestを使うパターンを作ってなかったんで、そっちも作ってみた。
デモの内容は、最小値と最大値を入力してフォームをsubmitする(Σボタンをクリック or ENTERキーを押す)と、最小値から最大値までを加算した式と結果を表示する。
HTMLは、
<form id="rangeform"> min: <input type="text" name="min" id="min" /> max: <input type="text" name="max" id="max" /> <input type="submit" value="Σ"> </form> <span id="range"></span>
な感じで、HTML_AjaxHandlerでハンドラーを登録するPHPコードは、
require_once 'HTML/AjaxHandler.php'; $ajax =& new HTML_AjaxHandler(); $ajax->addHandler( 'rangeform', 'submit', array( 'url' => 'api.php', 'method' => 'get', 'parameters' => 'command=range', 'onSuccess' => 'rangeSuccess', ) ); echo $ajax->toJavaScript();
な感じになる。前のデモと違って、containerがセットされていないから、使われるAjax.*はAjax.Requestになる。で、onSuccessのコールバック関数はrangeSuccessね。
わざわざAjax.Requestを使う意味をだすために、サーバーサイド(api.php?command=range&min=[最小値]&max=[最大値])では、
$min = intval($_GET['min']);
$max = intval($_GET['max']);
$range = range($min, $max);
$sum = 0; foreach ($range as $num) {$sum += $num;}
echo '{expression: "' . implode(' + ', $range) . '", sum: ' . $sum . ' }';
なんて感じでJSON形式で結果を返している。で、Ajax.RequestのonSuccessコールバックに登録されたrangeSuccess関数では、
function rangeSuccess(res)
{
eval('var result = ' + res.responseText + ';');
var html = result['expression'] + ' = ' + result['sum'];
$('range').innerHTML = html;
}
なんて感じで、evalして計算式文字列と結果数値を取り出して、その内容から出力を組み立てている。
なんてのが、Ajax.Request+自前のコールバック関数を使った場合の記述パターンになる。
_ AjaxHandlerにあった方がいい分岐パターン
実際にいくつもアプリケーションを作ってみないと本当のところは分からない*1気がするけど、さすがにテストのためにそれなりの規模のアプリケーションを書く気にもなれないから、ひとまず思考実験してみよう。
まず思いつくのはフォームからの投稿をAjaxで代替処理する際に、JavaScriptバリデータと連係し、バリデーションの結果がfalseだったら、XmlHttpRequestが発動する前に処理を停止する、という流れ。これは非常によく使いそう。
で、ポイントとなるのはJavaScriptバリデーションコードをどうやって書くのか、というところ。
もしも自前でバリデーションコードを書くのならば、AjaxHandlerが生成するJavaScriptコードで、XmlHttpRequestの実行前にリクエストを実行するかどうかを判断するためのコールバックを用意し、そこにバリデーションコードを書けばいい。
なんにしろこういうパターンは絶対にあるだろうから、Ajax.*を生成する直前に呼ぶcheckValidCallbackとかを作って、その結果がfalseだったら処理を中断する、というオプションを作った方がいいだろう。
ただ、バリデーションコードがライブラリ化されている場合、自分でその実行タイミングをハンドリングできない可能性もある。たとえば、AjaxHandlerと同じように、Event.observeでバリデーション処理をハンドリングするようなバリデーションライブラリだったら、その実行タイミングはイベントハンドラーの実行順序に依存する。
また、HTML_QuickFormみたいにform.onsubmitに直接関数が記述されるようなパターンの場合、同じformのsubmitにEvent.observeしたら、どういう処理が行われるのか、という問題がある。
「まずform.onsubmitに直接記述された関数が呼ばれ、その戻り値がfalseでなかった場合にのみEvent.observeした関数が呼ばれる」なんて感じだといいのだが、「Event.observeしたら、form.onsubmitに記述されていた関数呼び出しが無効になる」とか、「Event.observeした関数とform.onsubmitに記述された関数の実行順序は不定」とかだと、話はややこしくなる。
まずはその辺の挙動について調査してみるか。
_ form.onsubmitとEvent.observeに関する実験
<script type="text/javascript" src="prototype.js"></script>
<form id="test" onsubmit="return formOnSubmit();">
<input type="submit">
</form>
<script type="text/javascript">
function formOnSubmit()
{
alert('formOnSubmit');
return false; /* A */
}
Event.observe('test', 'submit', function() {alert('EventObserve'); return false; /* B */}, false);
</script>
上記のようなコードを、Firefox 1.5.0.1、IE 6.0.2900.2180、Opera 8.5でテスト。
イベントハンドラー実行順序はどれも、form.onSubmit→Event.observeの順序。ただし、form.onSubmitの戻り値がfalseであっても、必ずEvent.observeした関数も呼ばれてしまう。
つまり、もしform.onSubmitのコードがバリデーションロジックで、バリデーションでエラーがあったのでfalseを返したとしても、その後に続くEvent.observeした関数=Ajaxリクエストコールは呼ばれてしまうし、Event.observeした関数には、バリデーションロジックの戻り値は伝わってこない。HTML_QuickFormみたいに、alertでエラーメッセージを表示するバリデーションロジックを出力する場合は、単純にEvent.observeしてAjaxリクエストコードを追加するパターンは使えない*2。
ちなみに、A、Bの戻り値をいろいろ変えて試してみたところ、そのあたりの処理にはブラウザ互換性がないことが分かった。
| A:true/B:true | A:true/B:false | A:false/B:true | A:false/B:false | |
| Firefox | 止まらない | 止まる | 止まる | 止まる |
| IE | 止まらない | 止まる | 止まらない | 止まる |
| Opera | 止まらない | 止まらない | 止まる | 止まる |
Firefoxはどちらかがfalseだったら、submit動作が止まる。IEは最後に実行されたイベントハンドラーがfalseを返したときのみ、submit動作が止まる。Operaはform.onsubmitでfalseを返したときのみ、submit動作が止まる。なんだか面倒くさいなー。
ちなみに、
Event.observe('test', 'submit', function(event) {alert('EventObserve'); Event.stop(event);}, false);
なんてしちゃえば、すべてのブラウザでsubmit動作が止まったんで、自分のコードで確実にsubmit動作を止めたければ、Event.stopを使えばいいのかな。
で、トータルでどうすればいいのかいろいろ試行錯誤して、
var currentHandler = $('test').onsubmit;
$('test').onsubmit = undefined;
Event.observe('test', 'submit', function(event) {
if ((typeof currentHandler == 'function') && (!currentHandler())) {Event.stop(event);return;}
alert('EventObserve');
Event.stop(event);
}, false);
なんて感じで、デフォルトのonsubmitを強引に後から追加したイベントハンドラーの中で実行し、その結果によって後から追加したイベントハンドラーのメイン処理を実行するかどうか分岐できるようにしてみたんだけど、FirefoxとOperaではこの方法でいけるんだけど、IEでは思ったように動いてくれないなー。
でも、どうにかしたいんだったら、こういう方向のアプローチくらいしかない気がする。JavaScriptのイベントハンドラー周りの実装っていまいち理解できていないから、その辺をちょっとお勉強してから、もう一度チャレンジしてみるか。
_ バーレーンGP予選
今年の予選の第1、第2セッションは面白いな。ただ、今回みたいに赤旗とかのトラブルが出たときの影響がちょっと大きすぎる気がするから、その辺はもうちょっと何とかした方がいいかも。
で、それに対して第3セッションは、最初何をやっているのかさっぱり分からなかった。第3セッションには、燃料制限があるってことは知っていたけど、それがどういう意味なのかよく分かってなかったっつーか。
要するに、第3セッションは
- 第3セッション開始時に入れた燃料で、決勝(のスタート)を走らないといけない。だから、第3セッション開始時には決勝スタートのための燃料を入れておく。
- 予選中に使用した分の燃料は(周回タイムが極端に遅くなければ)周回数に応じて、決勝前に補給できる。
- 予選でタイムを出すためには、燃料が少ない方がいい。だから、第3セッションの序盤は、終盤のタイムアタックのために燃料を減らすことに費やされる。
- ある程度燃料が減った段階で、タイムアタックをはじめるチームが出てくる。ただし戦略によって、どの程度燃料が減った段階でタイムアタックをはじめるかの判断は異なるし、第3セッション中もタイヤ交換も可能なので、戦略の幅は非常に大きい。
って感じなのね。で、そういうことだって分かった上で、今回の予選の結果を考えると、フェラーリの1−2ってことの意味は、現状ではまだ何とも判断できない。少なくとも去年よりは速そうではあるけれども、ルノーやホンダとは戦略が全然違うんじゃん?って可能性がものすごく高そうだし。
なんて感じで、今年の予選の第3セッションは、単に見た目(序盤にみんなでだらだら走っている意味)がわかりにくいというだけでなく、その結果から決勝や各チームの実力が読みにくい、という意味でも分かりにくい。
ただ、このわかりにくさは、一応考えればそれなりのパターンに整理することは可能なので、ある程度情報がそろって、その解釈の仕方に慣れてくれば、F1の(見ての)面白さを増す効果があるかもしれない。
それにしても予想を外さなかったのはマクラーレンだな。今年も信頼性に問題がありそうと誰もが思っていたと思うけど、見事にその予想が当たっていることを示して見せた。しかも突然リアサスペンションが折れるって、去年のリアウイングが飛んだシーンとか思い出したな。ああいかん、今回も最初にリアウイングが外れて、それでリアサスペンションを壊したのか。サスペンションが折れた方が先かと思っていたよ。
一応レースに参加できているアグリチームは、ひとまず最初の数戦は走行距離を稼いでデータと(井出は)経験を蓄えつつ、他のチームの邪魔にならないように頑張りましょうって感じか。車が壊れないようならば、琢磨の方は2、3戦で下位チームの争いに参加できるようになるんじゃないかな。
2006-03-13 [長年日記]
_ バーレーンGP決勝
formula1.comのlive timingが認証サーバーのアクセス過多でログインできなくて見れず、テレビ中継の方だけだと情報が不足し、さらにその音声情報も子供が騒いでよく聞き取れず、非常にフラストレーションが溜まる決勝観戦だった。今度は早めにlive timingにログインしておこう。
で、結局今年のフェラーリ+シューマッハは十分戦闘力があるけれども、それでもルノー+アロンソの方がちょっと上手で、ホンダも車自体は負けていない気がするけど、相変わらず決勝のレース運びが「なんかうまくねーなー」という印象で、一方マクラーレンは相変わらず信頼性は微妙だけど速さはあるし、特にロングラン戦略で盛り返すパターンは昨年同様に通じるって感じか。
そういやトヨタは全然ダメっぽいね。トラブルとかではなく、「現在の車の実力があんなもん」というドライバーを初めとした関係者の評価らしいし。やっぱり去年のうちに一勝しておくべきだったよなー。
そういや全然ダメでこんなんだったら琢磨を走らせておいた方がよかったじゃん、とか思わせてくれたバリチェロは、どうやら序盤からギアが1個死んでいたらしい。まあそれならしょうがないか。あとバトンはスタート時にクラッチトラブルが出て出遅れた、とか。
そういやマッサって、序盤よかったのに中盤以降ぐだぐだになったのは、ドライバーの問題なのかなー、それとも車にトラブルが出たのかなー。何にしろ、「速さはあるけどずいぶんもろいな」という印象だった。
アグリチームは、1台完走できたし、もう1台も終盤まで走れたし、他人を巻き込むトラブルも出なかったみたいだし、情報収集+経験という意味ではとてもよかったんじゃないでしょうか。でも、井出の「マシンをより良くするために、僕が何をできたかはわからないけれど、でもメカニックの全員がハードに働いてくれたことはわかっている。ただ、自分の経験の無さが申し訳ない気持ちだよ。」ってコメントは、とても正直ではあるけれども、なんか「他にもっとちゃんとしたフィードバックができるドライバーを乗せた方がいいのでは」と思えたりするんで、言わない方がよかったかも。
_ 1470.netサービスを停止しています
高負荷のため、ほとんどアクセスができない状態になっていたので、サービスをいったん止めました。DB周りの最適化を行ってから再起動をかけるため、復旧には数時間かかります。利用者の方々ごめんなさい。
復旧しました
19時過ぎにサービスを復旧しました。復旧してしばらくは、キャッシュが効かないんで重いと思いますが、徐々に通常レベル(っつっても大して軽くないけど)に戻っていくと思います。
2006-03-15 [長年日記]
_ Athlon64とPentium4のサーバー用途での性能
結局のところ、同等のモデルナンバー/クロックのAthlon64とPentium4(たとえばAthlon64-3200+とPentium4-3.2GHz)って、サーバー(Web+DB) 用途での絶対性能としては、どっちがいいんだろう? 価格とか消費電力とかは考えずに、Web/DBサーバーとしてそれなりの環境を整備した上での絶対性能ね。
総合評価はほぼ同等らしいから、64ビットコンパイルしたアプリ(特にDB)が使える分、サーバー用途ではAthlon64の方が上なのかなーと思いつつも、やっぱり最終的にはクロック速度が効いてPentium4の方が上かもなーとも思う。
ググって見ても、Athlon64に関してはホビー用途での事例がほとんどだし、その評価に関する記述も、たいていの場合はコストパフォーマンスや消費電力も加味した総合評価っぽくなっていて、いまいちその実用レベルでの絶対性能評価が見えない。
その辺具体的に比較してみた事例ってどこかに公開されてないでしょうか? っつーかぶっちゃけ、さくらのAthlon64ってどうよ、と思っているわけですが。
2006-03-16 [長年日記]
_ 誠実という評価の不確かさ
実際のところ、「誠実な人」「不誠実な人」がいるわけではなく、実際にあるのは「誠実な態度」「不誠実な態度」であろう。それは、ある状況において、ある人によって観察された、一つの出来事である。
そのような出来事がいくつも積み重なることによって、「誠実な人」「不誠実な人」という評価が固まっていくが、それは理論的に再現性が保証された事実ではなく、単にある限られた状況において、ある人の目からはそのような出来事が多く観察されたという話であり、結局のところそれはある人の印象にすぎない。
また、「誠実な態度」を取った人が「誠実な人」であるかどうかはわからない。「誠実な態度」は(近年のネットワークがメディアとして有効に活用される状況においては)戦略的に有効であるので(不誠実な態度によってネットワークを介して広まる悪評は、即効性の毒のようなものだ)、つまりある人が「誠実な態度」を取っているのは単なる合理的な判断によるのかもしれない。
逆に(特に知的に)誠実な態度を採用せず、「俺は信じたいから信じる」のような態度を取る人が、「不誠実な人」であるというわけでもない。そういう態度を取る人にいわゆる「いい人」(時には「誠実な人」と評価される)が多いのも現実である。
日常的な行動の多くは、人に対する印象を判断基準として採用してもかまわないだろう。しかし、そのような印象レベルの評価とは別に、ありとあらゆる情報を用いての判断が必要とされることも、場合によってはあり得るし、そのような場合は印象ではない評価をきちんと行うべきであろう。
そのような評価を行う際に、「何らかの差別的に扱われかねない事実を考慮に入れるべきかどうか」については、近代においてはそのような判断基準はできる限り重視しない態度が是とされる。基本的には私もその態度が望ましいとは思う。
しかし、「その事実自体を完全に無視するべきだ」あるいは「その事実は知らされない方が望ましい」とは思わない。そのような事実はきちんと知らされた上で、その事実をどのレベルで採用するかを個々人の判断に任せることが望ましいと思う(現時点において、それがいい結果を生むかどうかはさておき、理念としては)。
もちろんこのような一般論には、合致する事例もあるだろうし、合致しない事例もあるわけだが、一般論は一般論であって、現実に適用する際には、それぞれの実情に合わせていく必要がある。この段落はエチケットペーパーである。
_ ソニー損保にしてみた
そういや車検を通したはいいけど、自動車保険の延長契約をするのをすっかり忘れていた。今契約しているところから「延長しませんか」というお知らせがずいぶん前に来ていたらしいけど、その手紙を探しても見つからなかったんで、ためしにオンラインものに手を出してみることに。
オクサンのお勧めはソニー損保。個人的にはソニー関連の保険会社なんて、そろそろどこかに売られちゃいそうだし、あまり手を出したくないなー、とか思いつつも、まあしょせん1年単位だから何かあってもそれほどいたくないし、ほかのところを選ぶ根拠もなかったんで、結局ソニー損保に。
ちなみに今の損保ジャパンの契約とさほど変わらない内容(だと思うけど、特約関連のカバー範囲とかいまいちよくわからん。お金的にわかりやすい部分は同等かそれ以上にした)で1万円くらい安くなった。ほとんど乗っていない原付特約をはずせばさらに1万5千円くらい安くなっただろうし、もはや1年以上放置しているDJEBEL125を復活させることはなさそうな気もするんだけど、一応バイクを持っている間は念のため入っておくことに。っつーか乗らないならちゃんと処分したほうがいいよな。
2006-03-17 [長年日記]
_ コメントとトラックバックとブックマーク
コメントは、著者に対してのメッセージという意味合いが強い。トラックバックは、自分自身の考えを述べたテキストを一般に向けて公開することが主となる。ブックマークは、ある程度他人の目を意識しつつも、基本的には自分のためのメモであろう。
宣伝や頷きトラックバックの類などは無視すると、その敷居の高さは、トラックバック>>コメント>>>ブックマークといった感じだろうか。
コメントから、著者に対してのメッセージという意味合いを弱め、ブックマーク的な気軽さで記述できるような環境を整えれば、現在よりも多くのフィードバックを得られるようになるだろうか?
2006-03-18 [長年日記]
_ そろそろPHP5への移行タイミングか
ようやくPHP5に移行する決心がついた。本格的に移行するための調査を始めよう。
今までPHP4にとどまっていた理由は、
- 既存のPHP4ベースのアプリケーションのメンテの都合上
- PHP5で(安心して)使えるアクセラレータが見あたらない
- PEARを中心とした主要なライブラリがPHP5にフル対応していない
が主な理由だったけれども、1番目については、
- 今後作成するアプリケーションの将来のメンテの都合
との兼ね合いで、この時点でPHP5に移行した方がましだろうと判断。
2番目については、すでにPHP 5.1系は速度的にはPHP4系の標準状態を大幅に上回っているらしいし、PHP5系に対応したアクセラレータもそろそろ出始めているようなので、許容範囲と判断。
最大の問題は3番目なんだけど、ちらっとながめてみた限りでは、PEARのライブラリもだいぶPHP4、5両対応になってきているようだ。また、次のPHP5.1系では、classのインスタンス変数をvarで宣言しても警告を出さなくなるらしいし、そうなるとE_STRICT非対応なライブラリもだいぶ使いやすくなる。あと、Zend Frameworkで発表されたライブラリ群でPEARの置き換えを狙っているものも結構あるみたいだし、その辺と組み合わせれば何とかなりそうな気配。
ただし、実際には主要な(今メインで使っている)ライブラリでもPHP5ではそのまま動かないものが結構ありそうだし、一通り調査する必要があるだろう。ただ、よほどのことがない限りは、同等のライブラリを自前で書いたり移植&メンテナンスすることになったとしても、PHP5に移行する方針は進めるつもり。
っつーかもう、PEAR_ErrorStackを使ったとしてもPHP4ベースのエラーハンドリングは限界だよ。素直にtry catch throwしたい。
あと、httpd周りの環境もApache 2.2系をフロントに置いて、PHP実行環境はlighttpd+fastcgiに移行するつもりだけど、こっちはまだ全般的に調査不足なんで、この方針でいけるかどうか未検証。うまくやれば、PHP4と5を同居させたり、複数のユーザー権限のアプリケーションを同居させたりできそうだし、パフォーマンス的やポータビリティも良さそうに思えるんだけど。
_ マレーシアGP予選
決勝グリッドの発表があったら書こうと思っていたんだけど、いつまで経っても発表されないんで、自分で暫定グリッドを書いてみよう。予選結果が、
- フィジケラ(ルノー)
- バトン(ホンダ)
- ロズベルグ(ウィリアムズ)
- ミハエル・シューマッハー(フェラーリ)
- ウェバー(ウィリアムズ)
- モントーヤ(マクラーレン)
- ライコネン(マクラーレン)
- アロンソ(ルノー)
- クリエン(レッドブル)
- ラルフ・シューマッハー(トヨタ)
- クルサード(レッドブル)
- バリチェロ(ホンダ)
- トゥルーリ(トヨタ)
- ビルヌーブ(BMW)
- ハイドフェルド(BMW)
- スピード(トロロッソ)
- マッサ(フェラーリ)
- リウッツィ(トロロッソ)
- アルバース(ミッドランド)
- モンテイロ(ミッドランド)
- 佐藤琢磨(アグリ)
- 井出有治(アグリ)
で、ミハエル、バリチェロ、クルサード、ラルフが10位降格、マッサが20位降格だから、
- フィジケラ(ルノー)
- バトン(ホンダ)
- ロズベルグ(ウィリアムズ)
- ウェバー(ウィリアムズ)
- モントーヤ(マクラーレン)
- ライコネン(マクラーレン)
- アロンソ(ルノー)
- クリエン(レッドブル)
- トゥルーリ(トヨタ)
- ビルヌーブ(BMW)
- ミハエル・シューマッハー(フェラーリ)
- ハイドフェルド(BMW)
- スピード(トロロッソ)
- リウッツィ(トロロッソ)
- アルバース(ミッドランド)
- モンテイロ(ミッドランド)
- ラルフ・シューマッハー(トヨタ)
- 佐藤琢磨(アグリ)
- クルサード(レッドブル)
- 井出有治(アグリ)
- バリチェロ(ホンダ)
- マッサ(フェラーリ)
が決勝グリッドになるはずだよね。
で、今回謎だったのはアロンソ。予選最後のアタックで存在感がないままに8位に沈んだけど、理由は何? 作戦ミスなのか、戦略通りなのか、トラブルなのか、ミスったのか、さっぱりわからなかった。フィジケラがポールだし、それまでのアロンソのタイムも悪くなかったから、車が悪いってことはあり得ないはずだけど、戦略だとしても8位は悪すぎだよな。フィジケラはエンジン交換しているから、その分の差が出たって可能性もなくはないんだけど。
あと、相変わらずバリチェロはぱっとしないけど、今回は実力通りなんだろうか? こんなんじゃ本当に琢磨の方が良かったと(主に日本のファンは)思ってしまうぞ。っつーか実績からいって、バトンを食うくらいの勢いがあるかと期待していたんだけど、実はバトンってものすごくいいドライバーなんだろうか? いやもちろん悪いドライバーとは思っていないけど、強いて言えばピークの頃のクルサード程度?みたいな印象だったんだけど。
そういや前回決勝で速かったウィリアムズが、今回は予選も速かったね。ウィリアムズはシーズン後半つらくなる(伸びしろが他のチームより少ない)という予想がくっついているから、ここで活躍して来シーズンの大口スポンサー(というかエンジンメーカー)をゲットしないとね。っつーか、今年ぱっとしなかったトヨタが、ひとまずエンジンだけでも勝っておきたいということで、ウィリアムズと組むというシナリオがありそうな予感。
あとはまあふつうですか。ただ今回は(特にエンジン周りの)トラブルが予想されることだし、上位が軒並みトラブルでつぶれるという展開がありそうだよね。フェラーリが予選グリッドを下げてまで2台ともエンジン交換してきたのが、結構怪しい。そういう展開を予想しているんじゃなかろうか。
っつーかバトン+ホンダって、最後の最後でトップを走っていてエンジンブローとかやりそうで怖いなー。今回ホンダは前回よりも早めに前に出る戦略をとってきたっぽいから、序盤から飛ばしていくつもりなんだろうし、終盤にエンジンがきつくなるってのはすげーありそうなんだよなー。まあそういうときにバリチェロがちゃんと後ろからあがってこれるようだと、今シーズン期待できるんだけどね。
追記
アロンソは給油機のトラブルで、予定よりも多く給油しちゃってたんだそうな。そういやそんな映像もあったっけ。ってことは決勝で予定外のロングラン作戦を実施する羽目になっちゃったんだね。
決勝グリッドは俺は予選順位の上位から順に、降格していくという予選順位に降格分を単純に足していく方法を採用したけど、誰から降格するかが明確でないらしい。なんか予想されるグリッドのパターンはいろいろあるみたい。
バリチェロは、エンジン交換のせいでセットアップが間に合わなかったという見解が発表されているけど、それだけでバトンとあんなに差が出るの?と思わないでもない。
2006-03-20 [長年日記]
_ 3/19夕方〜3/20朝まで一部でDBエラーが出ていました
サーバーの各種モジュールのアップデートを行った際に、mysqlがテンポラリディレクトリとして使用していた/dev/shm以下にアクセスできなくなり、上記ディレクトリにテンポラリテーブルを作成するようなqueryのみエラーになるという症状で、気づくのが遅れました。申し訳ありません。
_ 腰が痛い
昨日の昼に買物から帰ってソファに腰を下ろしたら、突然腰に痛みが走った。なんかいかにもやばそうな痛み。ちょっとでも腰に負担をかけたらかなりの痛みが走る。やばいなーと思いつつできるだけ腰に負担をかけないように1日を過ごしたんだけど、翌朝起きてもまだ痛い。今日の朝一番で病院に行こうと思っていたんだけど、近所の病院はゲロ混みだったんでひとまずやめて、会社に来てみたところ、椅子に座っているとだんだん痛みの範囲が広がってきたんで、これはまずいと午後一で病院に行った。で、結局診断としては、背骨の一番下の腰骨と接続するあたりの軟骨が、微妙に接触しているのが原因だと思われるけれども、特にひどい状態ではないから一週間くらいで治るでしょう、とのこと。それ以外は特に問題はないらしい。ヘルニアとかひどい方向を想像していたんで、ほっとした。痛いだけで済むならいいや。一応痛み止めを出してもらって、電気治療も受けてきた(結構効果があって、受けた後は痛みがずいぶん和らいだ)。
_ PHP5への道
PHP5への移行を決意したわけだが、早速気が重くなってきた。
ひとまずローカルWindows環境をPHP5に入れ替えてみたんだけど、パスとかは正しいにもかかわらず、指定したextensionのDLLが見つからないと言われるし、PEARも標準状態で素直にインストールされてくれない。単に動かすだけでも結構トラブルが多そう。こういうトラブルは、多分*nix系では出ないとは思うんだけど、印象が良くないなー。
さらに、手持ちのアプリケーションを動かそうとしたところ、各種エラー(主にE_NOTICEやE_STRICT)が出るのは覚悟していたけれども、そのエラーメッセージが訳分からない。エラーが出ているところとは全然違うファイル+行を差したエラーメッセージ(指定された場所にはそんなエラー発生源はない)が表示されたりする。これはいったい何が原因なんだろうなー。→E_STRICTをトラップしないエラーハンドラーが原因だった。
あと、予想していたよりも、思いのほかいろんなエラーが出てくるなー。クラスのメソッドの中で必要に応じて外部クラスをrequire_onceで読み込んだりすると、nested classとか言われちゃうのか。あとis_aとかもdeprecatedらしいし。どうせ移行するなら、自分のコード部分はE_STRICTレベルまできっちり対応しておきたいけど、この様子だとPHP4との互換性を保ったままPHP5に移行するなんてのは不可能っぽいな。全面的にPHP5に移行するしかなさそげ。
PHP4のコードがだいたいそのまま動くように作られているらしいと聞いて、もうちょっと互換性が高いことを期待していたんだけど、それはあくまでもPHP4のコードをだましだまし動かすこともできるよ、ってレベルだね。PHP5できちんと動くコードを書こうと思ったら、PHP4との互換性はとても保てない(クラスライブラリ的なものは特に)。
まあそれでも、今までPHP4だから妥協していた部分がいろいろきれいに書けるようになるし、何よりエラーハンドリングがまともに書けるようになるし、頑張ってPHP5に移行していこう。思ったよりもずいぶんつらそうだけど。
2006-03-21 [長年日記]
_ 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をまたがった入れ物としては使えないし。プラグインが状態を保持するというアプローチはありかもしれないけど、プラグインのインスタンスへの直接アクセス方法がないから、いまいち使い勝手が良くない気もする。
というところで疲れてきたので休憩。
_ 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をどう使うか その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月いっぱいまでってことだよね。まあそんな感じかなーとは思っていたけど、やっぱり遅そうだなー。
2006-03-22 [長年日記]
_ 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から解決する方がテスト用にはいいのかな。
まあそんな風にして動作させたところ、思ったような感じで動作してくれているんで、今までソースとマニュアルを読んだだけで理解してきたことは、特に大きくは外してはいない模様。
というわけで、しばらく実際にいろいろ動かして試してみることにする。
_ [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をどう使うか その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をどう使うか その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をどう使うか その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をもっと抽象化して他のテンプレートエンジンに対応できるようにしよう、なんて案には賛同する*4わけだけど、人間関係的な文脈が読めないんで、どうアプローチしていいのかさっぱりわからない。ひとまず各投稿者の名前とかでググって、どういう人がどういう立場でどういう発言をしているのか把握するところからはじめないとダメなのかな。


Before...
_ 秋元 [ちゃんと読み込んでないので単なるご紹介なんですが、"degradable Ajax"というキーワードでいくつかエント..]
_ ishinao [>secondlifeさん シンプルなフォームだと、そういう補助的なJavaScriptの書き方(基本的な流れは同じ..]
_ ishinao [>秋元さん 上記ページは、まさしく私のとろうとしたアプローチに関する話でした。 このページで紹介されている方法は、..]