0

Accessor & mutator — getting/setting model attributes with transformation

Beginner5 min read·eng-14-010
laravel-src

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 $casts array. 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
<?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