PHPのin_arrayは罠が多いので注意喚起が必要

  • 242
    いいね
  • 12
    コメント
この記事は最終更新日から1年以上が経過しています。

最初に結論

in_arrayには第三引数trueを指定しろ。絶対にだ。

はじめに

in_arrayについては、はじめにPHP: in_array - Manualをよく読んでおいてほしい。

この記事に書いたコード断片は、どうか読むだけではなく自分で手を動かして確認してほしい。

PHPで短いコードを動かすのはPsySHを利用すると、とても捗る。ローカルに動作環境がなければ、Ideone.comなどのオンラインサービスを利用しても差支ない。

static $fruits = ["apple", "orange", "banana"];

in_array("apple", $fruits); // => true
in_array("mikan", $fruits); // => false

これがin_arrayのふつうの使ひかただ。

Webアプリケーションでの実例

では、次のような例を見てみよう。$_GETから入力を受け取る、かなりシンプルなWebアプリケーションのような想定だ。

<?php
$input_id = isset($_GET["id"]) ? $_GET["id"] : -1;

static $ids = [0, 1, 2, 5, 8];

if (in_array($input_id, $ids)) {
    $status = 200;
    $msg = "id = $input_id";
} else {
    $status = 404;
    $msg = "idはありません";
}

http_response_code($status);
echo "<p>" . htmlspecialchars($msg) . "</p>\n";

しかし、このコードには看過しがたいバグがある。?id=0.0と入力したとき、「idはありません」ではなく「id = 0.0」と表示されてしまふ。

状況を整理しよう。上記のコードをさらに簡略化したバージョンで説明する

何も入力しないとき

/** $_GET["id"] がないとき */
$input_id = isset($_GET["id"]) ? $_GET["id"] : -1;

// $input_id は -1
in_array($input_id, [0, 1, 2, 5, 8]); // false

これは意図通りだ。ちゃんと弾けてる。

整数を入力したとき

/** $_GET["id"] = "5" のとき */
$input_id = isset($_GET["id"]) ? $_GET["id"] : -1;

// $input_id は "5"
in_array($input_id, [0, 1, 2, 5, 8]); // true

この動作は、果たしてあなたの想像通りだろうか。あるいはPHPでは当然の柔軟さだと感じるだろうか。

確認しておきたいのは、第一引数が文字列であること、第二引数は整数の配列であることだ。

小数を入力したとき

/** $_GET["id"] = "5.0" のとき */
$input_id = isset($_GET["id"]) ? $_GET["id"] : -1;

// $input_id は "5.0"
in_array($input_id, [0, 1, 2, 5, 8]); // true

もう一度聞くが、この動作は、果たしてあなたの想像通りだろうか。あるいはPHPでは当然の柔軟さだと感じるだろうか。

注目したいのは、もちろん最後の行だ。どうしてこうなるのか。in_arrayの比較は==演算子の結果と同じだ。


 5  ==  5  ;  // true 当然
"5" == "5" ;  // true 当然


"5" ==  5  ;  // true これも
 5  == "5" ;  // true 良いよね

 5  == 5.0 ;  // true まあ
5.0 ==  5  ;  // true わかる

 5  == "5.0"; // true えっ
"5" == "5.0"; // true ええっ

これでもあなたは「PHPはシンプルな言語」だと呼び続けられるだろうか。

文字列

/** $_GET["id"] = "apple" のとき */
$input_id = isset($_GET["id"]) ? $_GET["id"] : -1;

// $input_id は "apple"
in_array($input_id, [0, 1, 2, 5, 8]); // true えええっ

簡略してない最初のバージョンに突っ込むと、「id = apple」と表示される。

例によって==演算子での比較を見てみよう。

"apple" == 0 // true
0 == "apple" // true

あなたはこれを見ても、本当に「PHPはシンプルな言語」だとry

改善

では、どうすればサンプルのWebアプリケーションが改善されるか検討しよう。

==を知る

PHPで仕事するなら型の相互変換PHP 型の比較表を穴が空くまで読んでおくことで、今回のような問題を「匂い」で察知することができるようになります。

in_arrayに第三引数を渡す

in_array

(PHP 4, PHP 5)
in_array — 配列に値があるかチェックする

説明

bool in_array ( mixed $needle , array $haystack [, bool $strict = FALSE ] )

needlehaystack を検索します。 strict が設定されていない限りは型の比較は行いません。

http://php.net/manual/ja/function.in-array.php 2015年2月8日の版より引用

in_arrayには第三引数がある。型の比較をするとかしないとか表現がわかりにくいのだけれど、実際はfalseのときは == で、trueのときは === で比較されると覚えておけばいい。

文字列が(十進)整数かどうか調べる

ここでは0-9の文字だけを使って十進数の整数を表現した文字列のことだ。

PHP: PHP 型の比較表 - Manualには「注意: HTMLフォームは整数、浮動小数点数、booleanを渡してはくれず、文字列を渡します。文字が数値であるかどうか確認するには、 is_numeric()を使うとよいでしょう。」などと書いてあるのだが、実はそれでは不十分だ。上の「小数を入力したとき」のような場合で弾くことができない。

逆に、is_numeric()のチェックをせずにintにキャストすればいいじゃない、との発想もあるだろう。

(int)"a"; // 0
(int)1.0; // 1

「大は小を兼ねる」の発想で「?id=1.0」と「?id=1」は同一視していいじゃないか、と見ることもできるかもしれない。しかし、筆者はWebアプリケーションとしては、意図しない状態は極力減らしておくべきだと考へる。SEOのような観点からも、ふとしたきっかけで無駄にクローリングされることになるかもしれず好ましい状態ではない。

ある文字列が整数を表現するかどうかを判定するには、以下のようなイディオムが有名だ。

$input = "0";
$input_is_int = (string)$input === (string)(int)$input;

あるいは次のような正規表現でもチェックできる。自分の直感でわかりやすい方を選んでいい。(性能差などは瑣末な問題だ)

$input = "0";
$input_is_int = preg_match('/\A(?:0|-?[1-9][0-9]*)\z/', 1) > 0;

次のような函数を用意しておけば、値がintでもstringでもfloatでも汎用的に利用できるのでべんりかもしれない。

/**
 * @param  int|float|string $value
 * @return boolean
 */
function is_intval($value)
{
    return !is_float($value) && (string)$value === (string)(int)$value;
}

改良バージョン

<?php

$is_intval = function ($value)
{
    return !is_float($value) && (string)$value === (string)(int)$value;
};

$input_id = isset($_GET["id"]) ? $_GET["id"] : -1;
$input_id = $is_intval($input_id) ? (int)$input_id : $input_id;

static $ids = [0, 1, 2, 5, 8];

if (in_array($input_id, $ids, true)) {
    $status = 200;
    $msg = "id = $input_id";
} else {
    $status = 404;
    $msg = "idはありません";
}

http_response_code($status);
echo "<p>" . htmlspecialchars($msg) . "</p>\n";

そもそもの話

in_array()が良くないって意見もある。

以下のようなコードでは、in_array()バージョンほど厄介な問題はおこらない。 (整数と小数の問題は健在)

static $ids = [
    0 => true,
    1 => true,
    2 => true,
    5 => true,
    8 => true,
];

if (isset($ids[$input_id])) {
    $status = 200;
    $msg = "id = $input_id";
} else {
    $status = 404;
    $msg = "idはありません";
}

また、in_array()は線形探索するため、配列の要素数によっては深刻な性能劣化を引き起こすことがあるだろう。

もちろん、ケースバイケースで良い点と悪い点が存在するのは当然のことだ。おのおののケースでissetin_array()のどちらが良いかは読者への課題としたい。

そのほかの話

なぜかストックが増えたことだし、せっかくなのでちょっぴり追記する。

  • array_search()in_arrayと同じ問題がある
  • switch文も同様だが、in_array()と違ってstrictオプションに相当するものはない
    • 少々過激ではあるが、筆者はswitchではなくelseifで書くことを推奨したい
    • ちなみにPythonにはswitchに相当する構文は存在しない

まとめ

筆者はin_array()が大好きだ。