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 Accessor. In this case, the Accessor creates an excerpt from a DB field called 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.

avatar

There's a mistake in your code about Mutators. It should be like this:

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)),
    );
}

Comma at the end of set lines and semicolon at the end of return lines

👍 2
avatar

Thank you, updated!

avatar

Let's say for example that in the model observer of a post I want to delete all related comments (and responses) when deleting a post . Let's imagine we have a bunch of comments and reponses for the post that may make the request take too long to execute; How can we optimize it ? Should we queue the code inside the deleted method so that it runs in background ?

class PostObserver{
	   public function deleted(Post $post)
    {
        $post->comments()->responses()->delete();
				$post->comments()->delete();
    }
}
avatar

If it takes a long time to actually delete the comments - I would send the deletion to the queue worker instead of an observer. You can trigger the job via an observer, but not the whole deletion.

This will speed things up!

avatar
Сергій Каліш

I would use a foreign key with cascading deletion of records when developing a database, comments would be deleted along with the post

avatar

Hi, I noticed a small issue that I thought you might want to correct.

In the section where you mention:

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

It seems that the term "Mutators" should be "Accessors," as Accessors are used to manipulate and retrieve data from a model attribute.

Thank you for your great work.

avatar

Hi, thanks for a great note! Updating.

ps. The naming and usage is confusing - since if it does not have set: fn()... - that's an acessor, but as soon as you use the set - it's both at the same time...

avatar

Thank you. Have a great day.