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

Example · php
<?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.

Example · php
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.

Example · php
protected function password(): Attribute
{
    return Attribute::make(
        set: fn($value) => bcrypt($value)
    );
}

// Automatically hashed on assignment
$user->password = 'plain_text'; // stored as bcrypt hash

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

Example · php
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

  • Be the first to comment on this lesson.