{"id":248,"date":"2026-05-18T13:00:44","date_gmt":"2026-05-18T13:00:44","guid":{"rendered":"https:\/\/abrarqasim.com\/blog\/php-8-4-array-find-and-friends-helpers-i-stopped-writing\/"},"modified":"2026-05-18T13:00:44","modified_gmt":"2026-05-18T13:00:44","slug":"php-8-4-array-find-and-friends-helpers-i-stopped-writing","status":"publish","type":"post","link":"https:\/\/abrarqasim.com\/blog\/php-8-4-array-find-and-friends-helpers-i-stopped-writing\/","title":{"rendered":"PHP 8.4 array_find and Friends: Helpers I Stopped Writing Myself"},"content":{"rendered":"<p>Confession: I wrote the same eight-line <code>array_filter<\/code> plus <code>current<\/code> pattern for almost ten years before PHP 8.4 finally shipped <code>array_find<\/code>. A decade of <code>current(array_filter($items, fn(...) =&gt; ...))<\/code> and squinting at it the next month wondering what it actually returned when nothing matched.<\/p>\n<p>If you&rsquo;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&rsquo;t quite fit, so you stitched them together. Then PHP 8.4 dropped <code>array_find<\/code>, <code>array_find_key<\/code>, <code>array_any<\/code>, and <code>array_all<\/code>, and a family of helpers I&rsquo;d been hand-rolling for years became one line each.<\/p>\n<p>I&rsquo;m going to walk through what each one actually does, where I use them in production, and the one place where the old <code>foreach<\/code> is still cleaner. The <a href=\"https:\/\/www.php.net\/releases\/8.4\/en.php\" rel=\"nofollow noopener\" target=\"_blank\">PHP 8.4 release page<\/a> is the canonical reference for the version overall.<\/p>\n<h2 id=\"what-array_find-actually-returns\">What array_find actually returns<\/h2>\n<p>The signature looks innocent:<\/p>\n<pre><code class=\"language-php\">array_find(array $array, callable $callback): mixed\n<\/code><\/pre>\n<p>It returns the first value where the callback returns truthy, or <code>null<\/code> if nothing matches. That <code>null<\/code> is the part I want to flag, because it&rsquo;s the part that tripped me up.<\/p>\n<p>Old PHP code I deleted last month:<\/p>\n<pre><code class=\"language-php\">$user = current(array_filter($users, fn($u) =&gt; $u-&gt;email === $email));\nif ($user === false) {\n    return null;\n}\n<\/code><\/pre>\n<p><code>array_filter<\/code> returns an array with original keys preserved. <code>current<\/code> on that array gives you the first element or <code>false<\/code> on an empty array. So you have to check for <code>false<\/code>, even though your data type is <code>User<\/code>, and you have to know that <code>current<\/code> is positioned wherever the filter started. Reading this six months later, the <code>=== false<\/code> looks like a bug, not a feature.<\/p>\n<p>The PHP 8.4 version:<\/p>\n<pre><code class=\"language-php\">$user = array_find($users, fn($u) =&gt; $u-&gt;email === $email);\nif ($user === null) {\n    return null;\n}\n<\/code><\/pre>\n<p>Three changes that matter. The return type is <code>mixed<\/code> not <code>array|false<\/code>. Null is unambiguous, so the static analyzer doesn&rsquo;t have to think about whether <code>false<\/code> could be a valid value in your data. And there&rsquo;s no intermediate filtered array allocated, which matters if your list is large.<\/p>\n<h2 id=\"array_any-and-array_all-the-predicates-i-used-to-write-inline\">array_any and array_all: the predicates I used to write inline<\/h2>\n<p>These are the boolean cousins. <code>array_any<\/code> returns true if at least one item matches the callback. <code>array_all<\/code> returns true only if every item matches.<\/p>\n<p>Before PHP 8.4:<\/p>\n<pre><code class=\"language-php\">$hasAdmin = !empty(array_filter($users, fn($u) =&gt; $u-&gt;role === 'admin'));\n$allVerified = count(array_filter($users, fn($u) =&gt; $u-&gt;verified))\n    === count($users);\n<\/code><\/pre>\n<p>Both work. Both allocate a temporary array I don&rsquo;t care about, and the <code>array_all<\/code> version walks the whole list even when the first failure is enough to answer the question.<\/p>\n<p>After:<\/p>\n<pre><code class=\"language-php\">$hasAdmin = array_any($users, fn($u) =&gt; $u-&gt;role === 'admin');\n$allVerified = array_all($users, fn($u) =&gt; $u-&gt;verified);\n<\/code><\/pre>\n<p>The functions short-circuit. <code>array_any<\/code> stops at the first match. <code>array_all<\/code> stops at the first failure. On a list of ten thousand items where the first one fails the predicate, that&rsquo;s a real speedup. On a list of three items, it&rsquo;s a wash and you pick <code>array_all<\/code> because it reads better.<\/p>\n<p>These are equivalent to JavaScript&rsquo;s <code>Array.prototype.some<\/code> and <code>Array.prototype.every<\/code>, which I&rsquo;d been wishing for in PHP for years.<\/p>\n<p>One small footgun: if your callback throws, <code>array_any<\/code> and <code>array_all<\/code> propagate the exception. That&rsquo;s the right behavior, but I&rsquo;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.<\/p>\n<h2 id=\"array_find_key-when-i-care-about-position\">array_find_key: when I care about position<\/h2>\n<p><code>array_find<\/code> gives you the value. <code>array_find_key<\/code> gives you the key. Sometimes I want the key because I&rsquo;m about to write back into the array, or because the keys are meaningful (think a <code>Record&lt;orderId, Order&gt;<\/code> shape).<\/p>\n<pre><code class=\"language-php\">$items = [\n    'order-1001' =&gt; new Order(...),\n    'order-1002' =&gt; new Order(...),\n    'order-1003' =&gt; new Order(...),\n];\n\n$failedKey = array_find_key($items, fn($o) =&gt; $o-&gt;status === 'failed');\n\nif ($failedKey !== null) {\n    unset($items[$failedKey]);\n}\n<\/code><\/pre>\n<p>The old version of this would be <code>array_search<\/code> combined with a custom predicate, which <code>array_search<\/code> doesn&rsquo;t actually support. So you&rsquo;d write a <code>foreach<\/code> with a manual break. The new version is one line and the type signature carries through.<\/p>\n<p>There&rsquo;s a quirk worth knowing. If your keys can legitimately be <code>0<\/code>, <code>null<\/code>, or <code>''<\/code>, the &ldquo;not found&rdquo; sentinel of <code>null<\/code> collides with a real key. You&rsquo;ll want a strict comparison and probably a manual <code>array_key_exists<\/code> check if you&rsquo;re paranoid. The <a href=\"https:\/\/www.php.net\/manual\/en\/function.array-find-key.php\" rel=\"nofollow noopener\" target=\"_blank\">PHP manual entry for array_find_key<\/a> covers the edge cases.<\/p>\n<h2 id=\"where-these-still-dont-help-and-what-to-reach-for\">Where these still don&rsquo;t help (and what to reach for)<\/h2>\n<p>I want to push back on the framing that these functions replace <code>foreach<\/code>. They don&rsquo;t. They replace <code>array_filter<\/code> plus <code>current<\/code> and similar three-step chains. For anything more complex than a single predicate, a <code>foreach<\/code> is still cleaner.<\/p>\n<p>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 <code>array_find<\/code> I&rsquo;d need two passes, one to find the index and one to slice. With <code>foreach<\/code> it&rsquo;s one pass:<\/p>\n<pre><code class=\"language-php\">$processedBefore = [];\n$targetOrder = null;\nforeach ($orders as $order) {\n    if ($order-&gt;status === 'failed') {\n        $targetOrder = $order;\n        break;\n    }\n    $processedBefore[] = $order;\n}\n<\/code><\/pre>\n<p>This is the kind of mixed-state iteration that doesn&rsquo;t fit into a single predicate. Using <code>array_find<\/code> here would force me to either accept two passes or stop using these helpers entirely halfway through the function. The <code>foreach<\/code> is honest about what it&rsquo;s doing.<\/p>\n<p>Other places I still reach for the old patterns. When I need an index and a value together, I&rsquo;ll often use <code>array_map<\/code> with <code>array_keys<\/code> 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, <code>array_filter<\/code> with a named callback can be clearer than a long arrow function passed to <code>array_find<\/code>.<\/p>\n<p>There&rsquo;s also the case where I&rsquo;m working with a <code>Generator<\/code> rather than a plain array. None of the new helpers accept iterables. If you&rsquo;ve been writing memory-efficient pipelines with <code>yield<\/code>, 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 <code>iter_find<\/code> and friends. Until then, <code>foreach<\/code> over a generator is the cleanest answer.<\/p>\n<p>I also still write small classes that wrap collection logic when the operations get hairy. My take on this is in the <a href=\"https:\/\/abrarqasim.com\/blog\/php-84-property-hooks-what-i-actually-use-in-production\/\" rel=\"noopener\">property hooks post I wrote a couple of months back<\/a>, where the same &ldquo;let me delete some boilerplate&rdquo; argument applies to a different language feature.<\/p>\n<h2 id=\"what-to-try-this-week\">What to try this week<\/h2>\n<p>Open your codebase. Search for <code>array_filter<\/code> calls followed by <code>current<\/code> or <code>reset<\/code>. Every one of those is a candidate for <code>array_find<\/code>. Then search for <code>!empty(array_filter<\/code> and <code>count(array_filter<\/code> and similar shapes. Those are <code>array_any<\/code> and <code>array_all<\/code> respectively.<\/p>\n<p>You&rsquo;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&rsquo;ll get tighter inference for free.<\/p>\n<p>PHP 8.4 has been out for over a year at this point, so unless you&rsquo;re stuck on an older version, there&rsquo;s no reason not to lean on these. If your job involves the boring middle of a PHP codebase, <a href=\"https:\/\/abrarqasim.com\/about\" rel=\"noopener\">my work on long-running PHP projects<\/a> covers more of this kind of refactor. The cumulative effect of a couple hundred small simplifications like these is real.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>PHP 8.4 array_find, array_any, array_all, and array_find_key replaced years of array_filter plus current chains in my codebase. Here is what stuck.<\/p>\n","protected":false},"author":2,"featured_media":247,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"","rank_math_description":"PHP 8.4 array_find, array_any, array_all, and array_find_key replaced years of array_filter plus current chains in my codebase. Here is what stuck.","rank_math_focus_keyword":"php 8.4","rank_math_canonical_url":"","rank_math_robots":"","footnotes":""},"categories":[45],"tags":[49,53,287,50],"class_list":["post-248","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-programming","tag-backend","tag-php","tag-php-8-4-2","tag-programming"],"_links":{"self":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/248","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/comments?post=248"}],"version-history":[{"count":0,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/248\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media\/247"}],"wp:attachment":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media?parent=248"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/categories?post=248"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/tags?post=248"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}