Pinoco0.5は柔軟なバリデータが売り

ついこの前0.4を出したところですが、また機能的にひと皮むけた感じなので、0.5としてPinocoマイナーバージョンの桁を上げました。
Downloads · tanakahisateru/pinoco · GitHub

こちらが変更点の一覧です。
Changelog · tanakahisateru/pinoco Wiki · GitHub

一番わかりやすいのは、ライセンスをLGPLからMITに変更した点ですが、その動機となったのは、機能が増えて有用性が多様化してきたと思ったからです。

当初は、フック/レンダリングというフロー(固有の機能)以外に有用なところはなく、固有の機能は、使うか使わないかユーザが選ぶだけで、わざわざ書き換えて使うようなものでもなかった。のですが、いま、独立して有用な、一般問題を解決している箇所が増えてきて、そこから依存性を排除するよう書き換えて抜き出せば、単独で再利用できるライブラリが取り出せるという可能性。クレジットを残してありさえすれば、誰でも堂々と改造して使えるようにしたかった。

で、その再利用して有用な機能の大型新人が、Pinoco_Validatorです。

Pinoco0.5のバリデータはPHPの手続きコードの代替です。MVCな思想のフレームワークでは、永続化されるモデルだろうとフォームモデルだろうと、バリデータが宣言できますね。でも、Pinocoは残念ながらMVCではありません。なので、これまでのバージョンで普通にやると、こんなふうに書かなきゃいけない。

<?php
if(empty($_POST['title'])) {
    $errors['title'] = "入力してください";
}
if(empty($_POST['body'])) {
    $errors['body'] = "入力してください";
}
else if(strlen($_POST['body']) < 2) {
    $errors['body'] = "2文字以上入力してください";
}
//ç•¥
if(count($errors)) {
    //エラー系
    $this->errors = $errors;
    $this->page = "input.html";
}
else {
   //正常系
}
?>

ここまで書かないとぜんぜん動かないのは、めんどくさいですね。

バリデータを使うと、まずこれだけ書けば同じチェックができます。

<?php
$v = Pinoco::newObj('Pinoco/Validator.php/Pinoco_Validator', $_POST);
$v->check('title')->is('not-empty');
$v->check('body')->is('not-empty')->is('min-length 2');
if($v->invalid) {
    $this->form = $v->result;
    $this->page = "input.html";
}
else {
   //正常系
}
?>

全体的に英文として自然ですね。ちょっと DSLっぽいです。チェック対象のフィールド名が左に揃うので、ずいぶんコードが見やすくなります。チェックしたい条件が増えれば、右に足していけばいいだけなのでコードの見た目を悪くしません。

エラーがあったかどうかで分岐する部分も、パッと見で分岐の意図が明確です。で、resultをformと呼んでレンダリングに使おうとしてるところがさらにミソです。これやっておくと、HTMLでのビュー実装はこんなふうに書けます。

<form method="POST" tal:define="form this/form">
    <label>タイトル</label>
    <input name="title" type="input" tal:attributes="value form/title/value" />
    <span class="err" tal:condition="form/title/invalid" tal:content="form/title/message">エラー</span>
    <br />
    <label>本文</label>
    <textarea name="body" tal:content="form/body/value"></textarea>
    <span class="err" tal:condition="form/body/invalid" tal:content="form/body/message">エラー</span>
</form>

変数(に見えるもの)を流しこむだけの、TAL的に素直な記述にできます。
そりゃ高度なフォームヘルパのほうがいいんでしょうけど、なにせHTMLデザイナーにXHTML互換を約束した以上、高度すぎる抽象化は避けたいですよね。でもだからといって、高度な言語機能を強制するのもやりたくない。このぐらいがちょうどいいベタ加減かと思います。

resultにはcheckしたフィールドしか入らないので、チェック漏れがあれば警告されます。もし、とりあえずそのままノーチェックでいいという場合は、こうしておけばいいでしょう。

$v->check('field_name')->is('pass'); // 必ず成功。

でもこのままでは、エラーメッセージがデフォルトのままです。メッセージのカスタマイズをしましょう。とっても簡単。

<?php
$v = Pinoco::newObj('Pinoco/Validator.php/Pinoco_Validator', $_POST, array(
    'not-empty' => "入力してください",
    'min-length' => "{param}文字以上入力してください",
));
$v->check('title')->is('not-empty');
$v->check('body')->is('not-empty')->is('min-length 2');
?>

あるいはこう。

<?php
$v = Pinoco::newObj('Pinoco/Validator.php/Pinoco_Validator', $_POST);
$v->check('title')
        ->is('not-empty', "タイトルを入力してください");
$v->check('body')
        ->is('not-empty', "本文を入力してください")
        ->is('min-length 2', "本文が短かすぎます");
?>

こういうのでもいいです。

<?php
$v = Pinoco::newObj('Pinoco/Validator.php/Pinoco_Validator', $_POST, array(
    'not-empty' => "{label}を入力してください",
));
$v->check('title', "タイトル")->is('not-empty');
$v->check('body', "本文")->is('not-empty')->is('min-length 2', "{label}が短かすぎます。");
?>

ところでこれ、ホワイトスペースを文字数として数えてしまうとまずいですよね。項目のチェックフローのチェインの中には、値を正規化するためのフィルタを入れることもできます。

<?php
$v->check('title')->is('not-empty')->filter('trim');
$v->check('body')->is('not-empty')->filter('trim')->is('min-length 2');
?>

ビルトインのチェックだけでは不十分な場合、アドホックにチェッカを追加することも可能です。

<?php
$v->defineValidityTest('zip-code', function($value) {
    return preg_match('/^[0-9]{3}-[0-9]{4}$/', $value);
}, "Zip code only.");
$v->check('zip_code')->is('zip-code');
?>

それも面倒な人は、こういうのでもOKですよ。

<?php
$v->check('zip_code')->is(function($value) {
    return preg_match('/^[0-9]{3}-[0-9]{4}$/', $value);
}, "郵便番号を入力してください");
?>

またまたクロージャ万歳です。まあ、クロージャが使えない場合はコールバックでもいいですが。

あとこのバリデータの最大のメリットは、これが宣言ではなく手続きだということです。つまり、構造化プログラミングとの組み合わせができるわけです。宣言的バリデータではこうはいきません。

<?php
// 「URLあり」チェックボックスがオンのときのみURLを検証すること。
if($v->contextFor('checkbox_has_url')->is('== 1')->valid) {
    $v->check('link')->is('not-empty')->is('url');
}
?>

もし、「分岐なんていらない。宣言的バリデータのほうが好き」と思うんなら、そんなのはこれをベースに簡単に作れますしね。

ちなみに、not-emptyチェックせずに他のチェックした場合、値空っぽを通してしまうのでご注意を。オプショナルなフォームフィールドは、値があったら妥当性検証しなきゃいけないけど、空っぽならそれはスルーしてくれたほうが便利ですし。

これ、いろいろPinocoでの開発を意識しまくってAPI設計したのですが、場合によっては他のフレームワークに統合したり、あるいは、ほんとに生のPHPスクリプトで使っても便利なんじゃないだろうか、と思いました。というわけで、MITになったのでいろいろハックして使えそうなところだけ使ってください。GitHubでフィードバックあればとても嬉しいです。

次回、Pinoco0.5のPHPTALのネームスペース拡張につづく。