PHP 8.4 Property Hooks in Laravel Models
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.
Steven is a software engineer with a passion for building scalable web applications. He enjoys sharing his knowledge through articles and tutorials.