Cleaner Code with Eloquent Events

2017/10/17

Tags: laravel php

As part of a current side-project, I needed to “protect” certain model attributes, to ensure that, once saved, they do not change.

This is a pretty common requirement, and in the past I’ve seen it implemented as follows 1.

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Project extends Model
{
    public function save(array $options = [])
    {
        if ($this->exists) {
            $this->uuid = $this->getOriginal('uuid');
        }
        
        parent::save();
    }
}

This works, but thanks to Eloquent’s lifecycle events, there is a much better option.

Eloquent lifecycle events

Eloquent makes it easy to respond to certain key points in the lifecycle of a model instance, by exposing events such as creating, updating, and deleted.

Protecting an attribute

Rather than overriding the built-in save method, and checking if the model already exists, you can simply hook into the updating event:

public static function boot()
{
    parent::boot();
    
    static::updating(function ($instance) {
        $instance->uuid = $instance->getOriginal('uuid');
    });
}

Protecting all attributes

It’s even easier if you want to make your model instance immutable; just return false from the event listener.

public static function boot()
{
    parent::boot();
    
    static::updating(function ($instance) {
        return false;
    });
}

Keeping things clean

The “callback” approach works fine if you’re doing something very simple, but generally speaking, I’m not a fan.

Instead, I prefer to move my model event listener code to a separate “observer” class 2.

<?php

namespace App\Observers;

use App\Project;

class ProjectObserver
{
    public function updating(Project $instance)
    {
        $instance->uuid = $instance->getOriginal('uuid');
    }
}

And then register that observer in the model’s boot method 3.

public static function boot()
{
    parent::boot();
    static::observe(new \App\Observers\ProjectObserver);
}

Observing multiple events

One of the nice things about observers, aside from the fact they just look neater than callbacks, is that they can respond to as many model events as you need.

Just add a public method named after the event, and you’re good to go:

<?php

namespace App\Observers;

use App\Project;
use Ramsey\Uuid\Uuid;

class ProjectObserver
{
    public function creating(Project $instance)
    {
        $instance->uuid = Uuid::uuid4()->toString();
    }
    
    public function updating(Project $instance)
    {
        $instance->uuid = $instance->getOriginal('uuid');
    }
}

  1. I may even have been guilty of this myself.
  2. There are a couple of reasons why I prefer the “observer” approach to using $dispatchesEvents, as described in the Laravel documentation. I’ll get to at least one of those in a future article.
  3. The example in the Laravel documentation uses the AppServiceProvider::boot method, but I much prefer to register my observers in the model. That way, it’s at least obvious that something is happening in response to one or more lifecycle events.