{"id":402,"date":"2026-06-30T13:04:14","date_gmt":"2026-06-30T13:04:14","guid":{"rendered":"https:\/\/abrarqasim.com\/blog\/php-8-4-array-find-the-foreach-loops-i-finally-deleted\/"},"modified":"2026-06-30T13:04:14","modified_gmt":"2026-06-30T13:04:14","slug":"php-8-4-array-find-the-foreach-loops-i-finally-deleted","status":"publish","type":"post","link":"https:\/\/abrarqasim.com\/blog\/php-8-4-array-find-the-foreach-loops-i-finally-deleted\/","title":{"rendered":"PHP 8.4 array_find: The Foreach Loops I Finally Deleted"},"content":{"rendered":"<p>Confession: I have written the same <code>foreach<\/code> loop maybe two thousand times. The one where you iterate over a collection, check a predicate, set a variable, and <code>break<\/code>. Every time I do it I think &ldquo;there has to be a cleaner way&rdquo; and every time I shrug and write the loop anyway, because the alternative was <code>array_filter(...)[0] ?? null<\/code> and that&rsquo;s worse.<\/p>\n<p>PHP 8.4 finally fixed this. It shipped four new functions, <code>array_find<\/code>, <code>array_find_key<\/code>, <code>array_any<\/code>, and <code>array_all<\/code>, and after a few weeks of using them I&rsquo;ve deleted more boilerplate than I expected. This post is what I actually replaced, where they shine, and where I still reach for <code>foreach<\/code>.<\/p>\n<h2 id=\"the-loop-id-been-writing-forever\">The loop I&rsquo;d been writing forever<\/h2>\n<p>Here&rsquo;s the pattern. You&rsquo;ve got a list of users, you want the first admin, you want <code>null<\/code> if there isn&rsquo;t one. In PHP 8.3 and below, the cleanest version was something like this:<\/p>\n<pre><code class=\"language-php\">$admin = null;\nforeach ($users as $user) {\n    if ($user-&gt;role === 'admin') {\n        $admin = $user;\n        break;\n    }\n}\n<\/code><\/pre>\n<p>Four lines plus the <code>break<\/code> to remember. It&rsquo;s fine. It works. But it&rsquo;s noisy, and when you have five of them in a controller method you start scanning for the <code>break<\/code> line to confirm you didn&rsquo;t typo a condition.<\/p>\n<p>The &ldquo;functional&rdquo; alternative wasn&rsquo;t much better:<\/p>\n<pre><code class=\"language-php\">$admin = array_filter($users, fn($u) =&gt; $u-&gt;role === 'admin')[0] ?? null;\n<\/code><\/pre>\n<p>This lies. <code>array_filter<\/code> preserves keys, so <code>[0]<\/code> only works if the first matching element happens to be at index 0. Realistically you needed <code>array_values(array_filter(...))[0] ?? null<\/code>, which is now an unreadable chain that allocates a whole array just to throw it away.<\/p>\n<p>PHP 8.4 gives you this instead:<\/p>\n<pre><code class=\"language-php\">$admin = array_find($users, fn($u) =&gt; $u-&gt;role === 'admin');\n<\/code><\/pre>\n<p>Returns the first matching element, or <code>null<\/code> if nothing matches. Short-circuits. Doesn&rsquo;t allocate. The <a href=\"https:\/\/www.php.net\/releases\/8.4\/en.php\" rel=\"nofollow noopener\" target=\"_blank\">PHP 8.4 release announcement<\/a> covers the full feature list, but <code>array_find<\/code> and friends are the ones I miss the most when I&rsquo;m working on an 8.3 codebase now.<\/p>\n<h2 id=\"the-full-set-and-when-i-reach-for-each\">The full set, and when I reach for each<\/h2>\n<p>There are four new functions and they all take the same <code>(array, callable)<\/code> shape. Here&rsquo;s how I actually use them.<\/p>\n<p><strong><code>array_find($array, $fn)<\/code><\/strong> returns the first element where <code>$fn($value, $key)<\/code> is truthy, or <code>null<\/code>. Use when you want the matched value.<\/p>\n<p><strong><code>array_find_key($array, $fn)<\/code><\/strong> returns the key of the first match, or <code>null<\/code>. Use when you need the index back, often to mutate the array in place.<\/p>\n<p><strong><code>array_any($array, $fn)<\/code><\/strong> returns <code>true<\/code> if any element matches. Use for guards: &ldquo;does this collection contain at least one X.&rdquo;<\/p>\n<p><strong><code>array_all($array, $fn)<\/code><\/strong> returns <code>true<\/code> if every element matches. Use for invariants: &ldquo;every item in the cart is in stock.&rdquo;<\/p>\n<p>A real example from a checkout flow:<\/p>\n<pre><code class=\"language-php\">\/\/ Before, PHP 8.3\n$hasOutOfStock = false;\nforeach ($cart-&gt;items as $item) {\n    if ($item-&gt;stock === 0) {\n        $hasOutOfStock = true;\n        break;\n    }\n}\n\nif ($hasOutOfStock) {\n    throw new CheckoutException('Some items are out of stock');\n}\n<\/code><\/pre>\n<pre><code class=\"language-php\">\/\/ After, PHP 8.4\nif (array_any($cart-&gt;items, fn($i) =&gt; $i-&gt;stock === 0)) {\n    throw new CheckoutException('Some items are out of stock');\n}\n<\/code><\/pre>\n<p>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&rsquo;t ask me a single question about it.<\/p>\n<p>The <a href=\"https:\/\/wiki.php.net\/rfc\/array_find\" rel=\"nofollow noopener\" target=\"_blank\">original RFC<\/a> 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 <code>array_find<\/code>.<\/p>\n<h2 id=\"where-i-still-write-the-foreach\">Where I still write the foreach<\/h2>\n<p>The new functions aren&rsquo;t a hammer for everything. Three cases where I still reach for <code>foreach<\/code>:<\/p>\n<p><strong>You need both the matched element and its index.<\/strong> <code>array_find<\/code> gives you the element, <code>array_find_key<\/code> gives you the key, but calling both walks the array twice. If you need both, write the loop.<\/p>\n<pre><code class=\"language-php\">$index = null;\n$winner = null;\nforeach ($entries as $i =&gt; $entry) {\n    if ($entry-&gt;score &gt; 100) {\n        $index = $i;\n        $winner = $entry;\n        break;\n    }\n}\n<\/code><\/pre>\n<p><strong>You&rsquo;re mutating as you iterate.<\/strong> The new functions are read-only. If you need to update each item, the old <code>foreach ($items as &amp;$item)<\/code> pattern is still the cleanest path.<\/p>\n<p><strong>The predicate has side effects.<\/strong> Don&rsquo;t put a database call or a log line inside an <code>array_any<\/code> 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.<\/p>\n<p>If you&rsquo;ve spent any time with the <a href=\"https:\/\/abrarqasim.com\/blog\/laravel-12-new-features-what-actually-changed\" rel=\"noopener\">Laravel collection methods<\/a> I wrote about recently, the mental model is the same. The new native functions are just the parts of <code>Collection::first()<\/code>, <code>Collection::contains()<\/code>, and <code>Collection::every()<\/code> that finally made it into the language itself, so you don&rsquo;t need to wrap your data in a collection to get them.<\/p>\n<h2 id=\"a-small-bonus-better-error-messages\">A small bonus: better error messages<\/h2>\n<p>One thing I didn&rsquo;t expect to notice. When I&rsquo;d typo a predicate in <code>array_filter<\/code> and get back an empty array, debugging was &ldquo;set a breakpoint, walk the loop, figure out which case isn&rsquo;t matching.&rdquo; With <code>array_find<\/code> returning <code>null<\/code>, my code throws on the very next line where I try to use the result, which I always intended would never be <code>null<\/code> because the data invariant said so. That&rsquo;s a much faster failure than discovering it three function calls deep.<\/p>\n<p>Small thing. But after years of squinting at filter chains it&rsquo;s a meaningful upgrade.<\/p>\n<h2 id=\"migration-plan-if-youre-upgrading-from-83\">Migration plan if you&rsquo;re upgrading from 8.3<\/h2>\n<p>This is what I did across a Laravel 11 codebase last month. Took about ninety minutes.<\/p>\n<p>First, search the codebase for <code>foreach<\/code> blocks ending in <code>break<\/code>. In most editors that&rsquo;s a regex like <code>foreach.*\\n.*if.*\\n.*=.*\\n.*break<\/code>. About two-thirds of mine were <code>array_find<\/code> candidates.<\/p>\n<p>Second, search for <code>array_filter<\/code> followed by <code>array_values<\/code> or <code>reset<\/code>. Those are the &ldquo;I wanted <code>array_find<\/code> but didn&rsquo;t have it&rdquo; cases. Replace them outright.<\/p>\n<p>Third, search for boolean accumulator patterns: <code>$has = false; foreach ... { if ... { $has = true; break; } }<\/code>. Those are <code>array_any<\/code> candidates and they collapse to a single readable line.<\/p>\n<p>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&rsquo;t ship without the safety net.<\/p>\n<p>If you&rsquo;re still on PHP 8.3 and not ready to upgrade, you can polyfill <code>array_find<\/code> in about six lines, but you&rsquo;re not going to get the same engine-level optimizations. Worth waiting for the real 8.4 deploy.<\/p>\n<p>The <a href=\"https:\/\/symfony.com\/blog\/symfony-7-2-curated-new-features\" rel=\"nofollow noopener\" target=\"_blank\">Symfony team&rsquo;s PHP 8.4 compatibility notes<\/a> (look for the polyfill section) cover a similar deprecation cleanup if you want to see how a large project handled the upgrade.<\/p>\n<h2 id=\"what-to-try-this-week\">What to try this week<\/h2>\n<p>Grep your codebase for <code>foreach<\/code>. 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 <code>break<\/code> keyword and the boilerplate just goes away, you&rsquo;ll get why this matters.<\/p>\n<p>I also keep a short <a href=\"https:\/\/abrarqasim.com\/about\" rel=\"noopener\">notes page on PHP modernization<\/a> on my site for things like this, and I&rsquo;m happy to argue about which patterns are worth chasing on any given week. The honest answer for <code>array_find<\/code> is: yes, this one is worth the upgrade. It&rsquo;s a small feature that quietly improves every list-processing function you&rsquo;ll write for the next decade.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>PHP 8.4 added array_find, array_find_key, array_any, and array_all. Here&#8217;s where they actually replace a foreach in real code, and where they don&#8217;t.<\/p>\n","protected":false},"author":2,"featured_media":401,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"","rank_math_description":"PHP 8.4 added array_find, array_find_key, array_any, and array_all. Here's where they actually replace a foreach in real code, and where they don't.","rank_math_focus_keyword":"php 8.4 new features","rank_math_canonical_url":"","rank_math_robots":"","footnotes":""},"categories":[52],"tags":[436,49,56,437,53,339],"class_list":["post-402","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-php","tag-array-find","tag-backend","tag-laravel","tag-modern-php-2","tag-php","tag-php-8-4-3"],"_links":{"self":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/402","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=402"}],"version-history":[{"count":0,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/402\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media\/401"}],"wp:attachment":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media?parent=402"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/categories?post=402"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/tags?post=402"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}