Decoupling notifications from controllers

Decoupling notifications from controllers Image

In most Laravel examples around the web related to sending notifications, I see them sent directly from controller actions.

While it's perfectly fine for a simple project, it may become harder to maintain in bigger projects, with a lot of notifications and channels.

In this article, I am going to show you a simple way to refactor this and to group notifications together, so they are easier to maintain.

Let's start with the following example code where we want to send an SMS notification to a user when a new post is created and when an existing post is liked.

<?php

namespace App\Http\Controllers;

use App\Notifications\PostCreated;
use App\Notifications\PostLiked;
use App\Post;
use Illuminate\Http\Request;

class PostController extends Controller
{
    /**
     * Store a new post.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
        // Create the post..

        $user->notify(new PostCreated($post));
    }

    /**
     * Like the post.
     *
     * @param Post $post
     *
     * @return \Illuminate\Http\Response
     */
    public function like(Post $post)
    {
        // Like the post..

        $user->notify(new PostLiked($post));
    }
}

Dispatching events

Instead of sending the notification directly from the controller action, we are going to create and dispatch an event.

Using php artisan make:event, I created two events PostWasCreated and PostWasLiked corresponding to the notifications we want to send.

We can now replace the notify calls with the new event triggers in our controller.

<?php

namespace App\Http\Controllers;

use App\Events\PostWasCreated;
use App\Events\PostWasLiked;
use App\Post;
use Illuminate\Http\Request;

class PostController extends Controller
{
    /**
     * Store a new post.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
        // Create the post..

        event(new PostWasCreated($post));
    }

    /**
     * Like the post.
     *
     * @param Post $post
     *
     * @return \Illuminate\Http\Response
     */
    public function like(Post $post)
    {
        // Like the post..

        event(new PostWasLiked($post));
    }
}

The code of both events is identical, it's a simple value object holding the post model instance.

<?php

namespace App\Events;

use App\Post;
use Illuminate\Queue\SerializesModels;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Broadcasting\InteractsWithSockets;

class PostWasCreated
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    /**
     * The post.
     * 
     * @var Post
     */
    public $post;

    /**
     * Create a new event instance.
     *
     * @param Post $post
     */
    public function __construct(Post $post)
    {
        $this->post = $post;
    }
}

Creating an event subscriber

To decouple the notification action from the actual event, we are going to create a new event subscriber that listens to these events and sends the notifications.

<?php

namespace App\Listeners;

use App\Events\PostWasCreated;
use App\Events\PostWasLiked;
use App\Notifications\PostCreated;
use App\Notifications\PostLiked;

class SmsNotifier
{
    /**
     * Sends a notification to the user.
     *
     * @param PostWasCreated $event
     */
    public function onPostCreated(PostWasCreated $event)
    {
        // $user comes from somewhere, maybe the site admin
        $user->notify(new PostCreated($event->post));
    }

    /**
     * Sends a notification to the user.
     *
     * @param PostWasLiked $event
     */
    public function onPostLiked(PostWasLiked $event)
    {
        // $user comes from somewhere, maybe $post->owner
        $user->notify(new PostLiked($event->post));
    }

    /**
     * Register the listeners for the subscriber.
     *
     * @param \Illuminate\Events\Dispatcher $events
     */
    public function subscribe($events)
    {
        $events->listen(PostWasCreated::class, 'App\Listeners\SmsNotifier@onPostCreated');
        $events->listen(PostWasLiked::class, 'App\Listeners\SmsNotifier@onPostLiked');
    }
}

Conclusion

The interesting thing about using an event subscriber is that you group sending the notifications inside the same class which will be much easier to maintain.

Also, you can disable all notifications just by removing the subscriber from listening to the events, instead of having to go through each call to the notify() function.

If you use many different types of notifications and channels you may want to group them in multiple subscribers, one for each type of channel, so you can control them independently.