130
97

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

PHP:文字列が整数かの検証にはis_numeric, ctype_digit, filter_varどれが適切か?

Last updated at Posted at 2018-08-18

ユーザ入力の文字列が整数かどうかを検証するにあたって、is_numericctype_digitは適切ではなく、アプリケーションの仕様によってはfilter_varも妥当でない場合がある。CakePHP3のバリデーションライブラリについても問題点があることが明らかになった。なお、Symfonyはis_intis_numericctype_digitなどに移譲される実装になっており、Laravelはfilter_varに依存しているため事情は同様である。

結論としては、filter_varがアプリケーションの仕様と合致する場合はそれを使い、そうでない場合は正規表現で検証するか組み合わせるほうが良い。CakePHP3のバリデーションライブラリを使うときも同様である。ちなみに、文字列に対してis_intintvalをかけることは何の検証にもならないので注意されたし。

参考までにis_numericfilter_varctype_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);

所感

関連

130
97
6

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
130
97

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?