Every codebase I inherit has the same loop in it. Declare a variable, set it to null, foreach over a collection, flip the variable on the first hit, break. Five lines to answer one question: is there a match, and what is it? PHP 8.4 finally ships the array functions that collapse that pattern into a single expression — array_find, array_find_key, array_any and array_all. Property hooks and lazy objects got the PHP 8.4 headlines, but alongside property hooks these four helpers are the ones I reach for almost daily.
Find the first match with array_find and array_find_key#
array_find(array $array, callable $callback): mixed walks the array and returns the value of the first element where the callback returns true. No match, you get null. It stops the moment it finds one — no wasted iterations.
Here is the loop I keep deleting:
$pending = null;
foreach ($orders as $order) {
if ($order->status === 'pending') {
$pending = $order;
break;
}
}
And the replacement:
$pending = array_find($orders, fn (Order $order) => $order->status === 'pending');
One line, same short-circuit behaviour, and the intent reads straight off the page. When you need the key instead of the value, reach for array_find_key(array $array, callable $callback): mixed:
$drivers = ['cache' => 'redis', 'queue' => 'sqs', 'mail' => 'ses'];
$key = array_find_key($drivers, fn (string $driver) => $driver === 'sqs');
// 'queue'
The callback's signature is ($value, $key) — value first, key second. That ordering trips people up if they are used to writing comparison callbacks, so it is worth burning in. The second argument lets you match on the key when you need to:
// Match on the key, not the value.
$found = array_find(
$drivers,
fn (string $value, string $key) => str_starts_with($key, 'qu'),
);
// 'sqs'
Check the whole array with array_any and array_all#
Sometimes you do not want the element — you want a yes/no. array_any returns true if at least one element passes the callback; array_all returns true only if every element does. Both short-circuit on the first decisive element.
array_any kills the boolean-flag loop:
// Before
$hasAdmin = false;
foreach ($users as $user) {
if ($user->isAdmin()) {
$hasAdmin = true;
break;
}
}
// After
$hasAdmin = array_any($users, fn (User $user) => $user->isAdmin());
array_all is the one I use for validation guards — "are all of these in a state I can proceed from?":
$readyToShip = array_all(
$lineItems,
fn (LineItem $item) => $item->isInStock(),
);
Both take the same ($value, $key) callback, so you can assert on keys too. They read like the question you are actually asking, which is the whole point.
How array_find differs from array_filter and in_array#
This is the part the docs gloss over. array_filter and array_find look similar but do different work. array_filter evaluates every element and builds a brand-new array of all the matches. If you only want the first one, you are doing extra allocation and then fishing the head out:
// array_filter builds the whole result set, then you grab the first.
$matches = array_filter($numbers, fn (int $n) => $n % 2 === 0);
$firstEven = reset($matches) ?: null;
// array_find stops at the first match and hands it back directly.
$firstEven = array_find($numbers, fn (int $n) => $n % 2 === 0);
Use array_filter when you genuinely want all the matches. Use array_find when you want one. Same logic for the boolean pair: array_any is not array_filter(...) !== [], because array_any bails on the first hit instead of scanning the lot.
in_array is a different tool again — it only does value equality. The moment your match needs a callback, it can't help you:
// in_array: equality only.
in_array('admin', $roles, true);
// array_any: any condition you can express.
array_any($users, fn (User $u) => $u->role === 'admin' && $u->isActive());
One caveat worth stating plainly: these are plain-array functions. If you are already holding an Eloquent Collection, its first(), contains() and every() methods read better and chain with the rest of your pipeline — and for genuinely large datasets you would stream with Laravel's lazy collections rather than pull everything into an array first. Reach for the native functions when you are working with raw arrays, not when you have already paid for a Collection.
Using them before PHP 8.4#
Not every project is on 8.4 yet. You do not have to wait. The symfony/polyfill-php84 package backports all four functions to older runtimes, and most Laravel apps already pull Symfony components in transitively, so it is a small addition:
composer require symfony/polyfill-php84
Once it is installed the functions are defined globally, so the exact same array_find($orders, ...) call works on PHP 8.1, 8.2 or 8.3. When you do upgrade to 8.4 the native implementations take over and you can drop the polyfill with no code changes. There is also a focused polyfills/array-find package if you want only these functions and nothing else.
Gotchas and Edge Cases#
The null return from array_find is ambiguous. If null is a legitimate value in your array, a null result could mean "I found a null" or "I found nothing" — you cannot tell them apart:
$values = ['a' => null, 'b' => 2];
array_find($values, fn ($v) => $v === null); // null — found it, but looks empty
array_find_key($values, fn ($v) => $v === null); // 'a' — unambiguous
When null is a valid element, test the result of array_find_key against null instead — the key is never itself null.
The empty-array behaviour follows formal logic, which surprises people. array_all([]) returns true (every element of nothing satisfies any condition) and array_any([]) returns false. If an empty input should mean "not ready", guard for it explicitly before you call array_all.
And once more, because it is the single most common mistake: the callback receives ($value, $key), value first. If you copy a callback that expected the key first, every match will be wrong and nothing will throw.
Wrapping Up#
Reach for array_find and array_find_key when you want the first match, array_any and array_all when you want a yes/no, and keep array_filter for when you actually need every match. Add symfony/polyfill-php84 if you are not on 8.4 yet and start writing the readable version today.
If you like where this is heading, PHP 8.5's array_first and array_last continue the same cleanup of array fallbacks, and the #[\Deprecated] attribute is another PHP 8.4 addition worth wiring into your own packages.
FAQ#
What does array_find do in PHP 8.4?
array_find takes an array and a callback, and returns the value of the first element for which the callback returns true. If nothing matches, it returns null. It short-circuits, so it stops iterating as soon as it finds a match rather than scanning the whole array.
What is the difference between array_find and array_filter?
array_filter evaluates every element and returns a new array containing all the matches. array_find returns just the first matching value and stops there. Use array_filter when you want every match; use array_find when you only want one, because it avoids building an intermediate array and bails early.
How do array_any and array_all work?
array_any returns true if at least one element passes the callback, and array_all returns true only if every element passes. Both accept a ($value, $key) callback and both short-circuit — array_any stops at the first true result, array_all stops at the first false one. Note that array_all returns true for an empty array and array_any returns false for one.
Can I use array_find in older PHP versions?
Yes. Install symfony/polyfill-php84 and the four functions become available on PHP 7.1 and up, with the same signatures as the native versions. When you upgrade to PHP 8.4 the built-in implementations take over automatically, so you can remove the polyfill later without touching your code.