{"id":78,"date":"2026-04-15T05:53:41","date_gmt":"2026-04-15T05:53:41","guid":{"rendered":"https:\/\/abrarqasim.com\/blog\/?p=78"},"modified":"2026-04-15T05:53:41","modified_gmt":"2026-04-15T05:53:41","slug":"php-84-property-hooks-changed-how-i-write-classes","status":"publish","type":"post","link":"https:\/\/abrarqasim.com\/blog\/php-84-property-hooks-changed-how-i-write-classes\/","title":{"rendered":"PHP 8.4 property hooks changed how I write classes"},"content":{"rendered":"<h1 id=\"php-84-property-hooks-changed-how-i-write-classes-and-im-not-going-back\">PHP 8.4 property hooks changed how I write classes (and I&rsquo;m not going back)<\/h1>\n<p>I mass-updated a Laravel project to PHP 8.4 last week, mostly to get the security patches. Then I saw property hooks in the changelog and spent the next three hours rewriting code that already worked. No regrets.<\/p>\n<p>If you&rsquo;ve written PHP for any length of time, you know the getter\/setter dance. Private property, public getter, maybe a setter with validation, repeat forty times across a codebase. It works fine. It&rsquo;s also the kind of boilerplate that makes you mass-select lines and wonder if you picked the wrong language. PHP 8.4 property hooks kill most of that boilerplate, and they do it without the magic method chaos that <code>__get<\/code> and <code>__set<\/code> always brought.<\/p>\n<p>Let me walk through what&rsquo;s different and show you actual code.<\/p>\n<h2 id=\"the-old-gettersetter-ceremony\">The old getter\/setter ceremony<\/h2>\n<p>Before 8.4, enforcing read-only access or adding validation to a property meant writing methods. Everybody knows this pattern:<\/p>\n<pre><code class=\"language-php\">class Product\n{\n    private string $name;\n    private int $priceInCents;\n\n    public function __construct(string $name, int $priceInCents)\n    {\n        $this-&gt;name = $name;\n        $this-&gt;setPriceInCents($priceInCents);\n    }\n\n    public function getName(): string\n    {\n        return $this-&gt;name;\n    }\n\n    public function getPriceInCents(): int\n    {\n        return $this-&gt;priceInCents;\n    }\n\n    public function setPriceInCents(int $price): void\n    {\n        if ($price &lt; 0) {\n            throw new \\InvalidArgumentException('Price cannot be negative');\n        }\n        $this-&gt;priceInCents = $price;\n    }\n\n    public function getFormattedPrice(): string\n    {\n        return '$' . number_format($this-&gt;priceInCents \/ 100, 2);\n    }\n}\n<\/code><\/pre>\n<p>That&rsquo;s 30-something lines for two properties and one computed value. The actual logic is maybe five lines. The rest is ceremony.<\/p>\n<h2 id=\"property-hooks-what-they-actually-look-like\">Property hooks: what they actually look like<\/h2>\n<p><img decoding=\"async\" alt=\"PHP 8.4 property hooks changed how I write classes\" src=\"https:\/\/abrarqasim.com\/blog\/wp-content\/uploads\/2026\/04\/php-84-property-hooks-changed-how-i-write-classes-inline-1776229283.png\"><\/p>\n<p>PHP 8.4 lets you attach <code>get<\/code> and <code>set<\/code> hooks directly to properties. Same class, less noise:<\/p>\n<pre><code class=\"language-php\">class Product\n{\n    public string $name;\n\n    public int $priceInCents {\n        set(int $value) {\n            if ($value &lt; 0) {\n                throw new \\InvalidArgumentException('Price cannot be negative');\n            }\n            $this-&gt;priceInCents = $value;\n        }\n    }\n\n    public string $formattedPrice {\n        get =&gt; '$' . number_format($this-&gt;priceInCents \/ 100, 2);\n    }\n\n    public function __construct(string $name, int $priceInCents)\n    {\n        $this-&gt;name = $name;\n        $this-&gt;priceInCents = $priceInCents;\n    }\n}\n<\/code><\/pre>\n<p>The validation lives right where the property is declared. The computed <code>$formattedPrice<\/code> is a virtual property with only a <code>get<\/code> hook, so it reads like a property but calculates on access. No separate method, no <code>getFormattedPrice()<\/code> call in your templates.<\/p>\n<p>You access everything the same way you always have: <code>$product-&gt;formattedPrice<\/code>, <code>$product-&gt;priceInCents = 500<\/code>. The hooks fire transparently.<\/p>\n<h2 id=\"asymmetric-visibility-is-the-other-half-of-this\">Asymmetric visibility is the other half of this<\/h2>\n<p>PHP 8.4 also shipped asymmetric visibility, and it pairs well with property hooks. You can make a property publicly readable but only privately (or protectedly) settable:<\/p>\n<pre><code class=\"language-php\">class User\n{\n    public private(set) string $email;\n    public private(set) string $role = 'viewer';\n\n    public function __construct(string $email)\n    {\n        $this-&gt;email = $email;\n    }\n\n    public function promote(): void\n    {\n        $this-&gt;role = 'admin';\n    }\n}\n<\/code><\/pre>\n<p>Before this, you&rsquo;d write a public getter and keep the property private. Or you&rsquo;d use <code>readonly<\/code>, which is too strict when the class itself needs to update the value later. Asymmetric visibility fills that gap without extra methods.<\/p>\n<p>I went through a Laravel app and removed about 40 getters that existed purely to expose a privately-set property. The diff was satisfying.<\/p>\n<h2 id=\"where-hooks-actually-shine-in-laravel\">Where hooks actually shine in Laravel<\/h2>\n<p>DTOs and value objects are where I&rsquo;ve gotten the most mileage. If you build form request objects or API response shapes, you probably have classes that are 80% boilerplate:<\/p>\n<pre><code class=\"language-php\">\/\/ Before: the old way with manual casting\nclass MoneyDto\n{\n    private int $cents;\n\n    public function __construct(int $cents)\n    {\n        $this-&gt;cents = max(0, $cents);\n    }\n\n    public function getCents(): int\n    {\n        return $this-&gt;cents;\n    }\n\n    public function getDollars(): float\n    {\n        return $this-&gt;cents \/ 100;\n    }\n}\n\n\/\/ After: PHP 8.4 with hooks\nclass MoneyDto\n{\n    public int $cents {\n        set(int $value) {\n            $this-&gt;cents = max(0, $value);\n        }\n    }\n\n    public float $dollars {\n        get =&gt; $this-&gt;cents \/ 100;\n    }\n\n    public function __construct(int $cents)\n    {\n        $this-&gt;cents = $cents;\n    }\n}\n<\/code><\/pre>\n<p>The second version is shorter, but the real win is that <code>$dollars<\/code> behaves like a property everywhere. In Blade templates, JSON serialization, wherever. You don&rsquo;t need to remember whether it was <code>getDollars()<\/code> or <code>-&gt;dollars<\/code> or something else.<\/p>\n<p>Eloquent model accessors already had something like this with the <code>Attribute<\/code> cast system Laravel introduced a while back. Property hooks don&rsquo;t replace that for database models (Eloquent has its own thing going on), but for everything else, plain PHP properties with hooks are simpler and need zero framework dependency.<\/p>\n<h2 id=\"things-that-tripped-me-up\">Things that tripped me up<\/h2>\n<p>Property hooks aren&rsquo;t magic, and I ran into a few rough edges during the migration.<\/p>\n<p>First, <strong>hooks and constructor promotion don&rsquo;t always play nice together.<\/strong> You can use promoted properties with hooks in 8.4, but the syntax gets crowded fast. I found it cleaner to keep the property declaration separate when it has a hook and only use promotion for simple assignments.<\/p>\n<p>Second, <strong>virtual properties (get-only, no stored value) can&rsquo;t be set at all<\/strong>, which sounds obvious but caught me when I tried to mass-assign from an array. If you have a virtual property and try <code>$this-&gt;virtualProp = something<\/code>, PHP throws a runtime error. It&rsquo;s not an IDE warning. It&rsquo;s a hard fail.<\/p>\n<p>Third, <strong>serialization behaves differently<\/strong> for virtual properties. Since there&rsquo;s no backing value, <code>serialize()<\/code> and <code>json_encode<\/code> won&rsquo;t include them unless you handle it explicitly. If you&rsquo;re passing these objects across a queue or cache boundary, test that your data survives the round trip.<\/p>\n<p>None of these are dealbreakers. You just won&rsquo;t find them in the RFC. You find them at 11pm when your queue worker silently drops a property.<\/p>\n<h2 id=\"should-you-actually-migrate\">Should you actually migrate?<\/h2>\n<p>If you&rsquo;re already on PHP 8.3, the jump to 8.4 is painless. The property hooks RFC passed with overwhelming support, and the implementation is stable. I haven&rsquo;t hit a single runtime bug in three weeks of production use.<\/p>\n<p>If you&rsquo;re on 8.1 or 8.2, you have a bigger jump but also more to gain: enums, fibers, readonly properties, intersection types, and now hooks and asymmetric visibility all stack up into a language that feels substantially different from the PHP 7.x era.<\/p>\n<p>I wouldn&rsquo;t rewrite existing getters and setters just for the sake of it, at least not all at once. But for new code, I&rsquo;ve stopped writing traditional getters entirely. Property hooks are cleaner, the behavior is explicit, and the code lives where you&rsquo;d actually look for it.<\/p>\n<p>PHP has been quietly stacking wins since 8.1. Enums, readonly classes, typed constants, and now property hooks. Each version makes the language a bit tighter without breaking what came before. I wrote about <a href=\"https:\/\/abrarqasim.com\/blog\/react-compiler-what-it-does-to-your-code\" rel=\"noopener\">React doing something similar with its compiler<\/a> a few days ago. Same energy: less boilerplate, more meaning at the declaration site.<\/p>\n<p>If you want to try property hooks this week, spin up a fresh Laravel 12 project on PHP 8.4 and rewrite one DTO. You&rsquo;ll know within an hour whether the pattern clicks for you.<\/p>\n<p>The <a href=\"https:\/\/wiki.php.net\/rfc\/property-hooks\" rel=\"nofollow noopener\" target=\"_blank\">PHP 8.4 property hooks RFC<\/a> has the full spec, and the <a href=\"https:\/\/www.php.net\/manual\/en\/migration84.php\" rel=\"nofollow noopener\" target=\"_blank\">migration guide<\/a> covers everything else that shipped. I&rsquo;d read both before touching production code. I write about this kind of language and framework evolution on <a href=\"https:\/\/abrarqasim.com\" rel=\"noopener\">my site<\/a> if you want more of this.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>PHP 8.4 property hooks replace getter\/setter boilerplate with inline get and set logic. Here&#8217;s how they work in practice with Laravel DTOs and value objects.<\/p>\n","protected":false},"author":2,"featured_media":76,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"","rank_math_description":"PHP 8.4 property hooks replace getter\/setter boilerplate with inline get and set logic. Here's how they work in practice with Laravel DTOs and value objects.","rank_math_focus_keyword":"php 8.4 property hooks","rank_math_canonical_url":"","rank_math_robots":"","footnotes":""},"categories":[52],"tags":[49,56,53,54,55,39],"class_list":["post-78","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-php","tag-backend","tag-laravel","tag-php","tag-php-8-4","tag-property-hooks","tag-web-development"],"_links":{"self":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/78","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=78"}],"version-history":[{"count":1,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/78\/revisions"}],"predecessor-version":[{"id":79,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/78\/revisions\/79"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media\/76"}],"wp:attachment":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media?parent=78"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/categories?post=78"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/tags?post=78"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}