ユーザ入力の文字列が整数かどうかを検証するにあたって、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
の振る舞いも示す。整数文字表現はさまざまな形式が考えれるが、本稿では下記図表の「理想」にて「o」で示した値を整数、それ以外を非整数として扱う。
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
を渡すことで、フィルタリングに成功するとint
型が、失敗するとfalse
が返る。この仕様を利用して、値の検証に使われることがある。
assert(filter_var('0', FILTER_VALIDATE_INT) === 0);
assert(filter_var('A', FILTER_VALIDATE_INT) === false);
しかし、filter_var
には空白文字をtrimする性質があり'1 '
(1と半角スペース)についてはint(1)
を返す。この性質が許容できる場合はfilter_var
関数単体で十分と言えるが、そうでない場合はこれ単体での使用はおすすめできない。
assert(filter_var('1 ', FILTER_VALIDATE_INT) === 1);
また、正の整数の表現に+
をつけることが許される。string("+1")
のような入力の検証結果を妥当としたくない場合は、正規表現やstrpos
関数などで追加のチェックをする必要がある。
assert(filter_var('+1', FILTER_VALIDATE_INT) === 1);
ちなみに、Laravelのバリデーターはfilter_var
を使った実装になっているため、このケースと全く同じことが言える。LaravelのGitHubでは空白が入っている整数の場合はfalse
を返すようにして変更ほしいというプルリクエストが送られたことがあったが、下位互換性が壊れるという理由で既存のバリデーターを変更することは却下されている。
次のコードはfilter_var
の仕様を示す:
$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);
所感
PHPらしいさがあるが、厳密に整数かどうかをチェックしたいときは、is_numericはもちろんのことfilter_var、ctype_digitは避け、正規表現を使うか併用するのが良さそうです。自力で実装した際には、ここで示した入力値はPHPUnitのテストケースにぜひ組み込んでください。https://t.co/GwGTqBvBm9
— ❄️suin (@suin) 2018年8月18日
関連
- PHPのis_numeric関数は使うべきでないという話 - hnwの日記 - “私がこの関数について問題だと思うのは、「numeric」と言われたときに想像するものが人によって違う点です。”