The Power of Laravel's Observer Pattern featured image

Today I want to talk about hooking into Laravel model events, their benefits and potential drawbacks. This, along with some examples on how and when to use them and avoid them.

Why use Observers?

Keeps the controllers lean

Using observers will allow controllers to do what they’re supposed to do. Receive requests and return responses. By using observers, you are handing more of the logic over to the model layer which is generally seen to be good practice nowadays.

Encourages maintainable code (KISS)

Observers make it easier to follow the KISS (Keep it simple, stupid) design principle. By abstracting some of your logic into observers, you are organising your code base and defining design standards which will be more easily understood by the developers working on the code base in the future.

Encourages reusable code (DRY)

As the first example below will show, observers make for a very DRY (Don’t Repeat Yourself) codebase. By avoiding repeating yourself, you minimise bugs and localise them. Would you rather fix the same bug in many places? Or just in one place? It’s a no-brainer.

Pitfalls

As with any design pattern, the observer pattern can also be an inappropriate choice for abstracting code.

Avoid calling methods which may call other observables as this may lead to an infinite loop. You can mitigate the chances of this happening by being very mindful and minimal about what you are doing in your observables.

When using the saving or saved hooks, you should never call the model’s save method.

If you are using the saved hook and find you need to call the save method, you should probably be using the saving hook instead.

If the logic of what you are trying to achieve insists on calling the model’s save method, you should either rethink your logic or avoid using observers here.

Hooks

  • retrieved         // after a record has been retrieved
  • creating          // before a record has been created
  • created           // after a record has been created
  • updating         // before a record is updated
  • updated          // after a record has been updated
  • saving             // before a record is saved (either created or updated)
  • saved              // after a record has been saved (either created or updated)
  • deleting          // before a record is deleted or soft-deleted
  • deleted           // after a record has been deleted or soft-deleted
  • restoring        // before a soft-deleted record is going to be restored
  • restored         // after a soft-deleted record has been restored

 

A simple example

Post Model

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    protected $table = 'posts';

    protected $fillable = [
        'title',
        'slug'
    ];
}

Posts Controller

<?php

namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Http\Request;

class PostsController extends Controller
{
    public function create(Request $request)
    {
        $title = $request->input('title');

        Post::query()->create([
            'title' => $title,
            'slug' => str_slug($title, '-')
        ]);
    }

    public function edit($id, Request $request)
    {
        $post = Post::query()->findOrFail($id);

        $post->title = $request->input('title');
        // what if we forget to update the slug here?
        // $post->slug = str_slug($title, '-');

        $post->save();
    }
}

In the example above I have shown how repeating our code can lead to introducing bugs into the system. The bigger the application, the easier it is to accidentally fall into this trap.

What we intended to do in the example is update the slug attribute before the record has been created or updated in the database we will focus on the saving hook.

First, we want to create an observable:

php artisan make:observable PostObserver

This creates a file in the app/Observers directory called PostObserver.php and creates a name spaced class called PostObserver.

<?php

namespace App\Observers;

class PostObserver
{
    //
}

Now we want to register this observer in the boot methods of the application’s AppServiceProvider. By telling the Post model to observe the PostObserver class, it will hook into any recognised method.

<?php

namespace App\Providers; use App\Models\Post; 
use App\Observers\PostObserver;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        Post::observe(PostObserver::class);
    }}
}

In the observer, we want to add the saving hook and tell it to create a slug from the posts title before the post is saved.

<?php

namespace App\Observers;

use App\Models\Post;

class PostObserver
{
    public function saving(Post $post)
    {
        $post->slug = str_slug($post->title);
    }
}

It doesn’t matter whether it is being created for the first time, updated for the millionth time, or where we are calling the model’s save method from, it will always update the slug now.

As our new observer is updating the slug attribute we can remove all reference to the slug attribute in our controller.

<?php

namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Http\Request;

class PostsController extends Controller
{
    public function create(Request $request)
    {
        $title = $request->input('title');

        Post::query()->create([
            'title' => $title
        ]);
    }

    public function edit($id, Request $request)
    {
        $post = Post::query()->findOrFail($id);

        $post->title = $request->input('title');
        $post->save();
    }
}

And there it is. We should never have to think about whether or not our slug is being assigned ever again.

A more useful example

What if our post model has associated comments? Do you really want to have to delete those comments in every part of your code base where you delete posts? Of course not.

We can easily use the deleting hook to do this for us.

Model

I have updated the Posts model to create a HasMany relationship to the Comment model.

<?php

namespace App\Models;

use App\Models\Post\Comment;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    protected $table = 'posts';

    protected $fillable = [
        'title',
        'slug'
    ];

    public function comments()
    {
        return $this->hasMany(Comment::class);
    }
}

Observer

<?php

namespace App\Observers;

use App\Models\Post;

class PostObserver
{

    public function deleting(Post $post)
    {
        $post->comments()->delete();
    }
}

In conclusion

Eloquent model observers are a very powerful and useful feature but must be used with care. They can make your codebase more maintainable but you need to be mindful of their limitations.