Accessor & mutator — getting/setting model attributes with transformation
Concept
Accessor & mutator — Eloquent's hooks for transforming attribute values when GETTING or SETTING them on a model.
Accessor: Transforms a value when you READ it from the model. $user->full_name might combine first_name and last_name behind the scenes. You access a virtual attribute that doesn't exist in the database.
Mutator: Transforms a value when you WRITE it to the model. $user->password = 'secret' might automatically hash it via bcrypt() before storing.
PHP 8 syntax (Laravel 9+, the modern way): A single method returns a Illuminate\Database\Eloquent\Casts\Attribute object with get and/or set closures.
Old syntax (still works): getFirstNameAttribute() for accessor, setFirstNameAttribute() for mutator. Verbose but still supported.
Accessor vs Cast:
- Accessor: Custom logic in PHP. Can compute from multiple columns. Returns any value.
- Cast: Declarative type conversion. Defined in
$castsarray. PHP ↔ DB type conversion ('active' => 'boolean','metadata' => 'array').
Virtual attributes: Accessors can define attributes that have no DB column. full_name = first_name + last_name. These appear in toArray() only if added to $appends.
Code Example
<?php
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Casts\Attribute;
class User extends Model
{
protected $fillable = ['first_name', 'last_name', 'password', 'email'];
// PHP 8 ACCESSOR — transform on read
protected function fullName(): Attribute
{
return Attribute::make(
get: fn() => "{$this->first_name} {$this->last_name}",
// No 'set' — read-only virtual attribute
);
}
// PHP 8 MUTATOR — transform on write
protected function password(): Attribute
{
return Attribute::make(
get: fn($value) => $value, // no transform on read
set: fn($value) => bcrypt($value), // hash on write
);
}
// ACCESSOR + MUTATOR combined
protected function email(): Attribute
{
return Attribute::make(
get: fn($value) => strtolower($value), // always lowercase on read
set: fn($value) => strtolower(trim($value)), // clean up on write
);
}
}
// Usage
$user = new User(['first_name' => 'Alice', 'last_name' => 'Smith']);
echo $user->full_name; // 'Alice Smith' — virtual accessor, no DB column
$user->password = 'secret'; // mutator: stored as bcrypt('secret') in DB
$user->email = ' ALICE@EXAMPLE.COM '; // mutator: stored as 'alice@example.com'
// OLD SYNTAX (still works, pre-Laravel 9)
class UserOld extends Model
{
// Accessor: getAttributeNameAttribute()
public function getFullNameAttribute(): string
{
return "{$this->first_name} {$this->last_name}";
}
// Mutator: setAttributeNameAttribute($value)
public function setPasswordAttribute(string $value): void
{
$this->attributes['password'] = bcrypt($value);
}
}
// Making virtual attributes appear in toArray() / JSON
class Product extends Model
{
protected $appends = ['display_price']; // include in toArray()
protected function displayPrice(): Attribute
{
return Attribute::make(
get: fn() => '$' . number_format($this->price_cents / 100, 2),
);
}
}
$product = Product::find(1);
$product->toArray(); // includes: ['id' => 1, ..., 'display_price' => '$19.99']
// Caching — accessor results are cached after first access
$user->full_name; // computed
$user->full_name; // returns cached value (no recompute)
$user->clearAttrbiuteCacheFor('full_name'); // clear if needed after mutation