Laravel

Laravel is a powerful and popular PHP framework that simplifies and accelerates web application development. Its elegant syntax, robust features like Eloquent ORM, Blade templating, and built-in security tools help developers create efficient and scalable apps. With strong community support and extensive documentation, Laravel is an ideal choice for both novice and experienced developers

Laravel Eloquent Accessors and Mutators: A Comprehensive Guide

19 May 2025 | Category:

In Laravel’s Eloquent ORM, accessors and mutators (also known as getters and setters) allow you to manipulate model attributes when retrieving or setting them. Accessors format or transform attribute values when accessing them, while mutators modify values before they are saved to the database. Starting with Laravel 9, the term “mutators” has largely been replaced with attribute casting for defining setters, but legacy mutators are still supported. This SEO-friendly, plagiarism-free guide explains how to use accessors, mutators, and attribute casting in Laravel, with practical examples and best practices. Based on Laravel 11 (as of May 19, 2025), this tutorial builds on previous discussions about Eloquent models and relationships and is designed for beginners and intermediate developers.


What are Accessors and Mutators?

  • Accessors: Methods that transform an attribute’s value when it is accessed (e.g., formatting a name to uppercase).
  • Mutators: Methods (or attribute casting in newer versions) that modify an attribute’s value before it is saved to the database (e.g., hashing a password).
  • Attribute Casting: A modern alternative to mutators, using the $casts property or Attribute class to define getters and setters.

These features allow you to encapsulate attribute logic within the model, keeping your code clean and maintainable.

Key Benefits

  • Encapsulation: Keep attribute formatting logic in the model.
  • Reusability: Apply transformations consistently across the application.
  • Readability: Simplify how attributes are accessed or set in controllers and views.
  • Flexibility: Support complex transformations without raw queries.

Defining Accessors

An accessor is a method that modifies an attribute’s value when it is retrieved from the model. Accessors are typically defined using the get{Attribute}Attribute naming convention (legacy) or the Attribute class (modern approach).

Legacy Accessor (Before Laravel 9)

Example: Capitalize the title attribute when accessed.

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    public function getTitleAttribute($value)
    {
        return strtoupper($value);
    }
}
  • Naming: getTitleAttribute targets the title column.
  • Usage:
  $post = Post::find(1);
  echo $post->title; // Outputs: MY POST (if stored as "My Post")

Modern Accessor (Laravel 9+)

Use the Attribute class for a more concise syntax.

namespace App\Models;

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

class Post extends Model
{
    protected function title(): Attribute
    {
        return Attribute::make(
            get: fn ($value) => strtoupper($value)
        );
    }
}
  • Syntax: Define a protected method named after the attribute (title), returning an Attribute instance.
  • Usage: Same as legacy:
  $post = Post::find(1);
  echo $post->title; // Outputs: MY POST

Defining Mutators

A mutator modifies an attribute’s value before it is saved to the database. In older versions, mutators used the set{Attribute}Attribute convention. In Laravel 9+, attribute casting with the Attribute class is preferred.

Legacy Mutator (Before Laravel 9)

Example: Store the title attribute in lowercase.

class Post extends Model
{
    public function setTitleAttribute($value)
    {
        $this->attributes['title'] = strtolower($value);
    }
}
  • Naming: setTitleAttribute targets the title column.
  • Usage:
  $post = new Post;
  $post->title = 'My Post';
  $post->save();
  // Stored in database as: my post

Modern Mutator (Laravel 9+)

Use the Attribute class to define a setter.

class Post extends Model
{
    protected function title(): Attribute
    {
        return Attribute::make(
            set: fn ($value) => strtolower($value)
        );
    }
}
  • Syntax: Add a set closure to the Attribute instance.
  • Usage: Same as legacy:
  $post = new Post;
  $post->title = 'My Post';
  $post->save();
  // Stored as: my post

Combining Accessor and Mutator

You can define both a getter and setter in the same Attribute.

class Post extends Model
{
    protected function title(): Attribute
    {
        return Attribute::make(
            get: fn ($value) => strtoupper($value),
            set: fn ($value) => strtolower($value)
        );
    }
}
  • Behavior:
  • When setting: Converts My Post to my post before saving.
  • When getting: Converts my post to MY POST when accessed.

Attribute Casting with $casts

The $casts property allows you to automatically cast attributes to specific data types (e.g., boolean, array, datetime) without defining accessors or mutators. This is a simpler alternative for common transformations.

Example:

class Post extends Model
{
    protected $casts = [
        'is_published' => 'boolean',
        'published_at' => 'datetime',
        'metadata' => 'array', // JSON column to PHP array
    ];
}
  • Usage:
  $post = Post::find(1);
  $post->is_published; // true (cast from 1)
  $post->published_at; // Carbon instance
  $post->metadata; // Array from JSON

Custom Casts

For complex transformations, create a custom cast class.

namespace App\Casts;

use Illuminate\Contracts\Database\Eloquent\CastsAttributes;

class TitleCase implements CastsAttributes
{
    public function get($model, $key, $value, $attributes)
    {
        return ucwords($value); // Capitalize each word
    }

    public function set($model, $key, $value, $attributes)
    {
        return strtolower($value); // Store in lowercase
    }
}

Use in Model:

class Post extends Model
{
    protected $casts = [
        'title' => TitleCase::class,
    ];
}
  • Usage:
  $post = new Post;
  $post->title = 'my post';
  $post->save(); // Stored as: my post
  echo $post->title; // Outputs: My Post

Practical Examples

Assume a posts table:

  • id, title, content, is_published, user_id, metadata (JSON), created_at, updated_at.

Migration:

Schema::create('posts', function (Blueprint $table) {
    $table->id();
    $table->string('title');
    $table->text('content');
    $table->boolean('is_published')->default(false);
    $table->foreignId('user_id')->constrained()->onDelete('cascade');
    $table->json('metadata')->nullable();
    $table->timestamps();
});

Model (app/Models/Post.php):

namespace App\Models;

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

class Post extends Model
{
    protected $fillable = ['title', 'content', 'is_published', 'user_id', 'metadata'];

    protected $casts = [
        'is_published' => 'boolean',
        'metadata' => 'array',
    ];

    // Accessor and Mutator for title
    protected function title(): Attribute
    {
        return Attribute::make(
            get: fn ($value) => ucwords($value),
            set: fn ($value) => strtolower($value)
        );
    }

    // Accessor for content excerpt
    protected function excerpt(): Attribute
    {
        return Attribute::make(
            get: fn () => Str::limit($this->content, 100)
        );
    }

    // Accessor and Mutator for metadata
    protected function metadata(): Attribute
    {
        return Attribute::make(
            get: fn ($value) => array_merge(['default' => 'value'], json_decode($value, true) ?? []),
            set: fn ($value) => json_encode($value)
        );
    }
}

Usage

// Create a post
$post = Post::create([
    'title' => 'My First POST',
    'content' => 'This is a long post content that will be truncated.',
    'is_published' => 1,
    'user_id' => 1,
    'metadata' => ['category' => 'Tech'],
]);

// Access attributes
echo $post->title; // Outputs: My First Post
echo $post->excerpt; // Outputs: This is a long post content that will be...
var_dump($post->metadata); // Outputs: ['default' => 'value', 'category' => 'Tech']
var_dump($post->is_published); // Outputs: true (boolean)

// Update metadata
$post->metadata = ['category' => 'News', 'priority' => 'high'];
$post->save();
echo $post->metadata['priority']; // Outputs: high

Explanation

  • Title: Stored as lowercase (my first post), accessed as title case (My First Post).
  • Excerpt: Virtual accessor (not stored) that limits content to 100 characters.
  • Metadata: JSON column cast to an array, with a default value merged on retrieval.
  • Is Published: Cast to boolean for consistent type handling.

Best Practices for Accessors and Mutators

  1. Use Modern Syntax:
  • Prefer Attribute class over legacy get/set methods for Laravel 9+.
  1. Keep Logic Simple:
  • Avoid complex logic in accessors/mutators; use services or helpers for heavy processing.
  1. Leverage $casts:
  • Use $casts for simple type conversions (e.g., boolean, array, datetime).
  1. Create Custom Casts:
  • Use custom cast classes for reusable, complex transformations.
  1. Protect Attributes:
  • Ensure modified attributes are in $fillable for mass assignment:
    php protected $fillable = ['title', 'content', 'metadata'];
  1. Document Accessors:
  • Use PHPDoc to clarify virtual accessors: “`php /**
    • Get a truncated excerpt of the content.
      *
    • @return string
      */
      protected function excerpt(): Attribute
      “`
  1. Test Transformations:
  • Test accessors and mutators in unit tests or php artisan tinker:
    php $post = new App\Models\Post(['title' => 'Test']); echo $post->title;
  1. Avoid Overuse:
  • Only use accessors/mutators when necessary to avoid performance overhead.
  1. Combine with Relationships:
  • Use accessors to format related data:
    php protected function authorName(): Attribute { return Attribute::make( get: fn () => $this->user->name ); }

Debugging Accessors and Mutators

  • Unexpected Output:
  • Verify accessor/mutator logic (e.g., check strtoupper vs. ucwords).
  • Ensure method names match attributes (e.g., title for protected function title()).
  • Database Issues:
  • Check that mutators produce valid data for the column type (e.g., JSON for metadata).
  • Confirm column exists in the table.
  • Mass Assignment Errors:
  • Ensure attributes are in $fillable or $guarded is configured.
  • Debugging:
  • Use dd() to inspect values:
    php $post = Post::find(1); dd($post->title, $post->getRawOriginal('title'));
  • Enable query logging to verify database writes:
    php DB::enableQueryLog(); $post->save(); dd(DB::getQueryLog());
  • Logs:
  • Check storage/logs/laravel.log for errors.

Example: Complete Accessors and Mutators Workflow

Step 1: Migration

php artisan make:migration create_posts_table

Migration (database/migrations/2025_05_19_123456_create_posts_table.php):

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->text('content');
            $table->boolean('is_published')->default(false);
            $table->foreignId('user_id')->constrained()->onDelete('cascade');
            $table->json('metadata')->nullable();
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('posts');
    }
};

Step 2: Model

php artisan make:model Post

Model (app/Models/Post.php):

namespace App\Models;

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

class Post extends Model
{
    protected $fillable = ['title', 'content', 'is_published', 'user_id', 'metadata'];

    protected $casts = [
        'is_published' => 'boolean',
        'metadata' => 'array',
    ];

    // Title: Store lowercase, retrieve title case
    protected function title(): Attribute
    {
        return Attribute::make(
            get: fn ($value) => ucwords($value),
            set: fn ($value) => strtolower($value)
        );
    }

    // Excerpt: Virtual accessor for content preview
    protected function excerpt(): Attribute
    {
        return Attribute::make(
            get: fn () => Str::limit($this->content, 100)
        );
    }

    // Metadata: Merge default values
    protected function metadata(): Attribute
    {
        return Attribute::make(
            get: fn ($value) => array_merge(['default' => 'value'], json_decode($value, true) ?? []),
            set: fn ($value) => json_encode($value)
        );
    }

    // Relationship
    public function user()
    {
        return $this->belongsTo(User::class);
    }

    // Virtual accessor for author name
    protected function authorName(): Attribute
    {
        return Attribute::make(
            get: fn () => $this->user->name
        );
    }
}

Step 3: Controller

php artisan make:controller PostController

Controller (app/Http/Controllers/PostController.php):

namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Http\Request;

class PostController extends Controller
{
    public function index()
    {
        $posts = Post::with('user')->where('is_published', true)->get();
        return view('posts.index', compact('posts'));
    }

    public function store(Request $request)
    {
        $post = Post::create($request->validate([
            'title' => 'required|string|max:255',
            'content' => 'required|string',
            'is_published' => 'boolean',
            'user_id' => 'required|exists:users,id',
            'metadata' => 'nullable|array',
        ]));
        return redirect()->route('posts.index');
    }
}

Step 4: Blade View

View (resources/views/posts/index.blade.php):

@extends('layouts.app')

@section('content')
    <h1>Posts</h1>
    @forelse ($posts as $post)
        <article>
            <h2>{{ $post->title }}</h2>
            <p>By {{ $post->author_name }}</p>
            <p>{{ $post->excerpt }}</p>
            <p>Category: {{ $post->metadata['category'] ?? 'N/A' }}</p>
        </article>
    @empty
        <p>No posts found.</p>
    @endforelse
@endsection

Step 5: Routes

Routes (routes/web.php):

use App\Http\Controllers\PostController;

Route::get('/posts', [PostController::class, 'index'])->name('posts.index');
Route::post('/posts', [PostController::class, 'store'])->name('posts.store');

Step 6: Run and Test

  1. Run migrations:
   php artisan migrate
  1. Seed data (e.g., via tinker):
   $user = App\Models\User::create(['name' => 'John', 'email' => 'john@example.com']);
   App\Models\Post::create([
       'title' => 'Test Post',
       'content' => 'This is a test post content.',
       'is_published' => true,
       'user_id' => $user->id,
       'metadata' => ['category' => 'Tech'],
   ]);
  1. Access /posts to view formatted data.

Conclusion

Laravel’s accessors and mutators (or attribute casting) provide a powerful way to transform model attributes, ensuring consistent formatting and encapsulation of logic. By using the modern Attribute class, $casts, or custom cast classes, you can handle simple type conversions or complex transformations efficiently. Combining these with relationships and controllers creates a robust data management workflow.

Next Steps:

  • Add accessors/mutators to a model: protected function attribute(): Attribute.
  • Experiment with $casts for type conversions.
  • Test transformations in php artisan tinker.

For deeper insights, explore Laravel’s official documentation or connect with the Laravel community on platforms like X. Start enhancing your models with accessors and mutators today!

Laravel Accessors and Mutators Example

This artifact provides a practical example of using accessors, mutators, and attribute casting in a Post model.

Migration (database/migrations/2025_05_19_123456_create_posts_table.php)

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->text('content');
            $table->boolean('is_published')->default(false);
            $table->foreignId('user_id')->constrained()->onDelete('cascade');
            $table->json('metadata')->nullable();
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('posts');
    }
};

Model (app/Models/Post.php)

<?php

namespace App\Models;

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

class Post extends Model
{
    protected $fillable = ['title', 'content', 'is_published', 'user_id', 'metadata'];

    protected $casts = [
        'is_published' => 'boolean',
        'metadata' => 'array',
    ];

    /**
     * Accessor and Mutator for title.
     */
    protected function title(): Attribute
    {
        return Attribute::make(
            get: fn ($value) => ucwords($value),
            set: fn ($value) => strtolower($value)
        );
    }

    /**
     * Accessor for content excerpt.
     */
    protected function excerpt(): Attribute
    {
        return Attribute::make(
            get: fn () => Str::limit($this->content, 100)
        );
    }

    /**
     * Accessor and Mutator for metadata.
     */
    protected function metadata(): Attribute
    {
        return Attribute::make(
            get: fn ($value) => array_merge(['default' => 'value'], json_decode($value, true) ?? []),
            set: fn ($value) => json_encode($value)
        );
    }

    public function user()
    {
        return $this->belongsTo(User::class);
    }

    /**
     * Accessor for author name.
     */
    protected function authorName(): Attribute
    {
        return Attribute::make(
            get: fn () => $this->user->name
        );
    }
}

Usage

  1. Ensure the users table exists and is seeded.
  2. Save the migration and model files.
  3. Run the migration:
   php artisan migrate
  1. Seed sample data in php artisan tinker:
   $user = App\Models\User::create(['name' => 'John', 'email' => 'john@example.com']);
   App\Models\Post::create([
       'title' => 'Test Post',
       'content' => 'This is a test post content that will be truncated.',
       'is_published' => true,
       'user_id' => $user->id,
       'metadata' => ['category' => 'Tech'],
   ]);
  1. Test accessors:
   $post = App\Models\Post::find(1);
   echo $post->title; // Outputs: Test Post
   echo $post->excerpt; // Outputs: This is a test post content that will be...
   echo $post->author_name; // Outputs: John
   var_dump($post->metadata); // Outputs: ['default' => 'value', 'category' => 'Tech']

This example demonstrates modern accessors and mutators using the Attribute class, along with $casts for type conversion and a virtual accessor for related data.