0

Accessors and mutators (PHP 8 style: get/setNameAttribute → Attribute::make)

Intermediate5 min read·lv-12-014
laravel-src

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.

MechanismWhen to useRuns on
$castsSimple type coercionget and set
Attribute::make(get:) accessorDerived/computed strings, formatted valuesget only
Attribute::make(set:) mutatorNormalize before save (hash, trim, split)set only
Both get + set in one AttributeRound-trip transformation (money, encrypted)both

Code Example

php
<?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 = 1

Interview 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.