Skip to main content

Laravel model event handling alternatives

Back-end Development

Introduction

As explained in the Laravel documentation, there are many fairly granular events dispatched over the model lifecycle. Most of them come in "before" and "after" variations. For example, the creating event will fire before the model is persisted to storage (usually the database), and the created event happens after it is persisted. There are countless reasons one might care about these events, such as creating some kind of notification when a certain type of model is created.

In my case, I have a model attribute that may be directly supplied by a user but will otherwise be calculated from related data.  Adding a handler for the saving event will ensure that this attribute is always appropriately set before the model is written to the database. I will briefly review the options the documentation describes, explain why none of them felt appropriate for my needs, and show the options not directly explained in the documentation on model events. 

The needs for your particular case will of course vary greatly, but for me, there are two key aspects of what I needed to accomplish:

  1. I knew I was only going to care about a single-model event.
  2. The entirety of my handler would only be a few lines of code.

Custom Event Classes and Observers

The first event handling option described in the documentation is to create your own custom events and map them to the model events via the model's $dispatchesEvents property. In addition to creating the custom events and mapping them, you must write a listener for each event that handles it. This is very powerful and granular when you need it. I'd even say that for applications with complex event functionality, it's arguably the "right" way to do it too.

The documentation also describes a somewhat simpler approach of creating an observer for the model, which is a class specifically for handling the model's events. This is probably a good solution if you need to handle several events, but each event's handling code is not complex enough to warrant the custom event option. While simpler than the custom events, this approach still requires that the observer is registered as the handler for the model in the application's App\Provider\EventServiceProvider class.

Because my needs were so simple, I felt it was excessive to create a separate class for a few lines of code or have to register it somewhere else, so I looked for an even simpler alternative.

 

Closures Option

At the simple end of the complexity spectrum, the Laravel documentation explains the closures option for handling a model's events. In the model's booted method, the class's static event method is called with the closure that handles it.

protected static function booted(): void
{
    static::created(function (User $user) {
        // ...
    });
}

I ended up using a variation of this approach for two reasons:

  1. This is my personal opinion, but there's something that doesn't feel right about my event handling code existing within the boot method.
  2. I might still want to separate the event handling code into a separate class from the model itself.

I noticed in the more general Laravel documentation on events that it shows a different syntax for registering event handling. A little deduction and experimentation led me to this code below, with the registration in the booted function referring to a static method in the model class.

protected static function booted()
{
    parent::booted();
    static::saving([static::class, 'savingEventHandler']);
}

...

public static function savingEventHandler(ProjectItem $projectItem) {
    if (!$projectItem->name_override){
        $projectItem->name = $projectItem->items->first()->name;
    }
}

While I didn't feel it was necessary for my needs, I also confirmed that you could register a handler in another class, as shown below. In this way, you could still satisfy the separation of concerns principle of event handling in its own class, for cases where you have more complex handling of multiple events, without having to go as far as the observer or custom events approaches.

protected static function booted()
{
    parent::booted();
    static::saving([ProjectItemEventsHandler::class, 'saving']);
}

I should perhaps discuss two things that readers may question. First, it is true that this approach is not explicitly documented in the section dealing with model events. I feel this is a very low risk since it is documented elsewhere, and the event handler registration is actually a more general Laravel principle than in the specific case of the model events. What's more, I have a phpunit test confirming the behavior, so I'll know if it stops working.

Secondly, observant readers may feel the handler code is flawed in that it could throw exceptions if, for example, the items attribute is empty. This is actually not a concern because there is validation that has already confirmed the state of the model by the time this code runs.

Conclusion

Choosing between these options for model event handling is not only a technical decision but also largely one of personal opinion or preference. I feel that the approach I used will be the best balance of simplicity and good code design as a rule. Even for cases where I am handling multiple events, with more complexity in the handling, I would likely still use this technique with the separate handler class.