Accessors, Mutators & Custom Casts
Shape how attributes look in PHP versus how they sit in the database — cleanly and testably.
Your database columns and your application's mental model rarely match one-to-one. Money is stored as integer cents but you want a Money object. A name is two columns but you want $user->full_name. Accessors, mutators and casts bridge that gap so the rest of your code stays clean.
The modern Attribute API
Since Laravel 9 a single method returns an Attribute with a get and/or set closure — no more paired getXAttribute/setXAttribute.
use Illuminate\Database\Eloquent\Casts\Attribute;
protected function fullName(): Attribute
{
return Attribute::make(
get: fn () => "{$this->first_name} {$this->last_name}",
);
}
// $user->full_name;Built-in vs custom casts
The $casts array (or the casts() method in Laravel 12) handles the common ones: array, boolean, datetime, encrypted, AsStringable, enums. When you need real domain logic — a value object spanning several columns — you write a CastsAttributes class.
Example
<?php
namespace App\Casts;
use App\ValueObjects\Money;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
/**
* Casts a (amount_cents, currency) column pair <-> a Money value object.
* @implements CastsAttributes<Money, Money>
*/
class MoneyCast implements CastsAttributes
{
public function get(Model $model, string $key, mixed $value, array $attributes): ?Money
{
if ($attributes['amount_cents'] === null) {
return null;
}
return new Money(
cents: (int) $attributes['amount_cents'],
currency: $attributes['currency'] ?? 'USD',
);
}
/** @return array<string,mixed> */
public function set(Model $model, string $key, mixed $value, array $attributes): array
{
if (! $value instanceof Money) {
throw new \InvalidArgumentException('price must be a Money instance.');
}
return [
'amount_cents' => $value->cents,
'currency' => $value->currency,
];
}
}
// --- Wiring it up on the model (Laravel 12 casts() method) ---
namespace App\Models;
use App\Casts\MoneyCast;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
protected function casts(): array
{
return [
'price' => MoneyCast::class,
'options' => 'array',
'released_at'=> 'datetime',
'is_active' => 'boolean',
];
}
protected function slug(): Attribute
{
return Attribute::make(
get: fn (?string $value) => $value,
set: fn (string $value) => \Illuminate\Support\Str::slug($value),
);
}
}When to use it
- A User model defines a full_name accessor that concatenates first_name and last_name so views can call $user->full_name without any controller logic.
- A settings column stored as JSON is declared in $casts as 'array' so Eloquent automatically encodes and decodes it on save and retrieval.
- A price column is cast to 'decimal:2' to ensure that arithmetic in PHP always works with correctly rounded floats instead of raw strings.
More examples
Accessor (Laravel 9+ style)
An Attribute accessor using the modern Laravel 9+ syntax; the computed value is accessed as a regular property.
use Illuminate\Database\Eloquent\Casts\Attribute;
class User extends Model
{
protected function fullName(): Attribute
{
return Attribute::make(
get: fn() => trim($this->first_name . ' ' . $this->last_name)
);
}
}
// Usage
echo $user->full_name; // 'Alice Smith'Mutator (setter) alongside an accessor
The set callback in Attribute::make() intercepts assignment, here hashing the password transparently before it is stored.
protected function password(): Attribute
{
return Attribute::make(
set: fn($value) => bcrypt($value)
);
}
// Automatically hashed on assignment
$user->password = 'plain_text'; // stored as bcrypt hashNative casts for automatic type conversion
Each entry in $casts tells Eloquent to convert the raw database string to the specified PHP type when reading, and back when writing.
class Product extends Model
{
protected $casts = [
'price' => 'decimal:2',
'is_featured' => 'boolean',
'published_at' => 'datetime',
'metadata' => 'array', // JSON <-> PHP array
'tags' => 'collection', // JSON <-> Collection
];
}
Discussion