Home >Backend Development >PHP Tutorial >A guide to Laravel's model events

A guide to Laravel's model events

James Robert Taylor
James Robert TaylorOriginal
2025-03-06 02:25:14852browse

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.

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.

When dispatched, the event will trigger any listener that is listening on it.

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

#Listener

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

In Laravel, listeners are usually (but not always) classes found in the

directory. app/Listeners

Example of listener sending a welcome email to a user when a user registers might look like this:

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

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 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.
  • 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 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.
  • 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

Therefore, whenever the

model is softly deleted, all authors' articles will also be softly deleted. AppModelsAuthor

By the way, it is worth noting that you may want to use a more powerful, reusable solution to achieve this. But for the purposes of this article, we keep it simple.

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

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. However, if the code in these closures becomes more complex, it may be worth extracting the logic into a separate listener class.

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

Suppose, for example, we also want to define listeners for

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!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn