Courses

How to Structure Laravel 11 Projects

Before Saving: Mutator or Observer?

Let's say you want to transform some request data before saving it into the database.

In this lesson, I will show two examples of such transformation:

  • Formatting the date
  • Encrypting the password

Currently, we perform those transformations in the Controller:

public function store(Request $request)
{
// ...
 
$userData['start_at'] = Carbon::createFromFormat('m/d/Y', $request->start_at)->format('Y-m-d');
$userData['password'] = bcrypt($request->password);
 
$user = User::create($userData);
 
// ...

Instead, we may use Eloquent features for that: Mutators or Observers. Or, we can even store that logic in the Model's booted() method. Again, it's your personal preference which of those to use in your projects.


"Old School": Model booted()

This is a way I saw in older Laravel projects. The logic is that you give the logic of the Model to the Model itself, without any external classes.

Here's the Model code, then:

app/Models/User.php:

class User extends Authenticatable
{
// ...
 
public static function booted()
{
static::creating(function (self $user) {
$user->start_at = Carbon::createFromFormat('m/d/Y', $user->start_at)->format('Y-m-d');
$user->password = bcrypt($user->password);
});
}
}

But, as Laravel matured, developers got used to using the code structures explicitly dedicated to such operations.


Mutators

In Eloquent models, you can define Mutators. Here's an example of the Model code:

use Illuminate\Database\Eloquent\Casts\Attribute;
 
// ...
 
protected function startAt(): Attribute
{
return Attribute::make(
set: fn ($value) => Carbon::createFromFormat('m/d/Y', $value)->format('Y-m-d');
)
}
 
protected function password(): Attribute
{
return Attribute::make(
set: fn ($value) => bcrypt($value));
)
}

Observers

You can create the Observer by running command:

php artisan make:observer UserObserver --model=User

If you open our created app/Observers/UserObserver.php, you will see generated methods about events that already happened, like created() or updated(). But also, you can define creating(), which will be called before creating a record.

app/Observers/UserObserver.php:

class UserObserver
{
public function creating(User $user)
{
$user->start_at = Carbon::createFromFormat('m/d/Y', $user->start_at)->format('Y-m-d');
$user->password = bcrypt($user->password);
}
}

Notice that it's the same code as we saw in the Model booted() method above, just separated into an Observer class to shorten the Model.

In this case, you need to register the Observer on the Model with PHP attribute ObservedBy:

use App\Observers\UserObserver;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
 
#[ObservedBy([UserObserver::class])]
class User extends Authenticatable
{
// ...
}

Notice: This new PHP Attribute syntax appeared in Laravel 10.44. In the past, you needed to use a Service Provider to register Observers.

Observers are great, but specifically, this use case isn't mentioned in Laravel documentation. So it's probably not officially recommended, and Observers are more widely used for operations after saving the data to the DB.

So, if you want to shorten the controller and move that logic somewhere, I would probably recommend using Mutators. This is what I will personally use for our example.


Shorter Controller

So now, as we used Mutators, we don't need those two lines in the Controller, and we don't need the $userData variable. We can pass validated data directly into the User::create() method.

public function store(StoreUserRequest $request)
{
$userData['start_at'] = Carbon::createFromFormat('m/d/Y', $request->start_at)->format('Y-m-d');
$userData['password'] = bcrypt($request->password);
 
$user = User::create($request->validated());
$user->roles()->sync($request->input('roles', []));
 
// ...
}

Now, let's look at some examples from open-source projects.


Open-Source Examples

Example Project 1. ploi/roadmap

Observers are used not only for transforming the data before saving but also for performing operations after DB changes.

In this project, the Observer is used to delete everything that is connected with the user that is being deleted, like mentions, votes, etc.

app/Observers/UserObserver.php:

use App\Models\User;
use App\Jobs\Items\RecalculateItemsVotes;
 
class UserObserver
{
public function deleting(User $user)
{
dispatch(new RecalculateItemsVotes($user->items()->pluck('id')));
 
$user->mentions()->delete();
$user->votes()->delete();
$user->comments()->delete();
$user->userSocials()->delete();
$user->items()->update(['user_id' => null]);
}
}

Next: the same project, but a different example. Changing the data before it's saved directly in the Model, in its booted() method.

app/Models/User.php:

class User extends Authenticatable implements FilamentUser, HasAvatar, MustVerifyEmail
{
// ...
 
public static function booted()
{
static::creating(function (self $user) {
$user->username = Str::slug($user->name);
$user->notification_settings = [
'receive_mention_notifications',
'receive_comment_reply_notifications',
];
$user->per_page_setting = ['5','15','25'];
});
 
static::updating(function (self $user) {
$user->username = Str::lower($user->username);
});
}
}

It makes sense because Model is responsible for its own database operations: that's what Model is for, right? But again, it's just one of the options. The choice is yours.


Next example: the plot/roadmap project also uses Mutators. In this case, Mutator creates an excerpt from a DB field content.

app/Models/Item.php:

use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Casts\Attribute;
 
class Item extends Model
{
// ...
 
protected function excerpt(): Attribute
{
return Attribute::make(
get: function ($value) {
return Str::limit(strip_tags(str($this->attributes['content'])->markdown()->trim()), 150);
},
);
}
 
// ...
}

The excerpt can be called as a regular field on an Item Model like $item->excerpt.


Example Project 2. PHPJunior/mtube

The second example for the Observer is from the PHPJunior/mtube project. When the video is created, the Observer is used to convert the video. When the video is deleted, the Observer deletes files from the storage.

app/Observers/VideoObserver.php:

class VideoObserver
{
/**
* @param Video $video
*/
public function created(Video $video)
{
if ($video->type == 'upload') dispatch(new StartConvert($video->id));
}
 
/**
* @param Video $video
*/
public function deleted(Video $video)
{
Storage::disk('public')->delete($video->path);
Storage::disk($video->disk)->deleteDirectory('converted/' . $video->media_id);
}
}

So yeah, if you want to perform data transformations before saving, you may use Mutators. If you want to do something extra after saving, you may use Observers. Again, I emphasize the word "may." I hope you get that idea throughout this whole course.

No comments or questions yet...