トップ «前の日記(2006-03-07) 最新 次の日記(2006-03-09)» 編集

2002|01|02|03|04|05|06|07|08|11|12|
2003|01|02|03|04|05|06|07|08|09|10|11|12|
2004|01|02|03|04|05|06|07|08|09|10|11|12|
2005|01|02|03|04|05|06|07|08|09|10|11|12|
2006|01|02|03|04|05|06|07|08|09|10|11|12|
2007|01|02|03|04|05|06|07|08|09|10|11|12|
2008|02|03|04|07|

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>&lt;&gt;&quot;&amp;</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関数用のエスケープを実行するようにする、とか)けど、これでもないよりはずいぶんましって気がする。