PHPのarray_searchは先頭の要素で0を返す ifで判定すると見つかったのに見つからない扱いになる理由
array_search()は、配列から値を探して最初に見つかったキーを返す関数です。見つからなければfalseを返すので、先頭の要素が見つかったときの0とfalseを取り違えないのが最大のポイントです。
「見つかったらキーが返る」という性質は便利なのですが、そのキーが0だったときにifでそのまま判定してしまい、見つかったのに見つからない扱いにしてしまう。この手のバグ、私も実務で何度か見かけましたし、自分でも書いた覚えがあります。
先頭の要素を探すと0が返ってif文が偽になる
配列の添字は0から始まるので、先頭の要素のキーは0です。0はifの条件では偽として扱われます。ここが衝突点です。
<?php
$users = ['alice', 'bob', 'carol'];
$pos = array_search('alice', $users);
var_dump($pos); // int(0)
if ($pos) {
echo "found\n";
} else {
echo "not found?\n"; // こちらが出てしまう
}
見つからなかったときのfalseと、先頭で見つかったときの0は、ifの目には同じに映ります。たいていは先頭以外ならうまく動くので、テストをすり抜けて本番で初めて壊れる、というパターンになりがちです。
判定は!== falseで書く
型まで比べる厳密比較で「falseではない」と書けば、0でも正しく「見つかった」と判断できます。
<?php
var_dump(array_search('dave', $users)); // bool(false)
if ($pos !== false) {
echo "found ok\n";
}
これで、先頭の要素でも途中の要素でも同じ書き方で安全に判定できます。公式ドキュメントにも、戻り値の判定には===か!==を使うようにという注意書きがあります。
見つかった値を取り出すときはfalseのまま添字に使わない
見つからないときのfalseをそのまま$users[$idx]に渡すと、falseが添字として0に変換されて、先頭の要素を拾ってしまう危険があります。いったん変数に受けて、判定してから使うほうが安全です。
<?php
$idx = array_search('dave', $users);
$found = $idx !== false ? $users[$idx] : null;
var_dump($found); // NULL
「見つからなければnull」と決めておけば、呼び出し側では??で既定値を与えやすくなります。
第3引数のstrictを付けないと緩い比較になる
array_search()の第3引数にtrueを渡すと、値の型まで厳密に比べます。省略すると==と同じ緩い比較になるので、数値っぽい文字列は数値として比べられます。
<?php
var_dump(array_search('1e1', ['10', 'x'])); // int(0) ← 数値として10と等しい
var_dump(array_search('abc', [0, 'abc'])); // int(1)
var_dump(array_search('abc', [0, 'abc'], true)); // int(1)
2行目と3行目は、PHP 8では結果が同じになります。PHP 7までは0と’abc’が緩い比較で等しいとされ、2行目がint(0)になっていました。古いコードを引き継いだときは、この違いで挙動が変わっていないか気にしておくといいと思います。IDやコード値のように「型まで一致してほしい」探索では、迷わずstrictをtrueにしておくのが無難です。
同じ値が複数あるときはarray_keys()で全部取る
array_search()が返すのは最初のキー1つだけです。重複があって全部ほしいときは、array_keys()の第2引数に探したい値を渡します。
<?php
$tags = ['php', 'js', 'php'];
var_dump(array_search('php', $tags)); // int(0)
print_r(array_keys($tags, 'php')); // [0, 2]
array_keys()も第3引数でstrictを指定できます。「最初の1件」ならarray_search()、「全部」ならarray_keys()と使い分ければ、ループを自前で書かずに済みます。
存在確認だけならin_array()か、キーを逆引きに回す
キーが要らず、あるかないかだけを知りたいなら、in_array()のほうが素直です。こちらは最初からtrue/falseを返すので、0とfalseの取り違えは起きません。
<?php
var_dump(in_array('alice', $users, true)); // bool(true)
// 何度も探すなら、array_flip()でキーと値を入れ替えておく
$flip = array_flip($users);
var_dump($flip['bob'] ?? null); // int(1)
var_dump(isset($flip['alice'])); // bool(true)
同じ配列を何度も検索するなら、先に一度だけarray_flip()してisset()で引くほうが、毎回先頭から走査するより速くなります。ただし、array_flip()は値が重複すると後ろのものが残るので、重複がある配列には向きません。また値が整数か文字列でないと使えない点にも注意です。
まとめ
array_search()の戻り値は「キーか、false」という二択で、先頭ではキーが0になるせいでifと相性が悪い、というだけの話です。!== falseで判定する、strictは基本trueにする、全部ほしいならarray_keys()、存在確認だけならin_array()。この4つを押さえておけば、ほぼ困らないと思います。
よくある質問
Q. array_search()の戻り値はどう判定すればいいですか?
A. $result !== false で判定します。if ($result) と書くと、先頭要素のキー0が偽として扱われ、見つかったのに見つからない扱いになります。
Q. 同じ値が複数あるときはどれが返りますか?
A. 最初に見つかったキーだけが返ります。すべてのキーがほしい場合は array_keys($array, $value) を使ってください。
Q. 存在するかどうかだけ知りたいときもarray_search()を使うべきですか?
A. 使わなくて大丈夫です。in_array() はtrue/falseを返すので、0とfalseを取り違える心配がなく、意図もコードから読み取りやすくなります。