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.
What are events and listeners?
You may have heard of "events" and "listeners". But if you haven't heard of it, here's a brief overview of them:
#Event
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
Listeners are blocks of code you want to run when a specific event occurs.
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: http://www.miracleart.cn/link/d9a8c56824cfbe66f28f85edbbe83e09
What is a model event?
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:
- retrieved - Retrieve from the database.
- creating - Creating a model.
- created - The model has been created.
- updating - Updating the model.
- updated - The model has been updated.
- saving - Creating or updating a model.
- saved - The model has been created or updated.
- deleting - Deleting the model.
- deleted - The model has been deleted.
- trashed - The model has been softly deleted.
- forceDeleting - Forced deletion of the model.
- forceDeleted - The model has been forced to be deleted
- restoring - Recovering the model from soft deletion.
- restored - The model has been recovered from soft delete.
- replicating - Replicating the 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.
Use dispatchesEvents
Listen to model events
One 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.
#Set the model
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:
- Added a
dispatchesEvents
property that maps thedeleted
model event to theAppEventsAuthorDeleted
event class. This means that when the model is deleted, a newAppEventsAuthorDeleted
event will be scheduled. We will create this event class later. - defines a
posts
relationship. - Soft deletion is enabled on the model by using the
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:
- Added a
dispatchesEvents
property that maps thesaving
model event to theAppEventsPostSaving
event class. This means that when the model is created or updated, a newAppEventsPostSaving
event will be scheduled. We will create this event class later. - defines a
author
relationship. - Soft deletion is enabled on the model by using the
IlluminateDatabaseEloquentSoftDeletes
feature.
Our model is now ready, so let's create our AppEventsAuthorDeleted
and AppEventsPostSaving
event classes.
#Create event class
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.
#Create listener
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
Another way you can use is to define the listener as a closure on the model itself.
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
Using observer to listen for model events
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
No matter which model event method you use, you may want to write some tests to make sure your logic works as expected.
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:
- The first test ensures that the reading time of the article is calculated when creating it.
- The second test ensures that the reading time of the article is calculated when the article is updated.
Precautions when using model events
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.
Alternative methods to consider
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:
- Calculate the reading time of the article.
- Send API calls to X/Twitter to share the article.
- Send notifications to every subscriber on the platform.
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:
- If the operations in the listener make only minor changes to the model, consider using model events. Example: Generate slugs, calculate reading time, etc.
- If the operation will affect another model (whether it is automatically created, updated or deleted), it is more clear and do not use model events.
- If the operation will work with external processes (API calls, file processing, trigger notifications, queued jobs), it is more clear that you do not use model events.
Pros and cons of using model events
To quickly summarize what we have introduced in this article, here are some of the advantages and disadvantages of using model events:
#Pros
- Encourage you to decouple the code.
- allows you to automatically trigger actions, no matter where the model is created/updated/delete. For example, if the model was created in Laravel Nova, you can trigger business logic.
- You don't need to remember to schedule events every time you create/update/delete a model.
#Disadvantages
- May cause unexpected side effects. You may want to create/update/delete the model without triggering some listeners, but this can lead to unexpected behavior. This is especially problematic when writing tests.
- Business logic may be hidden in unexpected locations that are difficult to track. This will make the code flow more difficult to understand.
Conclusion
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!

Hot AI Tools

Undress AI Tool
Undress images for free

Undresser.AI Undress
AI-powered app for creating realistic nude photos

AI Clothes Remover
Online AI tool for removing clothes from photos.

Clothoff.io
AI clothes remover

Video Face Swap
Swap faces in any video effortlessly with our completely free AI face swap tool!

Hot Article

Hot Tools

Notepad++7.3.1
Easy-to-use and free code editor

SublimeText3 Chinese version
Chinese version, very easy to use

Zend Studio 13.0.1
Powerful PHP integrated development environment

Dreamweaver CS6
Visual web development tools

SublimeText3 Mac version
God-level code editing software (SublimeText3)

Hot Topics

ToversionaPHP-basedAPIeffectively,useURL-basedversioningforclarityandeaseofrouting,separateversionedcodetoavoidconflicts,deprecateoldversionswithclearcommunication,andconsidercustomheadersonlywhennecessary.StartbyplacingtheversionintheURL(e.g.,/api/v

TosecurelyhandleauthenticationandauthorizationinPHP,followthesesteps:1.Alwayshashpasswordswithpassword_hash()andverifyusingpassword_verify(),usepreparedstatementstopreventSQLinjection,andstoreuserdatain$_SESSIONafterlogin.2.Implementrole-basedaccessc

PHPdoesnothaveabuilt-inWeakMapbutoffersWeakReferenceforsimilarfunctionality.1.WeakReferenceallowsholdingreferenceswithoutpreventinggarbagecollection.2.Itisusefulforcaching,eventlisteners,andmetadatawithoutaffectingobjectlifecycles.3.YoucansimulateaWe

Proceduralandobject-orientedprogramming(OOP)inPHPdiffersignificantlyinstructure,reusability,anddatahandling.1.Proceduralprogrammingusesfunctionsorganizedsequentially,suitableforsmallscripts.2.OOPorganizescodeintoclassesandobjects,modelingreal-worlden

To safely handle file uploads in PHP, the core is to verify file types, rename files, and restrict permissions. 1. Use finfo_file() to check the real MIME type, and only specific types such as image/jpeg are allowed; 2. Use uniqid() to generate random file names and store them in non-Web root directory; 3. Limit file size through php.ini and HTML forms, and set directory permissions to 0755; 4. Use ClamAV to scan malware to enhance security. These steps effectively prevent security vulnerabilities and ensure that the file upload process is safe and reliable.

In PHP, the main difference between == and == is the strictness of type checking. ==Type conversion will be performed before comparison, for example, 5=="5" returns true, and ===Request that the value and type are the same before true will be returned, for example, 5==="5" returns false. In usage scenarios, === is more secure and should be used first, and == is only used when type conversion is required.

Yes, PHP can interact with NoSQL databases like MongoDB and Redis through specific extensions or libraries. First, use the MongoDBPHP driver (installed through PECL or Composer) to create client instances and operate databases and collections, supporting insertion, query, aggregation and other operations; second, use the Predis library or phpredis extension to connect to Redis, perform key-value settings and acquisitions, and recommend phpredis for high-performance scenarios, while Predis is convenient for rapid deployment; both are suitable for production environments and are well-documented.

The methods of using basic mathematical operations in PHP are as follows: 1. Addition signs support integers and floating-point numbers, and can also be used for variables. String numbers will be automatically converted but not recommended to dependencies; 2. Subtraction signs use - signs, variables are the same, and type conversion is also applicable; 3. Multiplication signs use * signs, which are suitable for numbers and similar strings; 4. Division uses / signs, which need to avoid dividing by zero, and note that the result may be floating-point numbers; 5. Taking the modulus signs can be used to judge odd and even numbers, and when processing negative numbers, the remainder signs are consistent with the dividend. The key to using these operators correctly is to ensure that the data types are clear and the boundary situation is handled well.
