Laravel の数値バリデーション結果に違和感があるので調べてみると、内部的には PHP の標準関数を使っていました。他の PHP 標準関数で自分の感覚と合うものがないか、6 つの方法で結果を比較してみます。
今回試したのは以下の6種類+1種類です。
$result = [
'A' => is_int($item),
'B' => is_float($item),
'C' => is_numeric($item),
'D' => ctype_digit($item),
'E' => filter_var($item, FILTER_VALIDATE_INT) !== false,
'F' => filter_var($item, FILTER_VALIDATE_FLOAT) !== false,
'G' => is_string($item) && preg_match('/^[+-]?[0]*[1-9]*[.]?[0-9]*$/', $item) === 1,
];
G の正規表現は、 BC Math のマニュアルにあるものを使用しました。(よく見ると間違ってますね。これだと 10.1 などが通りません)
バージョンは 7.3, 7.4 で試しました。結果は同じです。検証用のコードはこちらに置きました。
ひとまず全結果を貼り付けます。
# | 入力値 | 詳細 | A | B | C | D | E | F | G |
---|---|---|---|---|---|---|---|---|---|
1 | 1 | int(1) | 1 | 1 | 1 | 1 | – | ||
2 | 01 | int(1) | 1 | 1 | 1 | 1 | – | ||
3 | 0b1 | int(1) | 1 | 1 | 1 | 1 | – | ||
4 | 0x1 | int(1) | 1 | 1 | 1 | 1 | – | ||
5 | -1 | int(-1) | 1 | 1 | 1 | 1 | – | ||
6 | ‘1’ | string(1) “1” | 1 | 1 | 1 | 1 | 1 | ||
7 | ’01’ | string(2) “01” | 1 | 1 | 1 | 1 | |||
8 | ‘0b1’ | string(3) “0b1” | |||||||
9 | ‘0x1’ | string(3) “0x1” | |||||||
10 | ‘+1’ | string(2) “+1” | 1 | 1 | 1 | 1 | |||
11 | ‘-1’ | string(2) “-1” | 1 | 1 | 1 | 1 | |||
12 | 0.9 | float(0.9) | 1 | 1 | 1 | – | |||
13 | -0.9 | float(-0.9) | 1 | 1 | 1 | – | |||
14 | ‘0.9’ | string(3) “0.9” | 1 | 1 | 1 | ||||
15 | ‘+0.9’ | string(4) “+0.9” | 1 | 1 | 1 | ||||
16 | ‘-0.9’ | string(4) “-0.9” | 1 | 1 | 1 | ||||
17 | ‘0’ | string(1) “0” | 1 | 1 | 1 | 1 | 1 | ||
18 | ‘+0’ | string(2) “+0” | 1 | 1 | 1 | 1 | |||
19 | ‘-0’ | string(2) “-0” | 1 | 1 | 1 | 1 | |||
20 | ‘1337e0’ | string(6) “1337e0” | 1 | 1 | |||||
21 | ‘1.2e3’ | string(5) “1.2e3” | 1 | 1 | |||||
22 | ‘7E-10’ | string(5) “7E-10” | 1 | 1 | |||||
23 | ‘1_234_567’ | string(9) “1_234_567” | |||||||
24 | ‘1_234.567’ | string(9) “1_234.567” | |||||||
25 | ‘ 1’ | string(3) ” 1″ | 1 | 1 | 1 | ||||
26 | ‘ 1 ‘ | string(3) ” 1 “ | 1 | 1 | |||||
27 | ‘1,000’ | string(7) “1,000” | |||||||
28 | ‘10000000000000000000001’ | string(23) “10000000000000000000001” | 1 | 1 | 1 | 1 | |||
29 | ‘0.10000000000000000000001’ | string(25) “0.10000000000000000000001” | 1 | 1 | 1 | ||||
30 | ‘not numeric’ | string(11) “not numeric” | |||||||
31 | true | bool(true) | 1 | 1 | |||||
32 | false | bool(false) | |||||||
33 | null | NULL | |||||||
34 | [] | array(0) {} |
1 は true に読み替えてください。空白の部分は false です。BC Math は文字列型しか受け付けないので、 int や float は – にしています。
is_int, is_float
is_int, is_float が true になるのは int, float の時なので単純で分かりやすいです。「数値形式の文字列」は false になります。
is_numeric
is_numeric は数字または「数値形式の文字列」の場合に true になります。Laravel の numeric ルールもこれを使用しています。ここで気を付けたいのは以下の形式も true になる点です。
- +ありの数値
- 左側が0埋めの数値
- 指数表記(科学的表記法。 e がつくやつ)
- 頭にホワイトスペース(数値や改行など)がつく数値(PHP 8 以降は後ろのホワイトスペースも)
- INT の範囲外の(PHP_INT_MIN 未満や PHP_INT_MAX を超える)数値
- 浮動小数点の精度では扱えない(丸めや誤差が生じる)数値
- BC Math で扱えない数値(指数表記やホワイトスペースありの数値は BC Math で扱えない)
ctype_digit
ctype_digit は10進数値かどうかを判定します。ただし、 int 型の数値を渡すと ASCII コードのコード番号として扱うので、 int(1) は SOH であり 10 進数値ではないと判断されます。
これを通す前に is_string を通さないと、変なバグを生みそうです。
filter_var
filter_var の FILTER_VALIDATE_FLOAT, FILTER_VALIDATE_INT は float 型、int 型の数値かを見ているらしいです(詳細な仕様はドキュメントから見つけられませんでした)。Laravel の integer ルールとして FILTER_VALIDATE_INT が採用されています。
以下の点に要注意です。
- true は数値( false は数値では無い)
- INT の範囲外(PHP_INT_MIN 未満や PHP_INT_MAX を超える)の数値でも true
- 浮動小数点の精度では扱えない(丸めや誤差が生じる)数値も true
- 前後にホワイトスペース(数値や改行など)がつく数値も true
- BC Math (任意精度数学関数) で扱えない数値も true になる場合がある
- 左0埋めは INT ではない。FLOAT ではある。
Laravel の TrimStrings ミドルウェア
Laravel には標準で TrimStrings ミドルウェアが設定されています。入力パラメーターに含まれる前後のホワイトスペースを自動で除去するものです。
バリデーションを通す前にホワイトスペースを取り除いているので、上記で紹介したホワイトスペースを許容するチェック処理でも問題ないという事なのだと思います。
まとめ
is_int, is_float 以外の文字列を検査する関数に関しては、私の感覚(/\A(-0(?=[.])|0|-?[1-9][0-9]*)([.][0-9]+)?\z/
)とは違う結果を返すことがわかりました。
また、 BC Math を使う際は数値以外の文字列を 0 として扱うので事前チェックが重要なのですが、それを検知する関数はないようです。(PHP 7.4 以降は Warning: bccomp(): bcmath function argument is not well-formed
という Warning を吐いてくれるようになりました)
「数値形式の文字列」をチェックする場合は、自分で正規表現を書いた方がよさそうです。