Accessors and mutators (PHP 8 style: get/setNameAttribute → Attribute::make)
Concept
Accessors and mutators let you transform attribute values when you read from or write to an Eloquent model, without touching the database schema. The modern API (Laravel 9+) uses Illuminate\Database\Eloquent\Casts\Attribute::make(get: ..., set: ...) and a method named after the camelCase attribute. The legacy API used get{Name}Attribute / set{Name}Attribute methods and still works, but mixing both styles in one model is confusing — pick one.
An accessor runs when you access $model->full_name. The closure receives the raw stored value (or null if it is a computed attribute that has no column). A mutator runs when you assign $model->password = 'secret' and transforms the value before it hits the $attributes array (and eventually the database). Crucially, a mutator can return an array to set multiple underlying columns from one virtual attribute — the pattern for JSON-backed value objects.
Computed attributes — those with Attribute::make(get: ...) but no corresponding database column — can be serialized into toArray() / toJson() output by listing them in $appends. This is the right way to expose derived data to API consumers without adding junk columns to the schema.
The $casts array is a lighter-weight alternative when the transformation is simple: 'is_admin' => 'boolean', 'options' => 'array', 'published_at' => 'datetime', 'price' => 'decimal:2'. Laravel 10+ supports cast classes (objects implementing CastsAttributes) for domain-specific casting. $casts runs before accessors; if both exist for the same attribute, the accessor sees the already-cast value.
| Mechanism | When to use | Runs on |
|---|---|---|
$casts | Simple type coercion | get and set |
Attribute::make(get:) accessor | Derived/computed strings, formatted values | get only |
Attribute::make(set:) mutator | Normalize before save (hash, trim, split) | set only |
Both get + set in one Attribute | Round-trip transformation (money, encrypted) | both |
Code Example
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
class User extends Model
{
protected $casts = [
'is_admin' => 'boolean', // stored as 0/1, read as bool
'settings' => 'array', // stored as JSON string, read as PHP array
'email_verified_at' => 'datetime', // stored as DATETIME, read as Carbon
'balance' => 'decimal:2', // stored as DECIMAL(10,2), read as string with 2dp
];
// Appended computed attributes (no column needed)
protected $appends = ['full_name', 'gravatar_url'];
// Accessor — combines first_name + last_name columns
// $user->full_name → "Jane Doe"
protected function fullName(): Attribute
{
return Attribute::make(
get: fn () => trim("{$this->first_name} {$this->last_name}"),
);
}
// Accessor with no setter — derived from email column
protected function gravatarUrl(): Attribute
{
return Attribute::make(
get: fn () => 'https://www.gravatar.com/avatar/' . md5(strtolower($this->email)),
);
}
// Mutator — hash password before storing
// $user->password = 'secret' → stored as bcrypt hash
protected function password(): Attribute
{
return Attribute::make(
set: fn (string $value) => bcrypt($value),
);
}
// Mutator that sets multiple columns from one virtual assignment
// $user->address = ['street' => '1 Main St', 'city' => 'London']
// → writes address_street and address_city columns separately
protected function address(): Attribute
{
return Attribute::make(
get: fn ($value, array $attributes) => [
'street' => $attributes['address_street'] ?? null,
'city' => $attributes['address_city'] ?? null,
],
set: fn (array $value) => [
'address_street' => $value['street'],
'address_city' => $value['city'],
],
);
}
}
// --- Usage ---
$user = User::find(1);
echo $user->full_name; // "Jane Doe" — accessor
echo $user->gravatar_url; // "https://www.gravatar.com/avatar/..."
var_dump($user->is_admin); // bool(true) — cast
var_dump($user->settings); // array(...) — cast from JSON
$user->password = 'newpass'; // mutator hashes before storing
$user->save();
// SQL: UPDATE users SET password = '$2y$12$...' WHERE id = 1Interview Q&A
Q: What is the difference between using $casts and defining an Attribute::make() accessor for the same column?
$casts is a declarative type mapping: Laravel applies it automatically on both read and write using the built-in cast registry in Illuminate\Database\Eloquent\Concerns\HasAttributes. It handles the common cases (boolean, array, datetime, decimal, encrypted) with zero boilerplate. Attribute::make() gives you a PHP closure, so you can perform arbitrary logic — formatting, calling external services, combining multiple columns. Use $casts for pure type coercion. Use Attribute::make() when you need custom logic or want to produce a computed value from several columns. If both are defined for the same attribute, the cast runs first on raw DB data, then the accessor closure receives the already-cast value.
Q: How do you add a computed attribute (no database column) to a model's JSON output?
Define an Attribute::make(get: ...) accessor method for the attribute, then list the snake_case attribute name in the model's $appends array. When toArray() or toJson() is called — including implicitly when returning a model from a controller — Eloquent iterates $appends, calls each accessor, and merges the result into the serialization output. Never define the column in a migration; the accessor simply derives its value from other attributes or external logic at read time.
Q: A mutator needs to derive two column values from a single virtual property. How do you implement that in the modern Eloquent API?
Return an array from the set closure inside Attribute::make(). The keys of the returned array must match existing column names (or $attributes keys). Eloquent's setAttribute method in HasAttributes detects that the returned value is an array and merges it directly into $this->attributes, effectively setting multiple underlying fields from one high-level assignment. This is the correct pattern for value objects like Money, Address, or EncryptedPayload that span multiple columns.