CRUD with Eloquent — find, findOrFail, create, update, save, delete
Concept
Accessors transform model attributes when you READ them. Mutators transform values when you WRITE them. Since Laravel 9, both are defined using the unified Illuminate\Database\Eloquent\Casts\Attribute class and named with camelCase — no more getFooAttribute / setFooAttribute conventions.
Modern syntax (Laravel 9+):
protected function firstName(): Attribute
{
return Attribute::make(
get: fn($value) => ucfirst($value),
set: fn($value) => strtolower($value),
);
}Use Attribute::get() for read-only accessors, Attribute::set() for write-only mutators.
Legacy syntax (still works):
- Accessor:
public function getFirstNameAttribute($value)— called when accessing$model->first_name. - Mutator:
public function setFirstNameAttribute($value)— called when assigning$model->first_name = 'Alice'.
Computed/virtual attributes: Accessors with no corresponding database column. Must be added to $appends if they should appear in toArray() / JSON serialization.
withCacheable / cached accessors: Accessors run on every access. For expensive computations, cache the result: Attribute::get(fn() => expensiveCalculation())->shouldCache().
Code Example
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Casts\Attribute;
class User extends Model
{
protected $appends = ['full_name', 'gravatar_url'];
// Combined accessor + mutator — first_name column
protected function firstName(): Attribute
{
return Attribute::make(
get: fn(string $value) => ucfirst($value),
set: fn(string $value) => strtolower($value),
);
}
// Password mutator — auto-hash on assignment
protected function password(): Attribute
{
return Attribute::set(fn(string $value) => bcrypt($value));
}
// Virtual accessor — no DB column, added to appends
protected function fullName(): Attribute
{
return Attribute::get(fn() => trim("{$this->first_name} {$this->last_name}"));
}
// Virtual with caching (expensive operation)
protected function gravatarUrl(): Attribute
{
return Attribute::get(fn() =>
'https://gravatar.com/avatar/' . md5(strtolower($this->email))
)->shouldCache();
}
// Accessor transforming serialized data
protected function preferences(): Attribute
{
return Attribute::make(
get: fn(string $value) => json_decode($value, true) ?? [],
set: fn(array $value) => json_encode($value),
);
}
}
// Usage
$user = User::find(1);
$user->password = 'plaintext'; // mutator: auto-bcrypt before saving
$user->first_name = 'ALICE'; // mutator: stored as 'alice'
echo $user->first_name; // accessor: "Alice"
echo $user->full_name; // computed: "Alice Smith"
echo $user->gravatar_url; // computed + cached
$prefs = $user->preferences; // decoded array
$user->preferences = ['theme' => 'dark']; // auto-encoded
// toArray() / JSON includes $appends
$user->toArray(); // includes 'full_name' and 'gravatar_url'
$user->toJson(); // same
json_encode($user); // same