Introduction

Accessors and mutators transform model attributes on read and write, using the modern Attribute class syntax introduced in Laravel 9 and still current in Laravel 13.

Key Concepts

  • Attribute::make: The factory that creates an accessor/mutator pair with get and set closures.
  • get closure: Runs on attribute access, returning the transformed value.
  • set closure: Runs on attribute assignment, transforming the value before it hits the database.
  • shouldCache: Caches the result of an accessor for the current request so expensive computations only run once.
  • $appends: A model-level property listing accessors that should be included in array/JSON output.

Real World Context

Every app has derived values — full name from first/last, formatted price from cents, avatar URL from an S3 key. Accessors centralize that logic on the model instead of scattering it across views and controllers.

Deep Dive

Accessors and mutators allow you to transform Eloquent attribute values when retrieving or setting them on model instances. They're perfect for formatting data consistently.

Defining Accessors

Accessors transform attributes when you get them:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * Get the user's full name.
     * Combines first_name and last_name.
     */
    protected function fullName(): Attribute
    {
        return Attribute::make(
            get: fn () => "{$this->first_name} {$this->last_name}",
        );
    }

    /**
     * Always return email in lowercase.
     */
    protected function email(): Attribute
    {
        return Attribute::make(
            get: fn (string $value) => strtolower($value),
        );
    }

    /**
     * Format the created_at for display.
     */
    protected function createdAtFormatted(): Attribute
    {
        return Attribute::make(
            get: fn () => $this->created_at->format('F j, Y'),
        );
    }
}

Using Accessors

php
$user = User::find(1);

// first_name: "John", last_name: "Doe"
echo $user->full_name;  // "John Doe"

echo $user->email;      // "john@example.com" (lowercase)

echo $user->created_at_formatted;  // "January 15, 2024"

Defining Mutators

Mutators transform attributes when you set them:

php
class User extends Model
{
    /**
     * Always store email in lowercase.
     */
    protected function email(): Attribute
    {
        return Attribute::make(
            set: fn (string $value) => strtolower($value),
        );
    }

    /**
     * Capitalize each word of the name when setting.
     * Note: for passwords, prefer the built-in 'hashed' cast (see the next lesson)
     * over a custom mutator — it's safer and idiomatic since Laravel 10.
     */
    protected function name(): Attribute
    {
        return Attribute::make(
            set: fn (string $value) => ucwords(strtolower($value)),
        );
    }

    /**
     * Set multiple attributes from one input.
     */
    protected function fullName(): Attribute
    {
        return Attribute::make(
            set: function (string $value) {
                $parts = explode(' ', $value, 2);
                return [
                    'first_name' => $parts[0],
                    'last_name' => $parts[1] ?? '',
                ];
            },
        );
    }
}

Using Mutators

php
$user = new User;

$user->email = 'JOHN@EXAMPLE.COM';
// Stored as: "john@example.com"

$user->name = 'john DOE';
// Stored as: "John Doe"

$user->full_name = 'John Doe';
// Sets first_name = "John", last_name = "Doe"

Combined Accessor and Mutator

php
protected function phoneNumber(): Attribute
{
    return Attribute::make(
        // Get: Format for display
        get: fn (string $value) => sprintf(
            '(%s) %s-%s',
            substr($value, 0, 3),
            substr($value, 3, 3),
            substr($value, 6)
        ),
        // Set: Store only digits
        set: fn (string $value) => preg_replace('/[^0-9]/', '', $value),
    );
}
php
$user->phone_number = '(555) 123-4567';
// Stored as: "5551234567"

echo $user->phone_number;
// Displayed as: "(555) 123-4567"

Caching Accessors

For expensive computations:

php
protected function avatarUrl(): Attribute
{
    return Attribute::make(
        get: fn () => $this->calculateGravatarUrl(),
    )->shouldCache();  // Cache the result
}

// The calculation only runs once per request
echo $user->avatar_url;  // Calculates
echo $user->avatar_url;  // Uses cached value

Appending Accessors to JSON

php
class User extends Model
{
    /**
     * Accessors to append to model's array/JSON.
     */
    protected $appends = ['full_name', 'avatar_url'];

    protected function fullName(): Attribute
    {
        return Attribute::make(
            get: fn () => "{$this->first_name} {$this->last_name}",
        );
    }
}
php
$user->toArray();
// ['id' => 1, 'first_name' => 'John', ..., 'full_name' => 'John Doe']

$user->toJson();
// {"id": 1, "first_name": "John", ..., "full_name": "John Doe"}

Real-World Examples

php
class Product extends Model
{
    /**
     * Store price in cents, expose as dollars.
     */
    protected function price(): Attribute
    {
        return Attribute::make(
            get: fn (int $value) => $value / 100,   // Cents -> dollars
            set: fn (float $value) => $value * 100, // Dollars -> cents
        );
    }

    /**
     * Format the dollar price as a currency string.
     * Note: $this->price already goes through the price() accessor above,
     * so it's in dollars by the time it reaches here — we do NOT divide again.
     */
    protected function priceFormatted(): Attribute
    {
        return Attribute::make(
            get: fn () => '$' . number_format($this->price, 2),
        );
    }
}

class Post extends Model
{
    /**
     * Generate excerpt from body.
     */
    protected function excerpt(): Attribute
    {
        return Attribute::make(
            get: fn () => Str::limit(strip_tags($this->body), 150),
        );
    }

    /**
     * Calculate reading time.
     */
    protected function readingTime(): Attribute
    {
        return Attribute::make(
            get: function () {
                $words = str_word_count(strip_tags($this->body));
                $minutes = ceil($words / 200);  // 200 words per minute
                return "{$minutes} min read";
            },
        );
    }
}

Common Pitfalls

  1. Mixing old and new accessor syntax — The legacy getXxxAttribute/setXxxAttribute methods still work in Laravel 13 and are not deprecated, but the Attribute class is the idiomatic modern style. Pick one style per model and stay consistent.
  2. Forgetting $appends — Derived attributes are missing from toArray() and toJson() unless you list them.
  3. Non-idempotent mutators — If set transforms a value that's already been set, you get double-transformations.

Best Practices

  1. Use the Attribute class style — protected function name(): Attribute { return Attribute::make(...); }.
  2. Cache expensive accessors with shouldCache — For anything that does I/O or heavy computation.
  3. Append only the values clients need — $appends bloats every JSON response; use it sparingly.

Summary

  • Define accessors and mutators via protected function name(): Attribute.
  • Attribute::make(get: ..., set: ...) creates the pair.
  • shouldCache() memoizes the result per request.
  • $appends lists derived attributes for toArray/toJson output.
  • Use realistic transformations: formatting money, generating URLs, combining name parts.
✓ Completed