Confession: I have written the same foreach loop maybe two thousand times. The one where you iterate over a collection, check a predicate, set a variable, and break. Every time I do it I think “there has to be a cleaner way” and every time I shrug and write the loop anyway, because the alternative was array_filter(...)[0] ?? null and that’s worse.
PHP 8.4 finally fixed this. It shipped four new functions, array_find, array_find_key, array_any, and array_all, and after a few weeks of using them I’ve deleted more boilerplate than I expected. This post is what I actually replaced, where they shine, and where I still reach for foreach.
The loop I’d been writing forever
Here’s the pattern. You’ve got a list of users, you want the first admin, you want null if there isn’t one. In PHP 8.3 and below, the cleanest version was something like this:
$admin = null;
foreach ($users as $user) {
if ($user->role === 'admin') {
$admin = $user;
break;
}
}
Four lines plus the break to remember. It’s fine. It works. But it’s noisy, and when you have five of them in a controller method you start scanning for the break line to confirm you didn’t typo a condition.
The “functional” alternative wasn’t much better:
$admin = array_filter($users, fn($u) => $u->role === 'admin')[0] ?? null;
This lies. array_filter preserves keys, so [0] only works if the first matching element happens to be at index 0. Realistically you needed array_values(array_filter(...))[0] ?? null, which is now an unreadable chain that allocates a whole array just to throw it away.
PHP 8.4 gives you this instead:
$admin = array_find($users, fn($u) => $u->role === 'admin');
Returns the first matching element, or null if nothing matches. Short-circuits. Doesn’t allocate. The PHP 8.4 release announcement covers the full feature list, but array_find and friends are the ones I miss the most when I’m working on an 8.3 codebase now.
The full set, and when I reach for each
There are four new functions and they all take the same (array, callable) shape. Here’s how I actually use them.
array_find($array, $fn) returns the first element where $fn($value, $key) is truthy, or null. Use when you want the matched value.
array_find_key($array, $fn) returns the key of the first match, or null. Use when you need the index back, often to mutate the array in place.
array_any($array, $fn) returns true if any element matches. Use for guards: “does this collection contain at least one X.”
array_all($array, $fn) returns true if every element matches. Use for invariants: “every item in the cart is in stock.”
A real example from a checkout flow:
// Before, PHP 8.3
$hasOutOfStock = false;
foreach ($cart->items as $item) {
if ($item->stock === 0) {
$hasOutOfStock = true;
break;
}
}
if ($hasOutOfStock) {
throw new CheckoutException('Some items are out of stock');
}
// After, PHP 8.4
if (array_any($cart->items, fn($i) => $i->stock === 0)) {
throw new CheckoutException('Some items are out of stock');
}
Two of these in a single method took my checkout validator from 38 lines to 22, and the diff was nice enough that the reviewer didn’t ask me a single question about it.
The original RFC on the PHP wiki is short and worth reading if you want the design context for why all four landed at once instead of just array_find.
Where I still write the foreach
The new functions aren’t a hammer for everything. Three cases where I still reach for foreach:
You need both the matched element and its index. array_find gives you the element, array_find_key gives you the key, but calling both walks the array twice. If you need both, write the loop.
$index = null;
$winner = null;
foreach ($entries as $i => $entry) {
if ($entry->score > 100) {
$index = $i;
$winner = $entry;
break;
}
}
You’re mutating as you iterate. The new functions are read-only. If you need to update each item, the old foreach ($items as &$item) pattern is still the cleanest path.
The predicate has side effects. Don’t put a database call or a log line inside an array_any callback. The short-circuit behavior is well defined, but anyone reading the code will assume the closure is pure. Reserve these functions for predicates that just look at the value.
If you’ve spent any time with the Laravel collection methods I wrote about recently, the mental model is the same. The new native functions are just the parts of Collection::first(), Collection::contains(), and Collection::every() that finally made it into the language itself, so you don’t need to wrap your data in a collection to get them.
A small bonus: better error messages
One thing I didn’t expect to notice. When I’d typo a predicate in array_filter and get back an empty array, debugging was “set a breakpoint, walk the loop, figure out which case isn’t matching.” With array_find returning null, my code throws on the very next line where I try to use the result, which I always intended would never be null because the data invariant said so. That’s a much faster failure than discovering it three function calls deep.
Small thing. But after years of squinting at filter chains it’s a meaningful upgrade.
Migration plan if you’re upgrading from 8.3
This is what I did across a Laravel 11 codebase last month. Took about ninety minutes.
First, search the codebase for foreach blocks ending in break. In most editors that’s a regex like foreach.*\n.*if.*\n.*=.*\n.*break. About two-thirds of mine were array_find candidates.
Second, search for array_filter followed by array_values or reset. Those are the “I wanted array_find but didn’t have it” cases. Replace them outright.
Third, search for boolean accumulator patterns: $has = false; foreach ... { if ... { $has = true; break; } }. Those are array_any candidates and they collapse to a single readable line.
Fourth, run your tests. The functions are pure and the semantics are obvious, so I had zero regressions across maybe eighty edits, but I wouldn’t ship without the safety net.
If you’re still on PHP 8.3 and not ready to upgrade, you can polyfill array_find in about six lines, but you’re not going to get the same engine-level optimizations. Worth waiting for the real 8.4 deploy.
The Symfony team’s PHP 8.4 compatibility notes (look for the polyfill section) cover a similar deprecation cleanup if you want to see how a large project handled the upgrade.
What to try this week
Grep your codebase for foreach. Find a file with three or more of them. Spend twenty minutes converting the ones that are checking a predicate. Read the result out loud. If your eyes stop pulling on the break keyword and the boilerplate just goes away, you’ll get why this matters.
I also keep a short notes page on PHP modernization on my site for things like this, and I’m happy to argue about which patterns are worth chasing on any given week. The honest answer for array_find is: yes, this one is worth the upgrade. It’s a small feature that quietly improves every list-processing function you’ll write for the next decade.