Confession: I wrote the same eight-line array_filter plus current pattern for almost ten years before PHP 8.4 finally shipped array_find. A decade of current(array_filter($items, fn(...) => ...)) and squinting at it the next month wondering what it actually returned when nothing matched.
If you’ve been writing PHP since 7.x, you know the pattern. You need the first item in a list that matches some condition. The language gave you tools that didn’t quite fit, so you stitched them together. Then PHP 8.4 dropped array_find, array_find_key, array_any, and array_all, and a family of helpers I’d been hand-rolling for years became one line each.
I’m going to walk through what each one actually does, where I use them in production, and the one place where the old foreach is still cleaner. The PHP 8.4 release page is the canonical reference for the version overall.
What array_find actually returns
The signature looks innocent:
array_find(array $array, callable $callback): mixed
It returns the first value where the callback returns truthy, or null if nothing matches. That null is the part I want to flag, because it’s the part that tripped me up.
Old PHP code I deleted last month:
$user = current(array_filter($users, fn($u) => $u->email === $email));
if ($user === false) {
return null;
}
array_filter returns an array with original keys preserved. current on that array gives you the first element or false on an empty array. So you have to check for false, even though your data type is User, and you have to know that current is positioned wherever the filter started. Reading this six months later, the === false looks like a bug, not a feature.
The PHP 8.4 version:
$user = array_find($users, fn($u) => $u->email === $email);
if ($user === null) {
return null;
}
Three changes that matter. The return type is mixed not array|false. Null is unambiguous, so the static analyzer doesn’t have to think about whether false could be a valid value in your data. And there’s no intermediate filtered array allocated, which matters if your list is large.
array_any and array_all: the predicates I used to write inline
These are the boolean cousins. array_any returns true if at least one item matches the callback. array_all returns true only if every item matches.
Before PHP 8.4:
$hasAdmin = !empty(array_filter($users, fn($u) => $u->role === 'admin'));
$allVerified = count(array_filter($users, fn($u) => $u->verified))
=== count($users);
Both work. Both allocate a temporary array I don’t care about, and the array_all version walks the whole list even when the first failure is enough to answer the question.
After:
$hasAdmin = array_any($users, fn($u) => $u->role === 'admin');
$allVerified = array_all($users, fn($u) => $u->verified);
The functions short-circuit. array_any stops at the first match. array_all stops at the first failure. On a list of ten thousand items where the first one fails the predicate, that’s a real speedup. On a list of three items, it’s a wash and you pick array_all because it reads better.
These are equivalent to JavaScript’s Array.prototype.some and Array.prototype.every, which I’d been wishing for in PHP for years.
One small footgun: if your callback throws, array_any and array_all propagate the exception. That’s the right behavior, but I’ve seen people write predicates that call into network code or hit the database, then act surprised when one bad row blows up the whole check. Keep the predicates pure. If you need to do anything that can fail, do it in a separate pass and feed the result in.
array_find_key: when I care about position
array_find gives you the value. array_find_key gives you the key. Sometimes I want the key because I’m about to write back into the array, or because the keys are meaningful (think a Record<orderId, Order> shape).
$items = [
'order-1001' => new Order(...),
'order-1002' => new Order(...),
'order-1003' => new Order(...),
];
$failedKey = array_find_key($items, fn($o) => $o->status === 'failed');
if ($failedKey !== null) {
unset($items[$failedKey]);
}
The old version of this would be array_search combined with a custom predicate, which array_search doesn’t actually support. So you’d write a foreach with a manual break. The new version is one line and the type signature carries through.
There’s a quirk worth knowing. If your keys can legitimately be 0, null, or '', the “not found” sentinel of null collides with a real key. You’ll want a strict comparison and probably a manual array_key_exists check if you’re paranoid. The PHP manual entry for array_find_key covers the edge cases.
Where these still don’t help (and what to reach for)
I want to push back on the framing that these functions replace foreach. They don’t. They replace array_filter plus current and similar three-step chains. For anything more complex than a single predicate, a foreach is still cleaner.
A real example. I needed to find the first failed order in a list, but also collect every order processed before it, because the audit log needed both. With array_find I’d need two passes, one to find the index and one to slice. With foreach it’s one pass:
$processedBefore = [];
$targetOrder = null;
foreach ($orders as $order) {
if ($order->status === 'failed') {
$targetOrder = $order;
break;
}
$processedBefore[] = $order;
}
This is the kind of mixed-state iteration that doesn’t fit into a single predicate. Using array_find here would force me to either accept two passes or stop using these helpers entirely halfway through the function. The foreach is honest about what it’s doing.
Other places I still reach for the old patterns. When I need an index and a value together, I’ll often use array_map with array_keys even though it allocates, because the resulting code reads more like a transformation than a search. When the predicate is complex enough to deserve a named function, array_filter with a named callback can be clearer than a long arrow function passed to array_find.
There’s also the case where I’m working with a Generator rather than a plain array. None of the new helpers accept iterables. If you’ve been writing memory-efficient pipelines with yield, you still need to handle the search yourself or convert to an array first, which defeats the purpose. I keep hoping a future PHP release adds iter_find and friends. Until then, foreach over a generator is the cleanest answer.
I also still write small classes that wrap collection logic when the operations get hairy. My take on this is in the property hooks post I wrote a couple of months back, where the same “let me delete some boilerplate” argument applies to a different language feature.
What to try this week
Open your codebase. Search for array_filter calls followed by current or reset. Every one of those is a candidate for array_find. Then search for !empty(array_filter and count(array_filter and similar shapes. Those are array_any and array_all respectively.
You’ll probably find more than you expect. I found 23 in our backend monorepo on the first sweep, and a few more once I started looking at the longer functions. Pick the ones where the type narrowing helps your static analyzer. Psalm and PHPStan both understand these new helpers, so you’ll get tighter inference for free.
PHP 8.4 has been out for over a year at this point, so unless you’re stuck on an older version, there’s no reason not to lean on these. If your job involves the boring middle of a PHP codebase, my work on long-running PHP projects covers more of this kind of refactor. The cumulative effect of a couple hundred small simplifications like these is real.