skip to content

In Laravel, how do you define an Eloquent accessor and mutator with Attribute::make(), and when do you write a CastsAttributes class instead?

level: middleimportance: must knowfreq 56%

answer

  1. camelCase method returning Attribute
  2. return type must be declared
  3. get(value, attributes), set returns array
  4. cast class: reusable across models
  5. make:cast scaffolds get and set

basics

~20 s

Declare a camelCase method with a declared Attribute return type that returns Attribute::make(get: ..., set: ...). The get closure transforms reads and set can return several columns. Write a CastsAttributes class when the same conversion is reused across attributes or models.

solid answer

~50 s

For a `total` attribute, define `protected function total(): Attribute` returning `Attribute::make(get: fn ($value, $attributes) => ..., set: fn ($value) => [...])`. Eloquent finds it by reflecting on the method's **declared return type**, so forgetting `: Attribute` silently disables it. `get` receives the raw value and all raw attributes; `set` returns the stored value or an array of columns, such as `['total_cents' => ..., 'currency' => ...]`. Object results are cached by default and `shouldCache()` also caches scalars. A `CastsAttributes` class (`php artisan make:cast MoneyCast`) holds the same get/set logic in a reusable class that you list in `casts()`, optionally with parameters such as `MoneyCast::class.':EUR'`. Use an accessor for one model's computed or formatted value, a cast class for a value type such as money shared across models. The older `getTotalAttribute()` and `setTotalAttribute()` methods still work.

code

php · 24 lines
php
<?php

namespace App\Casts;

use App\Support\Money;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;

class MoneyCast implements CastsAttributes
{
    public function get(Model $model, string $key, mixed $value, array $attributes): ?Money
    {
        return isset($attributes['total_cents'])
            ? new Money((int) $attributes['total_cents'], $attributes['currency'])
            : null;
    }

    public function set(Model $model, string $key, mixed $value, array $attributes): array
    {
        return $value === null
            ? ['total_cents' => null, 'currency' => null]
            : ['total_cents' => $value->cents, 'currency' => $value->currency];
    }
}

go deeper

for a junior

Recall that an accessor is a camelCase method returning Attribute::make() with get and set closures.

for a middle

Explain return-type detection, the closure arguments, multi-column set arrays, object caching, and when a cast class is the better home.

for a senior

Design value-object casts for money or addresses, avoid mutable cached objects, and know where mutators are bypassed.

for a principal

Set a team rule separating formatting accessors from domain value types, so conversions are reused and tested in one place.

## Two tools for the same job Both an **accessor/mutator** and a **custom cast** transform an attribute on read and on write. They differ in where the logic lives and how widely it is reused. ## Accessors and mutators with Attribute::make() An accessor is a model method named after the attribute in camelCase that **declares** the return type `Illuminate\Database\Eloquent\Casts\Attribute`: ```php use Illuminate\Database\Eloquent\Casts\Attribute; protected function customerName(): Attribute { return Attribute::make( get: fn (?string $value) => $value === null ? null : ucwords($value), set: fn (string $value) => strtolower(trim($value)), ); } ``` How Eloquent treats it, reading the framework's attribute code: 1. When `customer_name` is read or written, Eloquent looks for a method `customerName()` and **reflects on its declared return type**. Without `: Attribute`, the method is not recognised and the raw value is returned. 2. `get` receives the raw value and the array of all raw attributes, which is how a computed attribute reads other columns. 3. `set` receives the new value and may return a single value or an **array of columns**, so one virtual attribute can write two. 4. If `get` returns an **object**, the result is cached on the model and later reads return the same instance; `withoutObjectCaching()` turns that off, and `shouldCache()` caches scalar results too, for expensive computations. `Attribute::get(...)` and `Attribute::set(...)` build one-sided attributes. The older style, `getCustomerNameAttribute()` and `setCustomerNameAttribute()`, still works; new code uses `Attribute`. ## Custom casts with CastsAttributes A cast class implements `Illuminate\Contracts\Database\Eloquent\CastsAttributes`, with `get($model, $key, $value, $attributes)` and `set($model, $key, $value, $attributes)`. You attach it in `casts()`: ```php protected function casts(): array { return ['total' => MoneyCast::class]; } ``` `php artisan make:cast MoneyCast` scaffolds the class. Arguments after a colon (`MoneyCast::class.':EUR'`) are passed to the constructor. Related contracts cover edge cases: `CastsInboundAttributes` for write-only transformations, `SerializesCastableAttributes` for custom array and JSON output, and `Castable`, which lets a value-object class name its own caster so you can write `'total' => Money::class`. ## Choosing between them | Question | Accessor (`Attribute`) | Cast class | |---|---|---| | Used by one model only? | good fit | overkill | | Same value type on many models (money, address) | duplicated logic | one class, listed everywhere | | Needs parameters per attribute | closures capture them | colon arguments | | Computed from other columns, no column of its own | natural (`$attributes`) | awkward | | Testable in isolation | via the model | plain class | A rule of thumb: **formatting and derived values** are accessors; **value types** are cast classes. ## What happens on a multi-column write With `'total' => MoneyCast::class` in `casts()`: 1. `$invoice->total = new Money(1990, 'EUR')` calls the cast's `set()`, which returns `['total_cents' => 1990, 'currency' => 'EUR']`. 2. Eloquent merges that array into the raw attributes, so both columns become dirty. 3. The `Money` object is cached, so reading `$invoice->total` returns the same instance without calling `get()` again. 4. `save()` writes both columns in one `UPDATE`. The same flow works for an accessor whose `set` closure returns an array; the difference is only where the code lives. ## Traps - A computed accessor with no matching column is **not** in `toArray()` unless it is appended. - Mutators do not run for query-level `update()` calls on the builder, because no model is involved. - If `get` returns a value object and code mutates it, the cached object is merged back through `set` on save; returning immutable value objects avoids surprise writes.

  • Why might an Attribute accessor silently not run?
    Eloquent detects `Attribute` accessors by reflecting on the method's declared return type and caches the result per class. A method named correctly but without `: Attribute` is ignored, so the raw column value comes back. A misspelt camelCase name has the same effect.
  • How does a value-object class avoid naming its cast in every model?
    It implements `Castable` and returns its caster from `castUsing(array $arguments)`. Models then write `'total' => Money::class` in `casts()`, and Eloquent asks `Money` for the cast class. The same pattern powers `AsCollection` and `AsArrayObject`.

saying these in an interview costs you the question

  • Naming the accessor method in snake_case after the column
  • Omitting the Attribute return type and expecting the accessor to work
  • Believing the set closure can only return a single value
  • Thinking getFooAttribute() methods were removed from Eloquent
  • Expecting mutators to run on query-builder update() calls