array_findとarray_any/array_allで配列の探索を素直に書く(PHP 8.4)
array_find は、配列から条件に合う最初の値を返す関数です。PHP 8.4 で array_find_key / array_any / array_all と一緒に追加されました。どれもコールバックで判定します。
「条件に合う最初の1件が欲しい」だけなのに、foreach を回して break する数行を書く。あるいは array_filter して current() で先頭を取る。どちらも動くのですが、書くたびに「これ毎回書いてるな」と思っていました。PHP 8.4 で、ようやくそこに名前が付きました。
これまでは何が面倒だったのか
従来の書き方は、だいたい次の2通りだったと思います。どちらにも小さな不満があります。
<?php
// (1) foreach + break。冗長だが確実
$found = null;
foreach ($users as $user) {
if ($user->isAdmin()) {
$found = $user;
break;
}
}
// (2) array_filter + current。短いが罠がある
$found = current(array_filter($users, fn($u) => $u->isAdmin()));
// 見つからないと false(null ではない)
// しかも array_filter は全要素を走査してから捨てている
(2) は短く見えて、見つからなかったときの戻り値が false になります。null を期待して ?? で受けると、思ったとおりに動きません。それに、先頭の1件しか要らないのに全件のコールバックを実行しているのも、地味に無駄です。
array_find はどう書く?
4つの関数はすべて (array $array, callable $callback) という同じ形です。コールバックには 第1引数に値、第2引数にキー が渡されます。
<?php
$nums = [1, 3, 4, 7, 8];
// 条件に合う最初の「値」。無ければ null
array_find($nums, fn($v) => $v % 2 === 0); // 4
// 条件に合う最初の「キー」。無ければ null
array_find_key($nums, fn($v) => $v % 2 === 0); // 2
// 1つでも合えば true(空配列は false)
array_any($nums, fn($v) => $v % 2 === 0); // true
// 全部合えば true(空配列は true)
array_all($nums, fn($v) => $v > 0); // true
嬉しいのは、意図が関数名になったことです。array_any と書いてあれば「存在チェックだな」と一目で分かります。count(array_filter(...)) > 0 より読む速度が明らかに速い。
短絡評価されるので、重い判定ほど効く
4つとも短絡評価です。array_find と array_any は最初に真を返した時点で、array_all は最初に偽を返した時点で、そこから先のコールバックを呼びません。
<?php
// 判定が重い(DBアクセスや外部API)ほど差が出る
$hasInvalid = array_any($rows, fn($r) => $validator->isBroken($r));
// 1件目で壊れていたら、残り9999件は検査しない
逆に言うと、コールバックに副作用(ログ出力やカウンタ加算)を書くと途中で止まります。ここはフィルタではなく探索の関数だ、と割り切って使うのが安全です。
null が返ってきたとき、それは「無い」のか「null が入っていた」のか
これが一番ハマりやすいところだと思います。array_find は見つからないとき null を返しますが、見つかった値そのものが null のときも null を返します。区別がつきません。
<?php
$data = ['a' => 1, 'b' => null, 'c' => 3];
array_find($data, fn($v) => $v === null); // null(見つかっているのに)
array_find_key($data, fn($v) => $v === null); // "b" ← 存在が分かる
// 値に null が混ざりうるなら、キーで判定する
$key = array_find_key($data, fn($v) => $v === null);
if ($key !== null) {
// 見つかった
}
値に null が入りうる配列を扱うときは、素直に array_find_key を使うのがいいと思います。DBから引いた行の配列など、NULL 列が混ざる場面では現実的にありえる話です。
空配列のときの答えを覚えておく
array_all([], ...) は true、array_any([], ...) は false です。数学の全称・存在量化と同じ規約で、他言語の every / some とも揃っています。
<?php
// 「全員が同意済みなら送信」のつもりが、0人でも通ってしまう
if (array_all($members, fn($m) => $m->agreed)) {
$this->send();
}
// 意図がそうでないなら、空チェックを明示する
if ($members !== [] && array_all($members, fn($m) => $m->agreed)) {
$this->send();
}
仕様として正しい挙動なのですが、業務ロジックの「全員」は暗黙に「1人以上いる前提」であることが多い。ここは事故になりやすいので、空チェックを一言足しておく方が親切だと思います。
なぜ組み込み関数をそのまま渡すと落ちるのか
コールバックには値とキーの2つが必ず渡されます。ユーザー定義関数やクロージャは引数を少なく宣言しても問題ありませんが、内部関数(組み込み関数)は引数の数が厳密なので、そのまま渡すと ArgumentCountError になります。
<?php
// NG: is_numeric() は引数1つ。値とキーの2つが渡されて落ちる
array_any($values, 'is_numeric');
// ArgumentCountError: is_numeric() expects exactly 1 argument, 2 given
// OK: 値だけ受けるクロージャで包む
array_any($values, fn($v) => is_numeric($v));
array_filter に ARRAY_FILTER_USE_BOTH を付けたときと同じ罠です。ファーストクラス callable 構文で is_numeric(...) と書いても内部関数の引数の数は変わらないので、包むならクロージャで引数を絞るのが確実です。
8.4 に上げられない環境では
これらは実装が単純なので、自前のヘルパーでも困りません。composer で入れるなら polyfills/array-find という polyfill もあります。先に呼び出し側の書き方だけ揃えておくと、バージョンを上げたときに実装を消すだけで済みます。
<?php
if (!function_exists('array_find')) {
function array_find(array $array, callable $callback): mixed {
foreach ($array as $key => $value) {
if ($callback($value, $key)) {
return $value;
}
}
return null;
}
}
まとめ
やっていること自体は foreach と変わりません。ただ、探索なのかフィルタなのか存在チェックなのかが関数名で分かるようになったのは、読む側にとって地味に大きいと思っています。注意点は「値が null だと見分けがつかない」「空配列で array_all は true」「内部関数はそのまま渡せない」の3つくらい。この3つさえ頭に置いておけば、あとは素直に置き換えていける関数だという気がしています。
よくある質問
Q. array_find と array_search は何が違いますか?
A. array_search は「値そのものが一致するか」を比較してキーを返します。array_find はコールバックで条件判定して値を返します。条件判定でキーが欲しいときは array_find_key です。
Q. コールバックでキーも使えますか?
A. 使えます。第1引数が値、第2引数がキーの順で渡されます。fn($v, $k) => is_numeric($k) のように書けば、キーだけで判定することもできます。
Q. array_filter と使い分ける基準は?
A. 結果として複数件の配列が欲しいなら array_filter、1件だけ、または真偽値が欲しいなら array_find 系です。後者を array_filter で書くと、全件走査したうえで結果を捨てることになります。