Create a Real-Time Shoutbox with Laravel Events

Introduction

Laravel is undoubtedly a very powerful framework with a lot of batteries included. One of the reasons I love laravel is the fact that it is built on events.

What are Laravel Events?

An event is an action or occurrence recognized by a program that may be handled by the program. Some examples of events in Laravel would be:

  • A new user has signed up
  • A comment was posted
  • A user creates a blog post
  • A user likes a photo
  • Much more…

Why Use Events?

The reason we use events is because they allow us to separate application concerns and creates a mechanism for us to hook into actions in our application. When an event is fired, the event does not need to know anything about its implementation, all the event needs to know is that an action is performed, the event is triggered, the event sends some data to a listener or subscriber somewhere that has to deal with it.

Without Events, Lots of Logic in One Place

To show off how an app would start to look without events, we would do something like the following:


Route::get('signup', function() {
  // create the new user
  $user = User::create($userData);

  // a user is created, do things!
  // send the user a confirmation email
  // set up that users account
  // email them introductory information
  // do more stuff 
});

We can see how the list could start getting a little long. We can separate this out into an event so that a lot of the logic is separated from the event itself.

Another reason we use events is to speed up application data processing.

NOTE: If you are already familiar with Pub Sub, events should not be a new concept.

When to Use Events

Still using the social blogging application, when a user creates a blog post, we might have a checklist of things to do like:

  • Using a REST API to check the post for plagiarism.
  • Notify followers of the new post.
  • Submit the post to social networks and search engines etc.

Lining actions like this can slow down your application and events can be used remove this bottleneck.

Laravel also triggers a lot of system events, for example:

  • Laravel triggers an event whenever Eloquent performs any CRUD operation.
  • When Laravel compiles a blade template an event is fired.

These are just some events fired by laravel.

Defining Our Events

To generate an event in Laravel, we simply use good old artisan

php artisan make:event ActionDone

This creates a class with the name ActionDone in app/Events directory, and it should look like this:

<?php namespace App\Events;

use App\Events\Event;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;

class ActionDone extends Event {

    use SerializesModels;

    /**
     * Create a new event instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }
    
    /**
     * Get the channels the event should be broadcast on.
     *
     * @return array
     */
    public function broadcastOn()
    {
        return [];
    }
}

Event creation is very simple and requires very little setup.

Event Listeners (When an Event Fires)

After an event is triggered, the application needs to know how to deal with the event and for that we need a Listener. To generate a listener in Laravel, we can simply use artisan:

php artisan make:listener ThingToDoAfterEventWasFired --event="ActionDone"

The command to generate a listener for the event takes in a required --event flag with the name of the event to listen for. The above command creates a class named ThingToDoAfterEventWasFired in app/Listeners, and contains;

<?php namespace App\Listeners;

use App\Events\ActionDone;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;

class ThingToDoAfterEventWasFired {

    /**
     * Create the event listener.
     *
     * @return void
     */
    public function __construct() { // }

    /**
     * Handle the event.
     *
     * @param  ActionDone  $event
     * @return void
     */
    public function handle(ActionDone $event)
    {
        //
    }
}

In the event listener class, the handle method is programmatically triggered by Laravel and it type-hints the triggered event’s class.

Registering Events

Before we go on firing events here and there, Laravel’s command bus needs to know about the events and their listeners, and how to handle them.

To register the events, we navigate to app/Providers/EventServiceProvider.php, we find the protected listen property on the EventServiceProvider class and:

protected $listen = [
    'App\Events\ActionDone' => [
        'App\Listeners\ThingToDoAfterEventWasFired',
    ],
];

That’s all we need to do to let the command bus know about an event and it’s listeners. Notice that there is an array of listeners, meaning the event can have multiple subscribers.

To have an event fire off multiple listeners, just add to that array:

protected $listen = [
    'App\Events\ActionDone' => [
        'App\Listeners\ThingToDoAfterEventWasFired',
        'App\Listeners\OtherThingToDoAfterEventWasFired',
        'App\Listeners\AnotherThingToDoAfterEventWasFired',
    ],
];

Event Subscribers

Event subscribers are classes that may subscribe to multiple events from within the class itself, allowing you to define several event handlers within a single class. In the above example, we explicitly name the listeners for an event inside the EventServiceProvider.php file. We’ll define events and listeners in a new UserEventListener.php file.

Subscribers should define a subscribe method, which will be passed an event dispatcher instance:

<?php namespace App\Listeners;

class UserEventListener { 

    /**
     * Handle user login events. 
     */ 
    public function onUserLogin($event) {}

    /**
     * Handle user logout events.
     */
    public function onUserLogout($event) {}
    
    public function subscribe($events)
    {
        $events->listen(
            'App\Events\UserLoggedIn',
            'App\Listeners\UserEventListener@onUserLogin'
        );
    
        $events->listen(
            'App\Events\UserLoggedOut',
            'App\Listeners\UserEventListener@onUserLogout'
        );
    }
}

To register an event subscriber, go back to the EventServiceProvider class, and in the subscribe property add the subscriber class:

protected $subscribe = [
    'App\Listeners\UserEventListener',
];

Now all the events and listeners created in the UserEventListener will work.

Dispatching Events

To fire events, we can use the Event Facade to fire the event


use Event;
use App\Events\ActionDone;

...

Event::fire(new ActionDone());

or we can use an event helper method to fire the event


use App\Events\ActionDone;

...

event(new ActionDone());

To pass data to the listeners, we can simply use the event class as a DTO.

Queued Event Listeners

Sometimes, we don’t want the events/listeners to hold up our application’s processing. For example, we wouldn’t want a user to have to wait for their new user signup email to get emailed to them while they send the request to create their new account.

We can add event listeners to our application queue and it would be handled by whatever queue driver you specify. This would not hold up our app processing. To do this we simply have the Listener class implement Illuminate\Contracts\Queue\ShouldQueue.

<?php namespace App\Listeners;

use App\Events\ActionDone;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;

class ThingToDoAfterEventWasFired implements ShouldQueue { /*...*/ }

Broadcasting Events in Real-Time

In many modern web applications, WebSockets are used to implement real-time, live-updating user interfaces. When some data is updated on the server, a message is typically sent over a WebSocket connection to be handled by the client.

To make an event broadcastable, you make the event class implement the Illuminate\Contracts\Queue\ShouldBroadcast

<?php namespace App\Events;

use App\Events\Event;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;

class ActionDone extends Event implements ShouldBroadcast { /*...*/ }

The ShouldBroadcast interface requires the event class to implement a broadcastOn method which returns an array of channels the event should broadcast on.

public function broadcastOn()
{
    return ['action-did-occur'];
}

By default, Laravel serializes the event’s public properties and sends it as JSON to the client. If you want more control over what is sent to the client you can add a broadcastWith method to the event class and return the data to be converted into JSON.

public function broadcastWith()
{
    return [
        'user' => [
            'name' => 'Klark Cent',
            'age' => 30,
            'planet' => 'Crypton',
            'abilities' => 'Bashing'
        ]
    ];
}

Currently laravel has only two drivers(pusher and socket.io) to help with data consumption on the client. For the scope of this article we would be using pusher.

Pusher is a WebSocket as a service, you send a request from your application to the service, and pusher broadcasts the message on all clients.

To consume data on the client using pusher simply do this:

var pusher = new Pusher('pusher-key');
var channel = pusher.subscribe('action-did-occur');

channel.bind('App\Events\ActionDone', function(data) {
   console.log(data);
});

NOTE: you need pusher’s javascript SDK on the page.

<script src="//js.pusher.com/2.2/pusher.min.js"></script>

You also need pusher’s php sdk which is available via composer

composer require pusher/pusher-php-server:~2.0

Creating the Shoutbox

The demo application is a simple shoutbox, users fill in a form with their twitter handle, email address and what they would like to shout out.

Configure your .env file, mine looks like this:

APP_ENV=local 
APP_DEBUG=true
APP_KEY=ByuBK280rnHzE6DbC57byMzjVEvH4MeM

DB_HOST=localhost
DB_DATABASE=scotchbox
DB_USERNAME=root 
DB_PASSWORD=password

PUSHER_KEY=69e9a2bc295b6ca2c212
PUSHER_SECRET=a2b80fa2b04c4fa0ccf0
PUSHER_APP_ID=140801

For more info on the .env file and what it does, read the following: Understanding Laravel Environment Variables

Pusher is a great service that makes real-time components of our application effortless. They are a paid service but there is a free sandbox option (20 max connections, 100k messages per day) which should be enough for a small application or just for development. Go ahead and create an account on Pusher to get your credentials.

Setting Up Our Database

Let’s get our database ready to hold our shoutouts. We’re going to need to create the shoutouts table using a migration and an Eloquent model.

Run the following from the command line in the root of your project:

php artisan make:migration create_shoutouts_table --create=shoutouts && php artisan make:model Models/Shoutout

The migration created by artisan (database/migrations/create_shoutouts_table.php) contains:

$table->increments('id');
$table->string('handle');
$table->string('email');
$table->text('content');
$table->timestamps();

The model should have protected fillable and table properties

/**
     * The database table used by the model.
     *
     * @var string
    */
    protected $table = 'shoutouts';

    /**
     * The attributes that are mass assignable.
     *
     * @var array
    */
    protected $fillable = ['handle', 'email', 'content'];

Routing and the Controller

Next we will setup routing and move on to creating our controller. In our app\Http\routes.php file, we will create a resource route.

Route::resource('shoutouts', 'SiteController');

Now that we have the routes mapped out, let’s create the SiteController using artisan.

php artisan make:controller SiteController

This resource route will automatically create some routes for our application including the ones needed for CRUD operations. To see the routes created, just use:


php artisan route:list

Handling Creating a Shoutbox

The store method in the newly created SiteController will be the place where we handle creating and storing shoutboxes to the database. In our controller store method let’s add the following:

<?php namespace App\Http\Controllers;

...

use App\Models\Shoutbox;

public function store(Request $request) {
    $validator = Validator::make($request->all(), Shoutout::$rules);

    /**
     * Try validating the request
     * If validation failed
     * Return the validator's errors with 422 HTTP status code
    */
    if ($validator->fails())
    {
        return response($validator->messages(), 422);
    }
    
    $shoutout = Shoutout::create($request->only('email', 'handle', 'content'));
    
    // fire ShoutoutAdded event if shoutout successfully added to database
    event(new ShoutoutAdded($shoutout));
    
    return response($shoutout, 201);
}

On the client side of things (JavaScript), connect to Pusher and listen to the App\Events\ShoutoutAdded event.

var notifyUser = function (data) {
    var data = data.shoutout;

    if (! ('Notification' in window)) {
        alert('Web Notification is not supported');
    
        return;
    }
    
    Notification.requestPermission(function(permission){
        var notification = new Notification('@'+ data.handle +' said:', {
            body: data.content,
            icon: document.getElementById('site_image').content
    });
};

var loadPusher = function (){
    Pusher.log = function(message) {
        if (window.console && window.console.log) {
            window.console.log(message);
        }
    };

    var pusher = new Pusher(document.getElementById('pusher_key').content);
    var channel = pusher.subscribe('shoutout-added');
    
    channel.bind('App\\Events\\ShoutoutAdded', notifyUser);
    

};

The client listens for any broadcasted event and uses the Notification API to alert any user currently on the site. All we had to do to fire off our chain of events was use the following line in our controller:


// fire ShoutoutAdded event if shoutout successfully added to database
event(new ShoutoutAdded($shoutout));

You can see how we could separate out our application logic from the controller and into the event listener. That keeps our application lean and mean.

Conclusion

Hopefully this helps you to see how easily events can be used and how they can streamline our applications.

Events inform the application that an action occurred. They should not be misused, not every operation requires an event, but if they can move a lot of logic out of your more bloated controllers, then we’ll be better organized.

Samuel Oloruntoba

Self-proclaimed full-stack web developer and a quasi-academic. I work mostly on the backend (PHP and Node) with a recent enthusiasm for frontend development (React, SVG, HTML5 Canvas).