ユーザ入力の文字列が整数かどうかを検証するにあたって、is_numeric
やctype_digit
を用いるのはもちろん、filter_var
も妥当とは言えず、CakePHP3のバリデーションライブラリにも問題点があることが明らかになった。なお、Symfonyはis_int
やis_numeric
、ctype_digit
などに移譲される実装になっており、Laravelはfilter_var
を使用していたため同様の問題があることが分かった。
結論としては、正規表現で検証することを推奨したい。filter_var
やCakePHP3のバリデーションライブラリを使うときも正規表現と組み合わせたほうが安全である。ちなみに、文字列に対してis_int
やintval
をかけることは何の検証にもならないので注意されたし。
参考までにis_numeric
、filter_var
、ctype_digit
そしてCakePHPのバリデーターと評価結果の関係性をまとめた表を示す。背景色が緑のところがtrue
が返るパターンだ。加えて、参考までにintval
の振る舞いも示す。
is_numeric
is_numeric
は「数字かどうか」をチェックする関数であり、整数に限らなれない。少数もtrue
になるだけでなく、指数表記やPHP_INT_MAX
を超える数もtrue
扱いである。したがって、「整数かどうか」をチェックする場面では適切ではない。
assert(is_numeric('0'));
assert(is_numeric('1'));
assert(is_numeric('1 ') === false); // 1と半角スペース
assert(is_numeric('a') === false);
assert(is_numeric('0.0'));
assert(is_numeric('.123'));
assert(is_numeric('123.'));
assert(is_numeric('-1'));
assert(is_numeric('-1.0'));
assert(is_numeric('+1'));
assert(is_numeric('+1.0'));
assert(is_numeric('042')); // 8進数
assert(is_numeric('08')); // 0で始まる10進数「8」
assert(is_numeric('1e2')); // 指数表現で100
assert(is_numeric('1.001e2')); // 指数表現100.1
assert(is_numeric('1e+2')); // 指数表現で100
assert(is_numeric('1e-2')); // 指数表現で0.01
assert(is_numeric('0xA') === false); // 16進数で10
assert(is_numeric('9223372036854775807')); // PHP_INT_MAX
assert(is_numeric('9223372036854775808')); // PHP_INT_MAX + 1
assert(is_numeric('-9223372036854775808')); // PHP_INT_MIN
assert(is_numeric('-9223372036854775809')); // PHP_INT_MIN - 1
assert(is_numeric('INF') === false);
assert(is_numeric('NAN') === false);
ctype_digit
ctype_digit
は「数字かどうかを調べる」関数である。文字通り文字列が半角数字文字'0'
~'9'
だけで構成されているかをチェックするものである。負の数はfalse
になる。したがって、「整数かどうか」をチェックする場面では適切ではない。
assert(ctype_digit('0'));
assert(ctype_digit('1'));
assert(ctype_digit('1 ') === false); // 1と半角スペース
assert(ctype_digit('a') === false);
assert(ctype_digit('0.0') === false);
assert(ctype_digit('.123') === false);
assert(ctype_digit('123.') === false);
assert(ctype_digit('-1') === false);
assert(ctype_digit('-1.0') === false);
assert(ctype_digit('+1') === false);
assert(ctype_digit('+1.0') === false);
assert(ctype_digit('042')); // 8進数
assert(ctype_digit('08')); // 0で始まる10進数「8」
assert(ctype_digit('1e2') === false); // 指数表現で100
assert(ctype_digit('1.001e2') === false); // 指数表現100.1
assert(ctype_digit('1e+2') === false); // 指数表現で100
assert(ctype_digit('1e-2') === false); // 指数表現で0.01
assert(ctype_digit('0xA') === false); // 16進数で10
assert(ctype_digit('9223372036854775807')); // PHP_INT_MAX
assert(ctype_digit('9223372036854775808')); // PHP_INT_MAX + 1
assert(ctype_digit('-9223372036854775808') === false); // PHP_INT_MIN
assert(ctype_digit('-9223372036854775809') === false); // PHP_INT_MIN - 1
assert(ctype_digit('INF') === false);
assert(ctype_digit('NAN') === false);
filter_var
filter_var
は値の検証を目的に使われる関数であり、第一引数に検証する値、第二引数にFILTER_VALIDATE_INT
を渡すことで、整数かどうかをチェックすることができる。しかし、これにも罠があり'1 '
(1と半角スペース)についてはtrue
を返してしまう。他の関数に比べると理想に最も近いが、前述の罠があるため、これ単体での使用はおすすめできない。
ちなみに、Laravelのバリデーターはfilter_var
を使った実装になっているため、このケースと全く同じことが言える。LaravelのGitHubでは空白が入っている整数の場合はfalse
を返すようにして変更ほしいというプルリクエストが送られたことがあったが、下位互換性が壊れるという理由で既存のバリデーターを変更することは却下されている。
$filter_var = function (string $value): bool {
return filter_var($value, FILTER_VALIDATE_INT) !== false;
};
assert($filter_var('0'));
assert($filter_var('1'));
assert($filter_var('1 ')); // 1と半角スペース
assert($filter_var('a') === false);
assert($filter_var('0.0') === false);
assert($filter_var('.123') === false);
assert($filter_var('123.') === false);
assert($filter_var('-1'));
assert($filter_var('-1.0') === false);
assert($filter_var('+1'));
assert($filter_var('+1.0') === false);
assert($filter_var('042') === false); // 8進数
assert($filter_var('08') === false); // 0で始まる10進数「8」
assert($filter_var('1e2') === false); // 指数表現で100
assert($filter_var('1.001e2') === false); // 指数表現100.1
assert($filter_var('1e+2') === false); // 指数表現で100
assert($filter_var('1e-2') === false); // 指数表現で0.01
assert($filter_var('0xA') === false); // 16進数で10
assert($filter_var('9223372036854775807')); // PHP_INT_MAX
assert($filter_var('9223372036854775808') === false); // PHP_INT_MAX + 1
assert($filter_var('-9223372036854775808')); // PHP_INT_MIN
assert($filter_var('-9223372036854775809') === false); // PHP_INT_MIN - 1
assert($filter_var('INF') === false);
assert($filter_var('NAN') === false);
CakePHP3のバリデーター
CakePHP3のバリデーターで整数かどうかをチェックしてくれるのがCake\Validation\Validation::isInteger()メソッドだ。filter_var
と近いふるまいをするが、8進数やPHP_MAX_INT
などは考慮されていないため、これ単体だけでバリデーションするのは避けたほうが良い。
/**
* Check that the input value is an integer
* This method will accept strings that contain only integer data
* as well.
* @param string $value The value to check
* @return bool
*/
function cakephp($value) // ライブラリから抜粋した実装
{
if (!is_scalar($value) || is_float($value)) {
return false;
}
if (is_int($value)) {
return true;
}
return (bool) preg_match('/^-?[0-9]+$/', $value);
}
assert(cakephp('0'));
assert(cakephp('1'));
assert(cakephp('1 ') === false); // 1と半角スペース
assert(cakephp('a') === false);
assert(cakephp('0.0') === false);
assert(cakephp('.123') === false);
assert(cakephp('123.') === false);
assert(cakephp('-1'));
assert(cakephp('-1.0') === false);
assert(cakephp('+1') === false);
assert(cakephp('+1.0') === false);
assert(cakephp('042')); // 8進数
assert(cakephp('08')); // 0で始まる10進数「8」
assert(cakephp('1e2') === false); // 指数表現で100
assert(cakephp('1.001e2') === false); // 指数表現100.1
assert(cakephp('1e+2') === false); // 指数表現で100
assert(cakephp('1e-2') === false); // 指数表現で0.01
assert(cakephp('0xA') === false); // 16進数で10
assert(cakephp('9223372036854775807')); // PHP_INT_MAX
assert(cakephp('9223372036854775808')); // PHP_INT_MAX + 1
assert(cakephp('-9223372036854775808')); // PHP_INT_MIN
assert(cakephp('-9223372036854775809')); // PHP_INT_MIN - 1
assert(cakephp('INF') === false);
assert(cakephp('NAN') === false);
intval
intval
は検証のための関数でないため、検証に使うのは不適切だが、参考までにそのふるまいを示す。
assert(intval('0') === 0);
assert(intval('1') === 1);
assert(intval('1 ') === 1); // 1と半角スペース
assert(intval('a') === 0);
assert(intval('0.0') === 0);
assert(intval('.123') === 0);
assert(intval('123.') === 123);
assert(intval('-1') === -1);
assert(intval('-1.0') === -1);
assert(intval('+1') === 1);
assert(intval('+1.0') === 1);
assert(intval('042') === 42); // 8進数
assert(intval('08') === 8); // 0で始まる10進数「8」
assert(intval('1e2') === 100); // 指数表現で100
assert(intval('1.001e2') === 100); // 指数表現100.1
assert(intval('1e+2') === 100); // 指数表現で100
assert(intval('1e-2') === 0); // 指数表現で0.01
assert(intval('0xA') === 0); // 16進数で10
assert(intval('9223372036854775807') === PHP_INT_MAX); // PHP_INT_MAX
assert(intval('9223372036854775808') === PHP_INT_MAX ); // PHP_INT_MAX + 1
assert(intval('-9223372036854775808') === PHP_INT_MIN); // PHP_INT_MIN
assert(intval('-9223372036854775809') === PHP_INT_MIN); // PHP_INT_MIN - 1
assert(intval('INF') === 0);
assert(intval('NAN') === 0);