Laravel Eloquent Accessors and Mutators: A Comprehensive Guide
19 May 2025 | Category: Laravel
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 orAttribute
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 thetitle
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 anAttribute
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 thetitle
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 theAttribute
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
tomy post
before saving. - When getting: Converts
my post
toMY 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
- Use Modern Syntax:
- Prefer
Attribute
class over legacyget/set
methods for Laravel 9+.
- Keep Logic Simple:
- Avoid complex logic in accessors/mutators; use services or helpers for heavy processing.
- Leverage $casts:
- Use
$casts
for simple type conversions (e.g.,boolean
,array
,datetime
).
- Create Custom Casts:
- Use custom cast classes for reusable, complex transformations.
- Protect Attributes:
- Ensure modified attributes are in
$fillable
for mass assignment:php protected $fillable = ['title', 'content', 'metadata'];
- Document Accessors:
- Use PHPDoc to clarify virtual accessors: “`php /**
- Get a truncated excerpt of the content.
* - @return string
*/
protected function excerpt(): Attribute
“`
- Get a truncated excerpt of the content.
- Test Transformations:
- Test accessors and mutators in unit tests or
php artisan tinker
:php $post = new App\Models\Post(['title' => 'Test']); echo $post->title;
- Avoid Overuse:
- Only use accessors/mutators when necessary to avoid performance overhead.
- 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
forprotected 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
- Run migrations:
php artisan migrate
- 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'],
]);
- 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
- Ensure the
users
table exists and is seeded. - Save the migration and model files.
- Run the migration:
php artisan migrate
- 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'],
]);
- 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.