Skip to content

PHP 8.4 Property Hooks: The Getters I Stopped Writing

PHP 8.4 Property Hooks: The Getters I Stopped Writing

Okay, this is going to sound dramatic for a language feature, but PHP 8.4 deleted a kind of code I’ve been writing on autopilot since roughly 2012. Getters and setters. The little paired methods you write so a property can have a tiny bit of logic, validation, or laziness behind it. I have written thousands of them. I will write very few more, and I’m weirdly emotional about it.

PHP 8.4 shipped property hooks, asymmetric visibility, and a handful of smaller quality-of-life changes. I want to walk through the two that actually changed my day-to-day, with the old code next to the new code, because that’s the only honest way to judge whether a feature earns its keep.

A quick note before the code: I’m not going to pretend any of this is revolutionary. PHP has been catching up to features that languages like C# and Kotlin had for years, and that’s fine. Catching up is good. What matters is whether the new syntax removes friction from code you write all the time, and on that test, property hooks land.

The getter and setter boilerplate I stopped writing

Here’s the pattern. You want a fullName that’s derived, and a temperature that validates on write. In PHP 8.3, that means a private property and a pair of methods, plus the mental tax of remembering to call the method instead of touching the property.

// PHP 8.3
class Person
{
    private string $first;
    private string $last;

    public function getFullName(): string
    {
        return "{$this->first} {$this->last}";
    }

    private float $temp = 0.0;

    public function setTemperature(float $value): void
    {
        if ($value < -273.15) {
            throw new InvalidArgumentException("below absolute zero");
        }
        $this->temp = $value;
    }

    public function getTemperature(): float
    {
        return $this->temp;
    }
}

It’s fine. It’s also a lot of ceremony for “this value is computed” and “this value has a floor.” Property hooks let you attach get and set logic directly to a property, so callers use plain property syntax and the logic rides along.

// PHP 8.4
class Person
{
    public function __construct(
        private string $first,
        private string $last,
    ) {}

    public string $fullName {
        get => "{$this->first} {$this->last}";
    }

    public float $temperature = 0.0 {
        set (float $value) {
            if ($value < -273.15) {
                throw new InvalidArgumentException("below absolute zero");
            }
            $this->temperature = $value;
        }
    }
}

$p = new Person("Ada", "Lovelace");
echo $p->fullName;          // "Ada Lovelace", computed on read
$p->temperature = 21.5;     // validated on write

Callers write $p->fullName and $p->temperature = 21.5. No method names to remember, no get/set prefix conventions, and the validation lives right next to the data it guards. The official property hooks RFC goes deep on the mechanics, including hooks on interfaces, which is the part that’s going to quietly reshape how people design contracts.

This pairs nicely with a cleanup I did earlier when class constants got typed; I wrote about pruning that kind of boilerplate in the PHP enums and constants I finally deleted.

Asymmetric visibility, or “public to read, private to write”

The second feature fixes a real annoyance: you often want a property the outside world can read but only the class can change. In 8.3 the only way to enforce that was to make the property private and expose a getter, which is exactly the boilerplate we just removed.

// PHP 8.3
class Order
{
    private string $status = "pending";

    public function getStatus(): string
    {
        return $this->status; // read-only to the outside via a method
    }

    public function markPaid(): void
    {
        $this->status = "paid";
    }
}

PHP 8.4 lets you set the read and write visibility separately on one line.

// PHP 8.4
class Order
{
    public private(set) string $status = "pending";

    public function markPaid(): void
    {
        $this->status = "paid"; // allowed: we're inside the class
    }
}

$o = new Order();
echo $o->status;        // "pending" — public read is fine
// $o->status = "paid"; // Error: write is private

public private(set) reads exactly like what it does: public to read, private to write. No getter, no defensive copy, no comment explaining the convention. The asymmetric visibility RFC covers the protected(set) variant too, which is the one I expect to use most in base classes.

These two features compose, by the way, and that’s where it gets nice. A property can be public private(set) and still have a get hook that computes or formats on read. So a value that’s set once internally, validated on write, and presented in a tweaked form on read can be a single property declaration instead of a private field plus two methods plus a comment. That’s the version of “rich domain object” I always wanted but never bothered to write because the ceremony wasn’t worth it.

The smaller wins: array_find and new without parentheses

Property hooks got the headlines, but two smaller 8.4 changes deserve a mention because they delete code I wrote weekly. First, the new array functions: array_find, array_find_key, array_any, and array_all. For years, “find the first element matching a condition” meant a foreach with a break, or an array_filter that built a whole array just so I could grab [0] and throw the rest away.

// PHP 8.3
$firstAdmin = null;
foreach ($users as $u) {
    if ($u->isAdmin()) { $firstAdmin = $u; break; }
}
// PHP 8.4
$firstAdmin = array_find($users, fn($u) => $u->isAdmin());
$hasAdmin   = array_any($users, fn($u) => $u->isAdmin());
$allActive  = array_all($users, fn($u) => $u->isActive());

It reads like the intent instead of the mechanics. I covered the larger pattern of deleting little helper functions like this in a separate post on the array helpers I stopped hand-writing, so I’ll keep it short here.

Second, and this is tiny but I love it: you can now call a method on a freshly constructed object without wrapping the whole thing in parentheses.

// PHP 8.3
$slug = (new Slugger())->slugify($title);

// PHP 8.4
$slug = new Slugger()->slugify($title);

The extra parentheses were never load-bearing. They were noise I’d typed ten thousand times. Removing them won’t change your life, but it’s one of those changes you stop noticing immediately and would resent losing.

Where I’d be careful

Property hooks are seductive, and seductive features get overused. The moment a hook starts doing real work, hitting the database, firing events, doing expensive computation on every read, you’ve hidden a method call behind something that looks like a field access, and the next person (probably you, in six months) will be confused about why reading a property is slow. Keep hooks cheap and obvious. If the logic is heavy, a named method that announces itself is the kinder choice.

Asymmetric visibility has fewer footguns, but remember it’s about visibility, not immutability. private(set) stops outside writes; it doesn’t make the object readonly, and a method inside the class can still change the value whenever it likes. If you want true immutability, readonly is still the tool.

There’s also a migration trap if you’re using these alongside an ORM or serializer. A lot of those tools read and write your properties through reflection, and some of them set values that your set hook would normally validate. Doctrine, Eloquent, and friends are catching up, but check that your hooks actually fire when an object is hydrated from the database rather than constructed in your own code. I got briefly fooled by a set hook that ran in my tests (where I built the object by hand) but not when the same object came back from a query. The behavior was correct; my mental model wasn’t.

What to do this week

Pick one model or value object in your codebase that’s mostly a private property wrapped in a getter, and rewrite it with a get hook or public private(set). Run your tests. The behavior should be identical and the class should be noticeably shorter. That single conversion will tell you more than any blog post about whether these features fit how you write PHP.

One caution worth repeating: don’t do a giant sweeping refactor on day one. Adopt these on new code and on the next class you’d touch anyway, the way I generally roll out language features across the projects I work on. PHP 8.4 didn’t make me a faster developer. It made my classes admit what they were actually doing, which turns out to be most of what I wanted from getters and setters in the first place.