FuelPHPのデフォルトバリデーションルールの仕様について調べた話

最近仕事でFuelPHPを使う機会を得たのですが、FuelPHPのデフォルトで用意されているバリデーションルールで少しハマった部分があったため、戒めのためにまとめておきます。
意外とググッても情報が出てこない部分があって、公式ドキュメントはそれなりに充実しているものの、細かな仕様までは書かれていません。最終的にはコアのコードを読むのが一番だと気づいてようやくバリデーションルールの仕様がわかってきたのですが、やっぱり困ったらコアのコードを読む気持ちを持とうよという話です。
発端になったバリデーション
今回の発端になったのは以下のような名前や年齢を受け取り、検証するフォームです。

コードはこんな感じ
app/classes/controller/validation.php
<?php class Controller_Validation extends Controller { public function action_index() { $form = Fieldset::forge(); $form->add('name', 'Name')->add_rule('required'); $form->add('age', 'Age') ->add_rule('required') ->add_rule('numeric_between',0,200); $form->add('submit', '', array('type'=>'submit', 'value'=>'Submit')); if (Input::method() === 'POST') { $val = $form->validation(); if ($val->run()) { print "入力を受付けました"; } else { print "不正な入力です"; } $form->repopulate(); } $view = View::forge('index/get'); $view->set_safe('form', $form->build('/validation/')); return $view; } }
入力値をデータベースに入れることを考えるならば文字コードの検証や文字列の長さの検証、制御文字の有無など判定する必要があると思いますが、今回はサンプルなので最低限にしています。
デフォルトでFuelPHPに用意されているrequiredとnumeric_betweenを使用して、年齢には0歳から200歳までを受け入れるようにルール設定をし、判定結果を表示するだけです。
これで以下のように問題なくバリデーションが機能します。

ところが、以下のような入力はパスしてしまいます。

あれ!?numeric_betweenで0から200の数値しか受け入れていないはずなのに、なんで数値ですらない文字列がバリデーションパスしてるの?バグじゃん!!!
・・・まあそんなときは慌てず騒がず、コアの実装を確認してみましょう。
core/classes/validation.php (抜粋)
<?php /* 中略 */ /** * Checks whether numeric input is between a minimum and a maximum value * * @param string|float|int * @param float|int * @param float|int * @return bool */ public function _validation_numeric_between($val, $min_val, $max_val) { return $this->_empty($val) or (floatval($val) >= floatval($min_val) and floatval($val) <= floatval($max_val)); }
はい、原因はfloatvalですね。そもそもnumeric_betweenは評価される値に数値以外が来ることを想定していません。ちなみに、これはnumeric_minやnumeric_maxでも同じです。
PHPでは文字列をfloat型にキャストする場合、「一般的には」0になるので、0以上200以下の条件に合致するため、バリデーションをパスできたということですね。一般的に、と書いたのは文字列に数値を含む場合にその数値としてキャストされる場合があったりするので書いてあります。詳細は以下のページを見てください。
問題を解決するためには
上記のように文字列が入ってくることを許容できないのであれば、valid_stringにnumericを指定するようなルールを経由する必要があります。
<?php /* 中略 */ $form->add('age', 'Age') ->add_rule('required') ->add_rule('valid_string','numeric') ->add_rule('numeric_between',0,200);
ただ、こうすると今度はマイナスの値がバリデーションをパスできなくなります。
なぜでしょう、迷ったらコアを読みます。
core/classes/validation.php (抜粋)
<?php /* 中略 */ /** * Validate input string with many options * * @param string * @param string|array either a named filter or combination of flags * @return bool */ public function _validation_valid_string($val, $flags = array('alpha', 'utf8')) { if ($this->_empty($val)) { return true; } if ( ! is_array($flags)) { if ($flags == 'alpha') { $flags = array('alpha', 'utf8'); } elseif ($flags == 'alpha_numeric') { $flags = array('alpha', 'utf8', 'numeric'); } elseif ($flags == 'specials') { $flags = array('specials', 'utf8'); } elseif ($flags == 'url_safe') { $flags = array('alpha', 'numeric', 'dashes'); } elseif ($flags == 'integer' or $flags == 'numeric') { $flags = array('numeric'); } elseif ($flags == 'float') { $flags = array('numeric', 'dots'); } elseif ($flags == 'quotes') { $flags = array('singlequotes', 'doublequotes'); } elseif ($flags == 'slashes') { $flags = array('forwardslashes', 'backslashes'); } elseif ($flags == 'all') { $flags = array('alpha', 'utf8', 'numeric', 'specials', 'spaces', 'newlines', 'tabs', 'punctuation', 'singlequotes', 'doublequotes', 'dashes', 'forwardslashes', 'backslashes', 'brackets', 'braces'); } else { return false; } } $pattern = ! in_array('uppercase', $flags) && in_array('alpha', $flags) ? 'a-z' : ''; $pattern .= ! in_array('lowercase', $flags) && in_array('alpha', $flags) ? 'A-Z' : ''; $pattern .= in_array('numeric', $flags) ? '0-9' : ''; $pattern .= in_array('specials', $flags) ? '[:alpha:]' : ''; $pattern .= in_array('spaces', $flags) ? ' ' : ''; $pattern .= in_array('newlines', $flags) ? "\r\n" : ''; $pattern .= in_array('tabs', $flags) ? "\t" : ''; $pattern .= in_array('dots', $flags) && ! in_array('punctuation', $flags) ? '\.' : ''; $pattern .= in_array('commas', $flags) && ! in_array('punctuation', $flags) ? ',' : ''; $pattern .= in_array('punctuation', $flags) ? "\.,\!\?:;\&" : ''; $pattern .= in_array('dashes', $flags) ? '_\-' : ''; $pattern .= in_array('forwardslashes', $flags) ? '\/' : ''; $pattern .= in_array('backslashes', $flags) ? '\\\\' : ''; $pattern .= in_array('singlequotes', $flags) ? "'" : ''; $pattern .= in_array('doublequotes', $flags) ? "\"" : ''; $pattern .= in_array('brackets', $flags) ? "\(\)" : ''; $pattern .= in_array('braces', $flags) ? "\{\}" : ''; $pattern = empty($pattern) ? '/^(.*)$/' : ('/^(['.$pattern.'])+$/'); $pattern .= in_array('utf8', $flags) || in_array('specials', $flags) ? 'u' : ''; return preg_match($pattern, $val) > 0; }
numericを指定した場合には、フラグにnumericが立つのでpreg_matchに書けられる正規表現は/^['0-9']+$/になります。つまり、numericは負数を想定していないんですね。切ない。まぁマイナス記号は数字ではないですからね。
文字列は許可したくないけど、マイナス記号は受け入れたい場合には以下のように修正すればいけます。
<?php /* 中略 */ $form->add('age', 'Age') ->add_rule('required') ->add_rule('valid_string',array('numeric','dashes')) ->add_rule('numeric_between',-100,200);
valid_stringには配列としてフラグ立てたものを与えれば判定ができます。
これで解決かとおもいきや、今度は「-100-」とか「_200」とかを受け入れられてしまいます。
数値を扱う際にfloatvalするとかの対応が考えられますが、やっぱりあんまり好ましくありませんので、結局自作バリデーションを使うしかなさそうです。
app/classes/validate/validation.php
<?php class Validate_Validation { public static function _validation_is_numeric($val) { return preg_match('/^\-*[0-9]+$/', $val) > 0; } }
app/classes/controller/validation.php (抜粋)
<?php /* 中略 */ $form->validation()->add_callable('Validate_Validation'); $form->add('age', 'Age') ->add_rule('required') ->add_rule('is_numeric') ->add_rule('numeric_between',-100,200);
これでようやく目的達成です。長かった・・・
ということでまとめた
そんなわけでnumeric_betweenに苦しめられたため、デフォルトのバリデーションルールに関しては読んで理解してまとめておくことにしました。こんな感じです。
| ルール名 | 概要 |
|---|---|
| required | false,null,空文字,空配列以外ならばパス |
| match_value[$compare,$strict] | PHPの比較。$strictがtrueならば===で判定される。$compareは配列でもOK。false,null,空文字,空配列ならばパスされる |
| match_pattern[$pattern] | preg_matchで正規表現を判定する。false,null,空文字,空配列ならばパスされる |
| match_field[$field] | 厳密な比較で指定されたPOSTのフィールドと一致するか確認する。メールアドレスの確認用入力などの検証に便利 |
| macth_collection[$collection,$strict] | in_arrayで配列に値が存在するか検証される。$strictはin_arrayに使用される。$collectionに配列が指定されない場合には、引数リストの配列を$collectionとして使用するため、match_valueっぽい振る舞いもできる |
| min_length[$length] | 文字列の長さをmb_strlenで判定する。文字コードはFuelPHPの設定値を使うため注意。等しい値は含む。false,null,空文字,空配列ならばパスされる |
| max_length[$length] | 文字列の長さをmb_strlenで判定する。文字コードはFuelPHPの設定値を使うため注意。等しい値は含む。false,null,空文字,空配列ならばパスされる |
| exact_length[$length] | 文字列の長さをmb_strlenで判定する。文字コードはFuelPHPの設定値を使うため注意。false,null,空文字,空配列ならばパスされる |
| vaild_email | PHPのFILTER_VALIDATE_EMAILで判定される。false,null,空文字,空配列ならばパスされる |
| valid_emals[$separator] | $separatorでexplodeされ、それぞれFILTER_VALIDATE_EMAILで判定される。false,null,空文字,空配列ならばパスされる |
| valid_url | PHPのFILTER_VALIDATE_URLで判定される。false,null,空文字,空配列ならばパスされる |
| valid_ip | PHPのFILTER_VALIDATE_IPで判定される。false,null,空文字,空配列ならばパスされる |
| valid_string[$flags] | 正規表現を軸に入力フラグに対してpreg_matchで判定する。決まったパターンセットを引数にすることが一般的だが、配列でフラグを選択して渡すこともできる。false,null,空文字,空配列ならばパスされる |
| numeric_min[$min_val] | >=で比較される。float型にキャストされるので注意。false,null,空文字,空配列ならばパスされる |
| numeric_max[$max_val] | <=で比較される。float型にキャストされるので注意。false,null,空文字,空配列ならばパスされる |
| numeric_between[$min_val,$max_val] | それぞれ>=と<=で比較される。float型にキャストされるので注意。false,null,空文字,空配列ならばパスされる |
| required_with[$field] | POSTされた際にフィールドの値が存在するときにはrequiredみたいな振る舞い。セットで入力してもらいたい際に便利。フィールドの値が存在する基準となる判定はfalse,null,空文字,空配列でないこと |
| valid_date[$format,$strict] | フォーマット指定時にはdate_parse_from_formatによって、指定なしの場合にはdate_parseによってパースできるかどうか。$strictをfalseにするとパース時のwarning_countを無視して判定する |
| valid_stringのパターンセット | 対応するフラグ |
|---|---|
| alpha | alpha,utf8 |
| alpha_numeric | alpha,utf8,numeric |
| specials | specials,utf8 |
| url_safe | alpha,numeric,dashes |
| integer | numeric |
| numeric | numeric |
| float | numeric,dots |
| quotes | singlequotes,doublequotes |
| slashes | forwardslashes,backslashes |
| all | alpha,utf8,numeric,specials,spaces,newlines,tabs,punctuation,singlequotes, doublequotes,dashes,forwardslashes,backslashes,brackets,braces |
| valid_stringのフラグ | 概要 |
|---|---|
| alpha | a-zA-Zをパターンに組み込む |
| uppercase | alphaフラグと併用。立てるとa-zをパターンに組み込まない |
| lowercase | alphaフラグと併用。立てるとA-Zをパターンに組み込まない |
| numeric | 0-9をパターンに組み込む |
| specials | [:alpha:]をパターンに組み込む |
| spaces | 半角スペースをパターンに組み込む |
| newlines | ¥r¥nをパターンに組み込む |
| tabs | ¥tをパターンに組み込む |
| dots | ドット記号をパターンに組み込む |
| commas | コンマをパターンに組み込む |
| punctuation | ".,!?:;&"をパターンに組み込む |
| dashes | "_-"をパターンに組み込む |
| forwardslashes | "/"をパターンに組み込む |
| backslashes | "\"をパターンに組み込む |
| brackets | ()をパターンに組み込む |
| braces | {}をパターンに組み込む |
| utf8 | preg_matchに/u修飾子を付加する |
requiredの振る舞いを見れば納得な気もする
numeric_betweenなんだから、そもそも数値以外は弾けよ!といいたい気持ちにもなりますが、それをいったら他のvalid_stringとかだって検証対象の値がNULLだったりしたら弾いてほしいって話になりますよね。でもその辺はrequiredを入れないことでNULLの場合は問答無用で許可してほしいわけです。
numeric_betweenとかも同じ考えのもとにあると思ったほうがいいのかもしれません。でも、数値の範囲を指定しておいて、数値以外は許可したいケースってあんまりないような・・・
まぁ困ったときにはFuelPHPのバリデーションは簡単に拡張できるようになっているので、可読性とかの観点からも自作バリデーションを書くのはおすすめです。
デフォルトのルールで困ったときにも、FuelPHPのコアは普通にPHPで書かれているので、非常に読みやすいです。特にバリデーションメソッドはTrueを返すかFalseを返すか書いてあるだけでシンプルなので、一度読んでみてはいががでしょうか。
参考
Validation - クラス - FuelPHP ドキュメント
# 1.7公式のバリデーションルールに関する解説。網羅されていて非常によいです。
FuelPHPのバリデーションクラスについて。 - 新人Webエンジニアの記録。
# FuelPHPのバリデーションについてかなり詳しく調べられています。非常に参考になりました。