Writing Clean Accessors and Mutators in Laravel
If you have been building applications with Laravel for a few years, you are likely familiar with defining accessors and mutators using the traditional prefixed method naming convention: getFirstNameAttribute() and setFirstNameAttribute().
While that older approach still works for backward compatibility, modern Laravel introduced a much cleaner, more expressive syntax using the Illuminate\Database\Eloquent\Casts\Attribute return type. This modern approach combines both reading (accessor) and writing (mutator) logic into a single, unified method.
In this tutorial, we will explore the modern way to write Eloquent accessors and mutators in Laravel, understand how they compare to the legacy approach, look at practical examples, and learn how to take advantage of built-in attribute caching.
What are Accessors and Mutators?
Before diving into the syntax, let's quickly recap what they do:
- Accessors: Transform an Eloquent attribute value when you retrieve it from the database (e.g., formatting a date, capitalizing a string, or generating a computed property).
- Mutators: Transform an Eloquent attribute value when you set it on a model before saving it to the database (e.g., lowercasing an email address or hashing a password).
The Old Way vs. The Modern Way
To appreciate why the modern syntax is better, let's look at how we used to write them compared to how we write them today.
The Legacy Approach (Laravel 8 and earlier)
Previously, you had to define two completely separate methods for the same database column:
// Old Accessor
public function getFirstNameAttribute($value)
{
return ucfirst($value);
}
// Old Mutator
public function setFirstNameAttribute($value)
{
$this->attributes['first_name'] = strtolower($value);
}
The Modern Approach (Laravel 9, 10, 11+)
In modern Laravel, both the getter and setter are defined inside a single method that returns an instance of Illuminate\Database\Eloquent\Casts\Attribute:
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* Interact with the user's first name.
*/
protected function firstName(): Attribute
{
return Attribute::make(
get: fn (string $value) => ucfirst($value),
set: fn (string $value) => strtolower($value),
);
}
}
Let's look at why this is a massive improvement:
- Unified Definition: Getting and setting logic for a column lives in a single place.
- Clean Method Names: The method name matches the attribute directly in camelCase (e.g.,
firstName()forfirst_name).
- Type Safety: Full return type hinting with
Attributeand closure argument types.
- PHP 8 Named Arguments: You can define
get:,set:, or both using named arguments.
Practical Code Examples
Let's look at realistic scenarios where modern accessors and mutators shine.
1. Cleaning and Normalizing Email Addresses
When users register, they might enter emails with trailing spaces or mixed casing (e.g., User@Example.COM ). We can use a mutator to automatically trim and lowercase the email address every time it is set:
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
protected $fillable = ['name', 'email', 'password'];
protected function email(): Attribute
{
return Attribute::make(
set: fn (string $value) => strtolower(trim($value)),
);
}
}
Now, whenever you assign an email, Laravel sanitizes it automatically before inserting or updating:
$user = User::create([
'name' => 'John Doe',
'email' => ' John.Doe@EXAMPLE.com ',
'password' => bcrypt('secret123'),
]);
echo $user->email; // Output: john.doe@example.com
2. Creating Virtual Accessors (Attributes Not in the Database)
An accessor doesn't need to correspond to an existing physical column in your database. You can create virtual computed attributes by accepting the $attributes array in your getter closure.
Here is how we create a virtual fullName attribute from first_name and last_name:
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
class Customer extends Model
{
protected function fullName(): Attribute
{
return Attribute::make(
get: fn (mixed $value, array $attributes) => trim("{$attributes['first_name']} {$attributes['last_name']}")
);
}
}
Now you can access it anywhere like a normal property:
$customer = Customer::find(1);
echo $customer->full_name; // Output: Kishan Kumar
3. Storing Currency Amounts in Cents/Paise and Displaying in Standard Units
A standard database best practice for financial data is storing monetary amounts as integers (e.g., cents or paise) to avoid floating-point rounding errors. With modern accessors and mutators, we can convert transparently:
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
protected $fillable = ['name', 'price'];
/**
* Stored in DB as integer (cents), accessed as decimal.
*/
protected function price(): Attribute
{
return Attribute::make(
get: fn (int $value) => $value / 100, // e.g., 2999 becomes 29.99
set: fn (float $value) => (int) round($value * 100), // e.g., 29.99 becomes 2999
);
}
}
Caching Complex Accessor Calculations
If your accessor performs a resource-heavy transformation—such as parsing Markdown, manipulating a string with complex regular expressions, or formatting complex nested data—recalculating it every time the property is accessed can slow down your application.
Laravel provides a built-in method called shouldCache() directly on the Attribute object:
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
class Post extends Model
{
protected function htmlContent(): Attribute
{
return Attribute::make(
get: fn (mixed $value, array $attributes) => Str::markdown($attributes['content'] ?? '')
)->shouldCache();
}
}
With shouldCache() enabled, Laravel computes the result once when first accessed on the model instance and caches it in memory for the remainder of the request lifecycle.
Comparison: Legacy vs Modern Accessors & Mutators
| Feature | Legacy Approach | Modern Approach |
|---|---|---|
| Method Definition | Two methods (get...Attribute, set...Attribute) |
Single method returning Attribute |
| Method Name | getFirstNameAttribute (verbose) |
firstName (clean camelCase) |
| Closure / Arrow Support | No | Yes (compact PHP 8 arrow functions) |
| Built-in Caching | Manual instance caching required | Native ->shouldCache() method |
Things to Remember and Best Practices
- Return Type Declaration: Always import
Illuminate\Database\Eloquent\Casts\Attributeand declare: Attributeas the return type for better IDE autocompletion and static analysis.
- Method Naming: The method name must be the camelCase representation of your database column. For example, a column named
phone_numbermust have a method namedphoneNumber(): Attribute.
- Accessing Other Columns in Accessors: When accessing other attributes within a virtual accessor, always use the second
$attributesargument of the closure (e.g.,fn ($value, array $attributes) => ...) rather than$this->column_name.
Conclusion:
The modern Attribute::make() syntax in Laravel is a massive upgrade over legacy accessors and mutators. It makes your Eloquent models significantly cleaner, groups related data transformations into a single method, and adds convenient features like built-in caching.
Thank you for reading this article 😊
For any query, do not hesitate to comment 💬
-compressed.jpg)