PHP 8.4 Property Hooks in Laravel Models

PHP 8.4 property hooks let you add get/set logic directly to properties. Here's how they work in Laravel Eloquent models — and where the caveat is.

Steven Richardson
Steven Richardson
· 4 min read

Every time I add a getFirstNameAttribute method to a Laravel model, I feel a small amount of shame. The method name is long, the pattern is implicit, and IDEs have historically struggled with it. PHP 8.4 property hooks look like the fix I've been waiting for — but using them inside Eloquent models is more nuanced than it first appears.

What are PHP 8.4 property hooks?#

Property hooks let you attach get and set logic directly to a class property, without a separate method:

class User
{
    public string $firstName {
        get => ucfirst($this->firstName);
        set(string $value) => $this->firstName = strtolower($value);
    }
}

The property above is a backed property — the hook references $this->firstName, so PHP allocates storage for the value. You can also define virtual properties that have no backing value at all — useful for computed fields:

class Money
{
    public function __construct(private int $amountInCents) {}

    public string $formatted {
        get => '$' . number_format($this->amountInCents / 100, 2);
    }
}

Virtual properties take up no memory; they're pure computation on read.

The old way: Laravel accessors and mutators#

Before this, a simple name formatter in an Eloquent model looked like this:

// Laravel 8 and older
class User extends Model
{
    public function getFirstNameAttribute(string $value): string
    {
        return ucfirst($value);
    }

    public function setFirstNameAttribute(string $value): void
    {
        $this->attributes['first_name'] = strtolower($value);
    }
}

Laravel 9 cleaned this up with the Attribute class:

// Laravel 9+
use Illuminate\Database\Eloquent\Casts\Attribute;

class User extends Model
{
    protected function firstName(): Attribute
    {
        return Attribute::make(
            get: fn (string $value) => ucfirst($value),
            set: fn (string $value) => ['first_name' => strtolower($value)],
        );
    }
}

Better, but still a method that returns an object describing other closures. Not the most intuitive surface area.

Rewriting with property hooks — and the caveat you need to know#

Here's where things get interesting. Eloquent doesn't store model data in PHP properties — it stores everything in an internal $attributes array, intercepting reads and writes via __get() and __set() magic methods.

When you declare a PHP property with hooks on an Eloquent model, PHP's property mechanism takes over for that property name, bypassing Eloquent's magic methods entirely. That means a set hook does not write into $this->attributes — so $user->save() won't persist whatever the hook stored.

What does work well is using virtual property hooks to derive display values from Eloquent's attributes directly:

class User extends Model
{
    // Read-only virtual property — safe to use, won't conflict with Eloquent
    public string $fullName {
        get => "{$this->attributes['first_name']} {$this->attributes['last_name']}";
    }

    // Also works: reading a formatted currency value from a DB column
    public string $formattedBalance {
        get => '$' . number_format(($this->attributes['balance_in_cents'] ?? 0) / 100, 2);
    }
}

// Usage
$user = User::find(1);
echo $user->fullName;         // "Steven Richardson"
echo $user->formattedBalance; // "$142.50"

These virtual properties have no backing value and read straight from $this->attributes, so they're entirely safe to use alongside Eloquent.

When to use hooks vs. casts vs. accessors#

I use property hooks on Eloquent models for computed, read-only display values that derive from one or more DB columns — think $fullName, $formattedPrice, $initials. No DB writes involved, no friction.

I stick with Attribute::make() whenever I need a proper get/set pair that writes back to the database. The set closure returns an array like ['first_name' => $value], which Eloquent merges into $attributes before saving — the only reliable way to mutate persisted data through this kind of abstraction.

Casts are still the first thing I reach for when the transformation is simple and standard: 'amount' => 'integer', 'published_at' => 'datetime', 'settings' => 'array'. They're declarative, cached, and well-understood by the rest of the framework.

One hard requirement: property hooks are PHP 8.4+ only. Laravel 13 requires PHP 8.3 as a minimum, so you'll need to explicitly require PHP 8.4 in your composer.json if you want to use them — worth checking your hosting environment first.

{
    "require": {
        "php": "^8.4",
        "laravel/framework": "^13.0"
    }
}

Property hooks are genuinely useful, just not the sweeping Eloquent makeover they might appear to be at first glance. Treat them as a complement to accessors, not a replacement.

For immutability at the class level rather than the property level, PHP readonly classes as value objects in Laravel is the natural companion — virtual property hooks and readonly value object casts solve overlapping problems from different angles. For type-safe status fields on the same models, replacing string constants with PHP backed enums is the other high-value PHP 8.1+ improvement worth pairing with hooks. Both patterns are part of The Complete Laravel Developer Toolchain for 2026.

FAQ#

Can I use property hooks to validate data before saving?

No, not reliably. A set hook on an Eloquent model doesn't write to $attributes, so the validator won't see the value and save() won't persist it. For validation, stick with Attribute::make() or Form Request validation before constructing the model.

Why don't set hooks write to $attributes?

Eloquent uses __get() and __set() magic methods to intercept all property access and redirect to the $attributes array. When you declare a real PHP property with a hook, PHP's property mechanism takes precedence and bypasses the magic methods entirely. They're two competing systems.

Should I replace all my accessors with property hooks?

Only for computed, read-only display properties. For accessors that transform data going into the database, keep using Attribute::make(). Property hooks work well for $fullName or $formattedPrice — virtual properties that derive values from other columns.

Do I need PHP 8.4 everywhere or just where I use hooks?

You need PHP 8.4 as your minimum requirement if you declare property hooks in your codebase. If you're sharing code, declare it in composer.json so consumers know. For internal projects, it's simpler to just require 8.4 everywhere once you decide to use them.

Steven Richardson
Steven Richardson

CTO at Digitonic. Writing about Laravel, architecture, and the craft of leading software teams from the west coast of Scotland.