概要
Laravel3のバリデーションルール'date_format:j'
が0~99までの入力を許していたため調査した。
本来ならば「j」は日にちを指すので、1~31の入力であることを判定してほしい。
結果、話が簡単に終わらなくて泣いた。
結論
- 「
required
とbetween
とctype_digitを使ったカスタマイズルール(+このルールをnumeric判定ルールに追加)
」の組み合わせを使う - または「
required
とdate_dormat
とbetween
とnumeric
」の組み合わせを使う - いっそ正規表現で判定する
- むしろまとめたカスマタイズルールを作成しろ
分解すると
- 0~99までを受け入れるのは、恐らく仕様(PHP5.3の結果)
- よって、日を判定するならば、別途
between:1,31
などが必要 - ただし、
between(min,max)
などはデフォルトでは文字列の文字数なので、別途「入力値を数値とみなす」ルールが必要 - 既存の「「入力値を数値とみなす」ルール」では数値文字だけを判定できない
- 「「入力値を数値とみなす」ルール」では数値文字だけを判定できないので、見逃した数値以外の文字列を弾く
date_format
か、そもそも完全に数値文字だけを判定するctype_digit()
を使用する
流れ
- Laravelバリデーションルール
date_format
の調査 -
data_format
ルールに使われるdate_create_from_format
の調査 - 0~99は仕様と受け止め、追加ルール
between:1,31
で収めようとした結果 -
between
ルールに必要なfilter_var
の調査 -
filter_var
では不十分なので代用のis_numeric
の調査 -
is_numeric
も不完全だけど、組み合わせで補完できるよね -
ctype_digit
ってもしかして完璧?今回は使わないけど
バリデーションの実装を見る
実装はLaravel\Validator
クラスのvalidate_date_format()
Laravel3のValidator.php
protected function validate_date_format($attribute, $value, $parameters)
{
return date_create_from_format($parameters[0], $value) !== false;
}
date_create_from_format
はDateTime::createFromFormat
のエイリアスである。
PHP: date_create_from_format - Manual
この関数は次の関数のエイリアスです。 DateTime::createFromFormat()
そして、DateTime::createFromFormat()
の説明には
format 文字 | 説明 | 取りうる値の例 |
---|---|---|
d および j | 2桁の日付。先頭のゼロを含むものと含まないもの | 01 から 31 あるいは 1 から 31 |
とある。
見かけ上は希望通りである。
動かしてみる
protected function validate_date_format($attribute, $value, $parameters)
{
\Log::debug("バリデーションチェック", true);
\Log::debug($value, true);
\Log::debug($parameters, true);
\Log::debug(date_create_from_format($parameters[0], $value), true);
\Log::debug('終了', true);
return date_create_from_format($parameters[0], $value) !== false;
}
2015-04-23 16:14:35 DEBUG - バリデーションチェック
2015-04-23 16:14:35 DEBUG - 1
2015-04-23 16:14:35 DEBUG - Array
(
[0] => j
)
2015-04-23 16:14:35 DEBUG - DateTime Object
(
[date] => 2015-04-01 16:14:35
[timezone_type] => 3
[timezone] => Asia/Tokyo
)
2015-04-23 16:14:35 DEBUG - 終了
2015-04-23 16:15:51 DEBUG - バリデーションチェック
2015-04-23 16:15:51 DEBUG - 99
2015-04-23 16:15:51 DEBUG - Array
(
[0] => j
)
2015-04-23 16:15:51 DEBUG - DateTime Object
(
[date] => 2015-07-08 16:15:51
[timezone_type] => 3
[timezone] => Asia/Tokyo
)
2015-04-23 16:15:51 DEBUG - 終了
2015-04-23 16:16:40 DEBUG - バリデーションチェック
2015-04-23 16:16:40 DEBUG - 0
2015-04-23 16:16:40 DEBUG - Array
(
[0] => j
)
2015-04-23 16:16:40 DEBUG - DateTime Object
(
[date] => 2015-03-31 16:16:40
[timezone_type] => 3
[timezone] => Asia/Tokyo
)
2015-04-23 16:16:40 DEBUG - 終了
2015-04-23 16:17:24 DEBUG - バリデーションチェック
2015-04-23 16:17:24 DEBUG - 100
2015-04-23 16:17:24 DEBUG - Array
(
[0] => j
)
2015-04-23 16:17:24 DEBUG -
2015-04-23 16:17:24 DEBUG - 終了
結果
入力 | 出力日付 | 判定 |
---|---|---|
0 | 先月最終日 | true |
1 | 今月1日 | true |
99 | 今月から99日後の正常に桁(月)上りした日付 | true |
100 | false | false |
ちなみに-1なども失敗。
月のフォーマットm
でも同様の結果になるが、
User Contributed Notesで同じようなことが書いてあった
マイナス評価なので、こちら側が悪い気がするが……
Laravel5のバリデーションの実装を見る
さすがにLaravel3時代の実装から改善されているかつ、流用できそうという期待からLaravel5の実装を見る。
protected function validateDateFormat($attribute, $value, $parameters)
{
$this->requireParameterCount(1, $parameters, 'date_format');
$parsed = date_parse_from_format($parameters[0], $value);
return $parsed['error_count'] === 0 && $parsed['warning_count'] === 0;
}
多分これ。
$this->requireParameterCount()
は要素数のバリデーションで無関係として、使用する関数が変わっている。
PHP: date_parse_from_format - Manual
パラメータ ¶
format
DateTime::createFromFormat() が理解できるフォーマット。
うーん嫌な予感。
動かしてみた結果
恐らくパラメーターの取り方は同じと信じてLaravel3に突っ込む。
protected function validate_date_format($attribute, $value, $parameters)
{
\Log::debug("バリデーションチェック", true);
\Log::debug($value, true);
\Log::debug($parameters, true);
\Log::debug(date_parse_from_format($parameters[0],$value), true);
\Log::debug('終了', true);
return date_create_from_format($parameters[0], $value) !== false;
}
ざっくりdate_parse_from_format()
の出力だけ。
読み飛ばしてOKです。
2015-04-23 16:55:26 DEBUG - Array
(
[year] =>
[month] =>
[day] => 0
[hour] =>
[minute] =>
[second] =>
[fraction] =>
[warning_count] => 0
[warnings] => Array
(
)
[error_count] => 0
[errors] => Array
(
)
[is_localtime] =>
)
2015-04-23 16:55:45 DEBUG - Array
(
[year] =>
[month] =>
[day] => 1
[hour] =>
[minute] =>
[second] =>
[fraction] =>
[warning_count] => 0
[warnings] => Array
(
)
[error_count] => 0
[errors] => Array
(
)
[is_localtime] =>
)
2015-04-23 16:56:04 DEBUG - Array
(
[year] =>
[month] =>
[day] => 99
[hour] =>
[minute] =>
[second] =>
[fraction] =>
[warning_count] => 0
[warnings] => Array
(
)
[error_count] => 0
[errors] => Array
(
)
[is_localtime] =>
)
2015-04-23 16:56:20 DEBUG - Array
(
[year] =>
[month] =>
[day] => 10
[hour] =>
[minute] =>
[second] =>
[fraction] =>
[warning_count] => 0
[warnings] => Array
(
)
[error_count] => 1
[errors] => Array
(
[2] => Trailing data
)
[is_localtime] =>
)
errorもwarningも出ないので、Laravel5でも0~99はOKかもしれません。
これはもう、当たり前の仕様なのかもしれませんね。
確かに日付の繰越は便利なときが多々ありますし、日付情報を取り出す関数をバリデーションに使うことが間違いなのでしょうか。
between
ルールでバリデーションする
date_format
では判定できそうに無いので、between:1,31
で0や99の入力を弾きます。
(2月や30日までの場合はどうすんだって感じですが…あくまで日付のバリデートなので)
…が、うまくいかない。
0でも99でも100でもエラーメッセージが出ません。
between
ルールの実装を見る
protected function validate_between($attribute, $value, $parameters)
{
$size = $this->size($attribute, $value);
return $size >= $parameters[0] and $size <= $parameters[1];
}
return
前の$size
を確認すると、1や2になっている。
$this->size()
が余計な変換をしているはず。
size
メソッドの実装を見る
protected function size($attribute, $value)
{
// This method will determine if the attribute is a number, string, or file and
// return the proper size accordingly. If it is a number, the number itself is
// the size; if it is a file, the kilobytes is the size; if it is a
// string, the length is the size.
if (is_numeric($value) and $this->has_rule($attribute, $this->numeric_rules))
{
return $this->attributes[$attribute];
}
elseif (array_key_exists($attribute, Input::file()))
{
return $value['size'] / 1024;
}
else
{
return Str::length(trim($value));
}
}
numeric_rule
が設定していなければsize()
は文字列の長さを返す模様。
なので、1は文字列長1、99は文字列長2でバリデーション成功、と。
マニュアルにも書いていました。
Laravel3
サイズ
与えられた文字数であること、もしくは数字項目の場合はその値であることをバリデートする属性です。
between:最小値,最大値
フィールドが指定された最小値と最大値の間のサイズであることをバリデートします。sizeルールと同様の判定方法で、文字列、数値、ファイルは評価されます。
size:値
フィールドは指定された値と同じサイズであることをバリデートします。文字列の場合、値は文字長です。数値項目の場合、値は整数値です。ファイルの場合、値はキロバイトのサイズです。
引用時に気づきましたがLaravel5(4にも?)にはこんなルールもあるんですね。こっちを使えば意図通りでしょうか。3にはナイヨー
digits_between:最小値,最大値
フィールドが整数で、桁数が最小値から最大値の間であることをバリデートします。
numeric_rules
は
protected $numeric_rules = array('numeric', 'integer');
integer
ルールを追加してバリデーションする
ここまでのルールはこんな感じ
'required|date_format:j|integer|between:1,31'
で、テストしようと数値を入力し、POSTした瞬間に気づく。
*「入力値なんだからstringじゃん。int型じゃないからinteger
ルールで弾かれるじゃん」*と。
しかし結果は成功。ちゃんと1~31までの入力に限定できました。
integerって整数値を判定しないで何を判定しているんだ。
と、なぜか成功したのに調べるハメに。
integer
ルールの実装を見る
protected function validate_integer($attribute, $value)
{
return filter_var($value, FILTER_VALIDATE_INT) !== false;
}
filter_var — 指定したフィルタでデータをフィルタリングする
ID | 名前 | オプション | フラグ | 説明 |
---|---|---|---|---|
FILTER_VALIDATE_INT | "int" | default, min_range, max_range | FILTER_FLAG_ALLOW_OCTAL, FILTER_FLAG_ALLOW_HEX | 値が整数であるかどうか、オプションで指定した範囲内にあるかどうかを検証します。 |
filter_var($value, FILTER_VALIDATE_INT)
の動作
ちょうど最近、投稿がありましたので引用させてもらいます。
php で 整数かチェックする(数値型文字列含む)
echo is_decimal(10); //true
echo is_decimal(10.01); //false
echo is_decimal("10"); //true
echo is_decimal("10.01"); //false
echo is_decimal("ABC"); //false
echo is_decimal(0x10); //true
echo is_decimal("0x10"); //false
function is_decimal($value) {
return filter_var($value, FILTER_VALIDATE_INT) !== false;
}
以上より、`integer`ルールは、引用元の題そのまま、整数(整数型文字列を含む)かチェックするルール、ということですね。
決して整数型であることの判定ではないですね。
#### `integer`ルールの欠点
`filter_var($value, FILTER_VALIDATE_INT) `が整数型文字列でもいいということは、入力値をキャストする必要がないので、ユーザー入力値のバリデーションに限れば歓迎することでした。
そして`between`ルールが有効になったことにより、`date_format`ルールも不要になりました。が!
別記事にはこんな事も……
["数字だけ"を判定する時に便利な関数](http://qiita.com/alphabet_h/items/d02edbfc2cc24cce1200)
>じゃあこっちのがいいじゃん!と思われるかもしれませんが、ちょっとまった!
実はこれ、'0123'という文字列を渡すと、falseを返してしまいます。
これは、0から始まる数字は10進数の整数として正しい形ではないと判断される為です。
オプションで指定できるFILTER_FLAG_ALLOW_OCTALフラグを用いて8進数表現(8進数の場合0から始まる必要がある)を許可することも可能ですが、「ゼロ (0) で始まる入力を八進数とみなします。 ゼロの後には 0-7 しか続けることができません。」と説明にもある通り'0912'のような数字はNGとなってしまいます。
>今回の条件は「変数が数字だけであるかどうか」なので、これらをfalseと返してしまうのは問題です。
また、"+123"という文字列を渡すと、trueを返してしまうので、コレも問題ですね。
ということで、`09`日なんて入力は弾かれてしまいます。
でも日付でこんな入力することって、無いこともないですよね。
`date_format`の時は受け入れていたので、`integer`を使わない方法を探します。
## `numeric`ルールを追加してバリデーションする
`between`ルールが入力を数値と判定するには、`integer`ルールの他にも`numeric`ルールを設定していればOKでした。
### `numeric`ルールの実装を見る
[Laravel3のValidator.php](https://github.com/laravel/laravel/blob/v3.2.14/laravel/validator.php)
```php:Validatorクラス
protected function validate_numeric($attribute, $value)
{
return is_numeric($value);
}
is_numeric — 変数が数字または数値形式の文字列であるかを調べる
これもキャスト無しで使えそうです。
指定した変数が数値であるかどうかを調べます。数値形式の文字列は以下の要素から なります。(オプションの)符号、任意の数の数字、(オプションの)小数部、 そして(オプションの)指数部。つまり、+0123.45e6 は数値として有効な値です。十六進表記(0xf4c3b00c など) や二進表記 (0b10100111001 など)、そして八進表記 (0777 など) も認められますが、この場合は符号や小数部、指数部を含めることはできません。
01
を許したら+1
もゆ、許された(と思ってましたがfilter_var
でも+1
はtrueでした)
整数以外の少数付きもtrueですし、惜しいですね。
帰ってきたdate_format
最終的には
- 0や32~99は許すけど+1とかは許さない
date_format
ルール - 0や32~は許さないが+1とかは許す
numeric|between:1,31
ルール
を組み合わせることで最強に見えました。
ctype_digit()
のススメ
最後に今後の展望的なものを
今回の問題点は既存の数値判定に厳密な整数を判定するものが無いことでした。
そこで、
"数字だけ"を判定する時に便利な関数
でも紹介されているように、ctype_digit()
関数を使えば、整数型文字列を正しく判定できるのではないかと
echo ctype_digit('+1'); // false
echo ctype_digit('1'); // true
echo ctype_digit('01'); // true
echo ctype_digit('1.1'); // false
これならばbetween
のみで入力値の制限が想定通りに出来そうです。
01を不正とする場合は………