Home >Backend Development >PHP Tutorial >A guide to Laravel's model events
Laravel's model events are a very convenient feature that helps you automatically run logic when performing certain operations on your Eloquent model. However, if used improperly, it can sometimes lead to strange side effects.
This article will explore what model events are and how to use them in Laravel applications. We will also explore how to test model events and some issues to be aware of when using them. Finally, we'll cover some alternatives to model events that you can consider using.
You may have heard of "events" and "listeners". But if you haven't heard of it, here's a brief overview of them:
These are what happens in the apps you want to act on—for example, users sign up on your website, users log in, etc.
Usually, in Laravel, events are PHP classes. In addition to events provided by frameworks or third-party packages, they are usually saved in the app/Events
directory.
The following is an example of a simple event class that you may want to schedule when a user registers to your website:
declare(strict_types=1); namespace App\Events; use App\Models\User; use Illuminate\Broadcasting\InteractsWithSockets; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; final class UserRegistered { use Dispatchable; use InteractsWithSockets; use SerializesModels; public function __construct(public User $user) { // } }
In the basic example above, we have a AppEventsUserRegistered
event class that accepts a User
model instance in its constructor. This event class is a simple container for saving registered user instances.
The following is a simple example of how to schedule the event when a user registers:
use App\Events\UserRegistered; use App\Models\User; $user = User::create([ 'name' => 'Eric Barnes', 'email' => 'eric@example.com', ]); UserRegistered::dispatch($user);In the example above, we are creating a new user and then scheduling the
event using the user instance. Assuming the listener is registered correctly, this will trigger any listener that is listening on the AppEventsUserRegistered
event. AppEventsUserRegistered
For example, stick with our user registration example, you may want to send a welcome email to the user when the user registers. You can create a listener to the
event and send a welcome email. AppEventsUserRegistered
directory. app/Listeners
declare(strict_types=1); namespace App\Listeners; use App\Events\UserRegistered; use App\Notifications\WelcomeNotification; use Illuminate\Support\Facades\Mail; final readonly class SendWelcomeEmail { public function handle(UserRegistered $event): void { $event->user->notify(new WelcomeNotification()); } }As we saw in the code example above, the
listener class has a AppListenersSendWelcomeEmail
method that accepts a handle
event instance. This method is responsible for sending a welcome email to the user. AppEventsUserRegistered
For more in-depth instructions on events and listeners, you may want to check out the official documentation: https://www.php.cn/link/d9a8c56824cfbe66f28f85edbbe83e09
In your Laravel application, you usually need to manually schedule events when certain actions occur. As we saw in the above example, we can schedule events using the following code:
declare(strict_types=1); namespace App\Events; use App\Models\User; use Illuminate\Broadcasting\InteractsWithSockets; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; final class UserRegistered { use Dispatchable; use InteractsWithSockets; use SerializesModels; public function __construct(public User $user) { // } }
However, when using the Eloquent model in Laravel, some events are automatically scheduled for us, so we don't need to schedule them manually. If we want to perform operations when events occur, we just need to define listeners for them.
The following list shows events and triggers automatically scheduled by the Eloquent model:
In the list above, you may notice some event names similar; for example, creating
and created
. Events ending with ing
are executed before the operation occurs, and the changes are persisted to the database. Events ending with ed
are executed after the operation occurs, and the changes are persisted to the database.
Let's see how to use these model events in a Laravel application.
dispatchesEvents
Listen to model eventsOne way to listen for model events is to define a dispatchesEvents
property on your model.
This property allows you to map Eloquent model events to the event class that should be scheduled when the event occurs. This means you can define the listener just like you would handle any other event.
To provide more context, let's look at an example.
Suppose we are building a blog application with two models: AppModelsPost
and AppModelsAuthor
. We will say that both models support soft deletion. When we save a new AppModelsPost
, we want to calculate the reading time of the article based on the length of the content. When we softly delete the author, we want the author to softly delete all articles.
We may have a AppModelsAuthor
model as shown below:
use App\Events\UserRegistered; use App\Models\User; $user = User::create([ 'name' => 'Eric Barnes', 'email' => 'eric@example.com', ]); UserRegistered::dispatch($user);
In the above model, we have:
dispatchesEvents
property that maps the deleted
model event to the AppEventsAuthorDeleted
event class. This means that when the model is deleted, a new AppEventsAuthorDeleted
event will be scheduled. We will create this event class later. posts
relationship. IlluminateDatabaseEloquentSoftDeletes
feature. Let's create our AppModelsPost
model:
declare(strict_types=1); namespace App\Events; use App\Models\User; use Illuminate\Broadcasting\InteractsWithSockets; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; final class UserRegistered { use Dispatchable; use InteractsWithSockets; use SerializesModels; public function __construct(public User $user) { // } }
In the above AppModelsPost
model, we have:
dispatchesEvents
property that maps the saving
model event to the AppEventsPostSaving
event class. This means that when the model is created or updated, a new AppEventsPostSaving
event will be scheduled. We will create this event class later. author
relationship. IlluminateDatabaseEloquentSoftDeletes
feature. Our model is now ready, so let's create our AppEventsAuthorDeleted
and AppEventsPostSaving
event classes.
We will create a AppEventsPostSaving
event class that will be scheduled when saving a new article:
use App\Events\UserRegistered; use App\Models\User; $user = User::create([ 'name' => 'Eric Barnes', 'email' => 'eric@example.com', ]); UserRegistered::dispatch($user);
In the above code, we can see the AppEventsPostSaving
event class, which accepts a AppModelsPost
model instance in its constructor. This event class is a simple container for saving the article instance being saved.
Similarly, we can create a AppEventsAuthorDeleted
event class that will be scheduled when deleting the author:
declare(strict_types=1); namespace App\Listeners; use App\Events\UserRegistered; use App\Notifications\WelcomeNotification; use Illuminate\Support\Facades\Mail; final readonly class SendWelcomeEmail { public function handle(UserRegistered $event): void { $event->user->notify(new WelcomeNotification()); } }
In the above AppEventsAuthorDeleted
class, we can see that the constructor accepts a AppModelsAuthor
model instance.
Now we can continue to create listeners.
Let's first create a listener that can be used to calculate the estimation of reading time of the article.
We will create a new AppListenersCalculateReadTime
listener class:
UserRegistered::dispatch($user);
As we see in the code above, we only have one handle
method. This is a method that will be automatically called when scheduling the AppEventsPostSaving
event. It accepts an instance of the AppEventsPostSaving
event class that contains the article being saved.
In the handle
method, we use a simple formula to calculate the reading time of the article. In this example, we assume that the average reading speed is 265 words per minute. We are calculating the reading time in seconds and then setting the read_time_in_seconds
attribute on the article model.
Since this listener will be called when the saving
model event is triggered, this means that the read_time_in_seconds
attribute is calculated every time the article is persisted to the database before creating or updating it.
We can also create a listener that softly deletes all related articles when softly deleting the author.
We can create a new AppListenersSoftDeleteAuthorRelationships
listener class:
declare(strict_types=1); namespace App\Events; use App\Models\User; use Illuminate\Broadcasting\InteractsWithSockets; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; final class UserRegistered { use Dispatchable; use InteractsWithSockets; use SerializesModels; public function __construct(public User $user) { // } }
In the listener above, the handle
method accepts an instance of the AppEventsAuthorDeleted
event class. This event class contains the author being deleted. Then, we use the posts
relationship to delete the author's article. delete
model is softly deleted, all authors' articles will also be softly deleted. AppModelsAuthor
Use closure to listen for model events
Let's take a look at the example of soft deleting articles when we softly deleted the author. We can update our
model to contain a closure that listens for AppModelsAuthor
model events: deleted
use App\Events\UserRegistered; use App\Models\User; $user = User::create([ 'name' => 'Eric Barnes', 'email' => 'eric@example.com', ]); UserRegistered::dispatch($user);We can see in the above model that we are defining the listener in the
method of the model. We want to listen for the booted
model event, so we used deleted
. Similarly, if we want to create a listener for the self::deleted
model event, we can use created
and so on. The self::created
method accepts a closure that receives the self::deleted
being deleted. This closure will be executed when the model is deleted, so all author articles will be deleted. AppModelsAuthor
A handy trick is that you can also use the
function to make closures queued. This means that the listener's code will be pushed into the queue to run in the background, rather than in the same request lifecycle. We can update the listener to queueable as follows: IlluminateEventsqueueable
declare(strict_types=1); namespace App\Listeners; use App\Events\UserRegistered; use App\Notifications\WelcomeNotification; use Illuminate\Support\Facades\Mail; final readonly class SendWelcomeEmail { public function handle(UserRegistered $event): void { $event->user->notify(new WelcomeNotification()); } }As we saw in the example above, we wrap the closure in the
function. IlluminateEventsqueueable
Another way you can take to listen for model events is to use model observers. Model observers allow you to define all listeners for the model in one class.
Usually, they are classes that exist in the app/Observers
directory, and they have methods corresponding to the model events you want to listen to. For example, if you want to listen for a deleted
model event, you will define a deleted
method in the observer class. If you want to listen for a created
model event, you will define a created
method in the observer class, and so on.
Let's see how to create a model observer for our AppModelsAuthor
model listening for deleted
model events:
declare(strict_types=1); namespace App\Events; use App\Models\User; use Illuminate\Broadcasting\InteractsWithSockets; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; final class UserRegistered { use Dispatchable; use InteractsWithSockets; use SerializesModels; public function __construct(public User $user) { // } }
As we see in the code above, we create an observer with the deleted
method. This method accepts instances of the AppModelsAuthor
model being deleted. Then, we use the posts
relationship to delete the author's article. delete
and created
model events. We can update our observers like this: updated
use App\Events\UserRegistered; use App\Models\User; $user = User::create([ 'name' => 'Eric Barnes', 'email' => 'eric@example.com', ]); UserRegistered::dispatch($user);In order to run the
method, we need to instruct Laravel to use it. To do this, we can use the AppObserversAuthorObserver
attribute. This allows us to associate observers with the model, similar to how we register global query scopes using the #[IlluminateDatabaseEloquentAttributesObservedBy]
attribute (as shown in Understanding How to Master Query Scope in Laravel). We can update our #[ScopedBy]
model like this to use the observer: AppModelsAuthor
declare(strict_types=1); namespace App\Listeners; use App\Events\UserRegistered; use App\Notifications\WelcomeNotification; use Illuminate\Support\Facades\Mail; final readonly class SendWelcomeEmail { public function handle(UserRegistered $event): void { $event->user->notify(new WelcomeNotification()); } }I really like this way of defining listener logic, because it can immediately see if it registers an observer when opening the model class. So while the logic is still "hidden" in a separate file, we can know that we have registered listeners for at least one event of the model.
Test your model event
Let's see how to test the model events we created in the example above.
We first write a test to ensure that the author's article is softly deleted when the author is softly deleted. The test might look like this:
UserRegistered::dispatch($user);In the above test, we are creating a new author and article for that author. We then softly deleted the author and asserted that both the author and the article were softly deleted.
This is a very simple but effective test that we can use to make sure our logic works as expected. The advantage of this test is that it should work with each of the methods we discuss in this article. So if you switch between any of the methods we discussed in this article, your test should still pass.
Similarly, we can write some tests to ensure that the reading time of the article is calculated when creating or updating it. The test might look like this:
declare(strict_types=1); namespace App\Events; use App\Models\User; use Illuminate\Broadcasting\InteractsWithSockets; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; final class UserRegistered { use Dispatchable; use InteractsWithSockets; use SerializesModels; public function __construct(public User $user) { // } }
We have two tests on it:
Although model events are very convenient, there are some issues to be aware of when using them.
Model events are scheduled from the Eloquent model only. This means that if you use IlluminateSupportFacadesDB
facade to interact with the underlying data of the model in the database, its events will not be scheduled.
For example, let's look at a simple example, we use IlluminateSupportFacadesDB
facade to delete the author:
use App\Events\UserRegistered; use App\Models\User; $user = User::create([ 'name' => 'Eric Barnes', 'email' => 'eric@example.com', ]); UserRegistered::dispatch($user);
Running the above code will delete the author from the database as expected. However, the deleting
and deleted
model events are not scheduled. So if you define any listeners for these model events when deleting the author, they won't be run.
Similarly, if you use Eloquent to batch update or delete a model, the saved
, updated
, deleting
, and deleted
model events are not scheduled for the affected models. This is because events are scheduled from the model itself. However, when batch updates and deletions are updated, the model is not actually retrieved from the database, so events are not scheduled.
For example, suppose we use the following code to delete the author:
declare(strict_types=1); namespace App\Listeners; use App\Events\UserRegistered; use App\Notifications\WelcomeNotification; use Illuminate\Support\Facades\Mail; final readonly class SendWelcomeEmail { public function handle(UserRegistered $event): void { $event->user->notify(new WelcomeNotification()); } }
Since the delete
method is called directly on the query builder, the deleting
and deleted
model events are not scheduled for that author.
I like to use model events in my projects. They serve as a good way to decouple my code and also allow me to automatically run the logic when I don't have much control over the code that affects the model. For example, if I delete the author in Laravel Nova, I can still run some logic when deleting the author.
However, it is important to know when to consider using different methods.
To explain this, let's look at a basic example where we might want to avoid using model events. Extend our previous simple blog application example, assuming we want to run the following when creating a new post:
So we might create three separate listeners (one for each task) that run every time a new AppModelsPost
instance is created.
But now let's review one of our previous tests:
declare(strict_types=1); namespace App\Events; use App\Models\User; use Illuminate\Broadcasting\InteractsWithSockets; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; final class UserRegistered { use Dispatchable; use InteractsWithSockets; use SerializesModels; public function __construct(public User $user) { // } }
If we run the test above, it will also trigger these three operations when the AppModelsPost
model is created through its factory. Of course, calculating reading time is a secondary task, so it doesn't matter much. But we don't want to try to make API calls or send notifications during testing. These are unexpected side effects. If the developer writing the test is not aware of these side effects, it may be difficult to track down why these operations occur.
We also want to avoid writing any test-specific logic in the listener, which prevents these operations from running during tests. This will make the application code more complex and harder to maintain.
This is one of the cases where you might want to consider a more explicit approach rather than relying on automatic model events.
One way can be to extract your AppModelsPost
creation code into a service or action class. For example, a simple service class might look like this:
use App\Events\UserRegistered; use App\Models\User; $user = User::create([ 'name' => 'Eric Barnes', 'email' => 'eric@example.com', ]); UserRegistered::dispatch($user);
In the above class, we are manually calling code that calculates reading time, sends notifications, and posts to Twitter. This means we have better control over when these operations are run. We can also easily mock these methods in tests to prevent them from running. We can still queue these operations if needed (in this case we will most likely do so).
Therefore, we can delete the model events and listeners for these operations. This means we can use this new AppServicesPostService
class in our application code and safely use the model factory in our test code.
The added benefit of doing this is that it also makes the code easier to understand. As I briefly mentioned, a common criticism of using events and listeners is that it may hide business logic in unexpected places. So if new developers join the team, if they are triggered by model events, they may not know where or why some operations occur.
However, if you still want to use events and listeners for such logic, you might consider using a more explicit approach. For example, you can schedule an event from the service class to trigger the listener. This way, you can still use the decoupling advantages of events and listeners, but you have better control over when events are scheduled.
For example, we can update the AppServicesPostService
method above in our createPost
example to schedule events:
declare(strict_types=1); namespace App\Listeners; use App\Events\UserRegistered; use App\Notifications\WelcomeNotification; use Illuminate\Support\Facades\Mail; final readonly class SendWelcomeEmail { public function handle(UserRegistered $event): void { $event->user->notify(new WelcomeNotification()); } }
By using the above method, we can still have a separate listener to make API requests and send notifications to Twitter. But we have better control over when these operations are run, so they are not run when testing using the model factory.
There is no golden rule when deciding to use any of these methods. It all depends on you, your team, and the features you are building. However, I tend to follow the following rules of thumb:
To quickly summarize what we have introduced in this article, here are some of the advantages and disadvantages of using model events:
Hope this article provides an overview of what model events are and the various ways to use them. It should also show you how to test model event codes and some issues to be aware of when using them.
You should now have enough confidence to use model events in your Laravel application.
The above is the detailed content of A guide to Laravel's model events. For more information, please follow other related articles on the PHP Chinese website!