{"id":521,"date":"2026-07-29T13:00:54","date_gmt":"2026-07-29T13:00:54","guid":{"rendered":"https:\/\/abrarqasim.com\/blog\/php-8-4-in-2026-the-features-that-stuck\/"},"modified":"2026-07-29T13:00:54","modified_gmt":"2026-07-29T13:00:54","slug":"php-8-4-in-2026-the-features-that-stuck","status":"publish","type":"post","link":"https:\/\/abrarqasim.com\/blog\/php-8-4-in-2026-the-features-that-stuck\/","title":{"rendered":"PHP 8.4 in 2026: The Features That Stuck"},"content":{"rendered":"<p>Confession: I put off upgrading a client&rsquo;s billing service to PHP 8.4 for almost a year because the last &ldquo;big&rdquo; PHP release burned me on a Friday deploy. So I sat on 8.3, watched the changelog from a safe distance, and told myself I&rsquo;d get to it. I finally did the migration this spring, and now I&rsquo;m a little annoyed at past me for waiting.<\/p>\n<p>Some of what shipped in 8.4 is the kind of thing you use once and forget you ever lived without. Some of it I read the RFC for, nodded, and have never typed since. This is the honest version of that split. Not &ldquo;10 amazing features&rdquo; \u2014 just the ones that survived contact with real code, and the ones that didn&rsquo;t.<\/p>\n<p>If you&rsquo;re still on 8.3 and wondering whether the jump is worth a sprint, here&rsquo;s what I actually kept.<\/p>\n<h2 id=\"property-hooks-deleted-a-whole-class-of-getters\">Property hooks deleted a whole class of getters<\/h2>\n<p>This is the one. If you only look at a single 8.4 change, make it this.<\/p>\n<p>For years, the second you wanted a computed value or a validated write, you reached for a getter and a setter. Here&rsquo;s the shape I&rsquo;d written a thousand times:<\/p>\n<pre><code class=\"language-php\">class Temperature\n{\n    private float $celsius = 0.0;\n\n    public function getFahrenheit(): float\n    {\n        return $this-&gt;celsius * 9 \/ 5 + 32;\n    }\n\n    public function setFahrenheit(float $f): void\n    {\n        $this-&gt;celsius = ($f - 32) * 5 \/ 9;\n    }\n}\n<\/code><\/pre>\n<p>Every caller then has to know it&rsquo;s <code>getFahrenheit()<\/code>, not <code>$temp-&gt;fahrenheit<\/code>. Property hooks let you keep the plain property syntax and still run code on read and write:<\/p>\n<pre><code class=\"language-php\">class Temperature\n{\n    public float $celsius = 0.0;\n\n    public float $fahrenheit {\n        get =&gt; $this-&gt;celsius * 9 \/ 5 + 32;\n        set(float $value) =&gt; $this-&gt;celsius = ($value - 32) * 5 \/ 9;\n    }\n}\n<\/code><\/pre>\n<p>Now <code>$temp-&gt;fahrenheit<\/code> reads and writes like a normal field, and the conversion lives with the data instead of in a pair of methods three screens apart. The <a href=\"https:\/\/wiki.php.net\/rfc\/property-hooks\" rel=\"nofollow noopener\" target=\"_blank\">property hooks RFC<\/a> has the full grammar if you want the edge cases, including hooks declared on interface properties, which is genuinely useful for DTOs where you want to promise a value exists without dictating how it&rsquo;s stored. I wrote up how this held up across a bigger codebase in <a href=\"https:\/\/abrarqasim.com\/blog\/php-8-4-property-hooks-a-year-later-what-i-kept\" rel=\"noopener\">my longer take on property hooks a year in<\/a>, so I won&rsquo;t relitigate all of it here.<\/p>\n<p>The migration itself was calmer than I feared. Old getter methods keep working, so you can convert one property at a time and leave the rest alone. I did the whole billing service in an afternoon, one class per commit.<\/p>\n<p>The one caveat: don&rsquo;t put slow work in a <code>get<\/code> hook. It looks like a field, so callers assume it&rsquo;s cheap. I cache anything that touches the database and expose it through a normal method instead, so the cost is visible at the call site.<\/p>\n<h2 id=\"asymmetric-visibility-retired-my-readonly-workarounds\">Asymmetric visibility retired my readonly workarounds<\/h2>\n<p>Before 8.4, if I wanted a property the outside world could read but only the class could change, I had two bad options. Make it <code>readonly<\/code> and give up all mutation, or make it private and write a getter that does nothing but return it.<\/p>\n<pre><code class=\"language-php\">class Order\n{\n    private string $status = 'pending';\n\n    public function getStatus(): string\n    {\n        return $this-&gt;status;\n    }\n}\n<\/code><\/pre>\n<p>Asymmetric visibility collapses that into a modifier:<\/p>\n<pre><code class=\"language-php\">class Order\n{\n    public private(set) string $status = 'pending';\n}\n<\/code><\/pre>\n<p><code>$order-&gt;status<\/code> is readable anywhere. Writing to it from outside the class is a compile error. Inside the class, <code>transitionTo()<\/code> or whatever your state machine is called can still mutate it freely. I use <code>public protected(set)<\/code> on aggregate roots so subclasses can adjust state but controllers can&rsquo;t reach in and scribble on it.<\/p>\n<p>The official <a href=\"https:\/\/www.php.net\/releases\/8.4\/en.php\" rel=\"nofollow noopener\" target=\"_blank\">8.4 announcement<\/a> lists this next to property hooks, and the two pair up nicely: hooks for computed and validated fields, asymmetric visibility for &ldquo;read freely, write carefully&rdquo; state. I went deeper on the state-machine angle in a <a href=\"https:\/\/abrarqasim.com\/blog\/php-8-4-asymmetric-visibility-the-getters-i-finally-deleted\" rel=\"noopener\">separate post on asymmetric visibility<\/a>, because it changed how I model orders more than I expected.<\/p>\n<h2 id=\"new-without-parentheses-is-a-small-daily-joy\"><code>new<\/code> without parentheses is a small daily joy<\/h2>\n<p>This one is pure ergonomics, and I didn&rsquo;t think I&rsquo;d care. I was wrong.<\/p>\n<p>You know the double-paren dance where you instantiate something and immediately call a method on it?<\/p>\n<pre><code class=\"language-php\">$name = (new ReflectionClass($obj))-&gt;getShortName();\n<\/code><\/pre>\n<p>Those outer parentheses were never load-bearing. They were there to satisfy the parser. In 8.4 you drop them:<\/p>\n<pre><code class=\"language-php\">$name = new ReflectionClass($obj)-&gt;getShortName();\n<\/code><\/pre>\n<p>It reads the way you&rsquo;d say it out loud. I hit this most in test setup and in one-liner factory calls, and after a week my hands stopped adding the parens automatically. Tiny change, but it&rsquo;s the kind that removes a paper cut you&rsquo;d stopped noticing.<\/p>\n<h2 id=\"the-array-helpers-i-keep-reaching-for\">The array helpers I keep reaching for<\/h2>\n<p>PHP&rsquo;s array functions have always been a bit of a grab bag, and for the longest time there was no clean way to say &ldquo;find the first element matching this.&rdquo; You wrote the loop, broke early, and moved on:<\/p>\n<pre><code class=\"language-php\">$firstAdmin = null;\nforeach ($users as $user) {\n    if ($user-&gt;isAdmin()) {\n        $firstAdmin = $user;\n        break;\n    }\n}\n<\/code><\/pre>\n<p>8.4 added <code>array_find<\/code>, <code>array_any<\/code>, and <code>array_all<\/code>, and they cover the three questions I actually ask about a collection:<\/p>\n<pre><code class=\"language-php\">$firstAdmin = array_find($users, fn($u) =&gt; $u-&gt;isAdmin());\n$hasAdmin   = array_any($users, fn($u) =&gt; $u-&gt;isAdmin());\n$allActive  = array_all($users, fn($u) =&gt; $u-&gt;isActive());\n<\/code><\/pre>\n<p><code>array_find<\/code> returns the element or <code>null<\/code>. <code>array_any<\/code> and <code>array_all<\/code> return booleans. The RFC for the <a href=\"https:\/\/wiki.php.net\/rfc\/array_find\" rel=\"nofollow noopener\" target=\"_blank\">new array find functions<\/a> spells out the ordering guarantees, which matter if you care about which match you get back. I&rsquo;ve swapped a lot of tiny foreach loops for these, and validation code reads noticeably better for it. There&rsquo;s also <code>array_find_key<\/code> if you need the key instead of the value.<\/p>\n<p>The honest limit: these still don&rsquo;t short-circuit any better than a hand-written loop would, and they&rsquo;re not lazy, so on a giant array you&rsquo;re iterating the whole thing unless you get lucky with an early match. For the collection sizes I deal with day to day, request payloads, config lists, a few hundred rows, that&rsquo;s a non-issue. I care more that the intent is legible at a glance than about shaving microseconds.<\/p>\n<h2 id=\"what-i-read-the-rfc-for-and-then-skipped\">What I read the RFC for and then skipped<\/h2>\n<p>Not everything earned a place. Lazy objects landed in 8.4, and they&rsquo;re clever: the runtime defers a heavy object&rsquo;s initialization until something actually touches it, which is exactly what an ORM or a DI container wants under the hood. But that&rsquo;s the thing. It&rsquo;s a framework-author feature. In application code I never construct my own lazy proxies, because Doctrine and the container already do it for me. I&rsquo;m glad it exists. I&rsquo;ve just never typed <code>newLazyGhost()<\/code> in anger, and I doubt most app developers will.<\/p>\n<p>The <code>#[\\Deprecated]<\/code> attribute is the borderline case. Being able to mark a method deprecated and have the engine emit a warning is nice for library maintainers. In a normal app I lean on static analysis and code review for that, so it hasn&rsquo;t changed my week. If you ship a package other teams depend on, it&rsquo;s probably worth adopting.<\/p>\n<h2 id=\"so-is-the-jump-worth-it\">So, is the jump worth it?<\/h2>\n<p>If you maintain PHP for a living, yes, and mostly for the first two features. Property hooks and asymmetric visibility together delete a genuinely tedious category of boilerplate, and they make your models say what they mean. The rest is a pleasant tailwind rather than a reason to upgrade on its own.<\/p>\n<p>Here&rsquo;s the concrete thing to do this week: pick one model class that&rsquo;s mostly private fields wrapped in getters, and rewrite it with property hooks and <code>private(set)<\/code>. One class. You&rsquo;ll feel immediately whether it fits your codebase, and you&rsquo;ll have a real diff to show your team instead of a changelog link. If you want to see how I think about this kind of incremental modernization on real projects, that&rsquo;s most of what I do in <a href=\"https:\/\/abrarqasim.com\/work\" rel=\"noopener\">my client work<\/a>.<\/p>\n<p>Start small, keep the old class in git, and let the diff make the argument for you.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>A practical look back at PHP 8.4 well into 2026: which features I kept in production, from property hooks to the new array helpers, and which I quietly skipped.<\/p>\n","protected":false},"author":2,"featured_media":520,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"","rank_math_description":"A practical look back at PHP 8.4 well into 2026: which features I kept in production, from property hooks to the new array helpers, and which I quietly skipped.","rank_math_focus_keyword":"php 8.4","rank_math_canonical_url":"","rank_math_robots":"","footnotes":""},"categories":[52,45],"tags":[503,49,437,53,339,502],"class_list":["post-521","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-php","category-programming","tag-asymmetric-visibility-2","tag-backend","tag-modern-php-2","tag-php","tag-php-8-4-3","tag-property-hooks-2"],"_links":{"self":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/521","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=521"}],"version-history":[{"count":0,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/521\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media\/520"}],"wp:attachment":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media?parent=521"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/categories?post=521"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/tags?post=521"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}