Define, fire and listen for custom Laravel model events within a trait

It took me some time to figure out the right way to define, fire and listen for custom events for a Laravel model using a trait, so I’m writing down how I did it in case it’s helpful to others.

Let’s say you have a model Post that you set up as having a “draft” status by default, but eventually will have a status of “publish”. Let’s also say you want to make the act of publishing a post a custom model event that can be listened for in addition to the standard model events like “created” or “updated”. And let’s say you want to do all of this using a trait so that you can apply the same logic to another model in the future, such as comments on the post, without repeating yourself.

Here’s what my Post model might look like:

<?php

namespace App\Models;

class Post extends Model
{
    //
}

Let’s create a Publishable trait that can be applied to this model:

<?php

namespace App\Models\Traits;

trait Publishable
{
    // Add to the list of observable events on any publishable model
    public function initializePublishable()
    {
        $this->addObservableEvents([
            'publishing',
            'published',
        ]);
    }

    // Create a publish method that we'll use to transition the 
    // status of any publishable model, and fire off the before/after events
    public function publish()
    {
        if (false === $this->fireModelEvent('publishing')) {
            return false;
        }
        $this->forceFill(['status' => 'publish'])->save();
        $this->fireModelEvent('published');
    }

    // Register the existence of the publishing model event
    public static function publishing($callback)
    {
        static::registerModelEvent('publishing', $callback);
    }

    // Register the existence of the published model event
    public static function published($callback)
    {
        static::registerModelEvent('published', $callback);
    }
}

This new trait can now be applied to the Post model:

<?php

namespace App\Models;

class Post extends Model
{
    use Publishable;

    // ...
}

Now, when I want to publish a post, I can call $post->publish(); and Laravel will not only update the value of the publish field in the database, but also fire off the publishing and published model events.

I can listen for these events just like any other model event. I can do this in an observer for the Post model:

<?php

namespace App\Observers;

use App\Models\Post;
use App\Notifications\Posted;
use Illuminate\Support\Facades\Notification;

class PostObserver
{
    public function publishing(Post $post)
    {
        $post->generatePermalink();
    }

    public function published(Post $post)
    {
        Notification::send(User::all(), new Posted($post));
    }

    public function creating(Post $post)
    {
        //
    }

    ...
}

Or even within my own new trait:

    protected static function bootPublishable()
    {
        // When the publishable model is published, log it
        static::published(function ($model) {
            Log::debug('Just published: ' . $model->title);
        });
    }

(Note that in the examples above I’m making use of Laravel’s special boot{traitName} and initialize{traitName} methods that should be used with traits to avoid unexpected problems with the usual boot / parent methods.) I can do the same for something like applying a global scope that, say, makes sure I’m always only fetching published posts unless I explicitly request unpublished ones:

    public static function bootPublishable()
    {
        static::addGlobalScope(new PublishedScope);
    }

I like how this approach keeps all of the publish/publishing related logic in one place, and keeps my main model class nice and clean.

That’s it! If you have tips or suggestions for improvement, let us know in the comments.

Published by

Chris Hardie

I'm a deep generalist, currently focused on software engineering + writing + the open web.

Leave a Reply

Your email address will not be published.