Simple WebSocket Calls in Laravel & Vanilla JS using Ably and Laravel Echo

Hi there,

In this article, we will explore how to set up WebSocket calls between Laravel and a Vanilla JS application (as the client) using Ably and the Laravel Echo package.

The use cases for WebSocket communication differ. In some cases, it may be used for real-time notifications and real-time updates, in other cases, it may be used for a chat application.

For the purposes of this article, we want to walk through how you can get up and running with Websockets in Laravel and listen for those events on the client side with a Vanilla JS app.

Okay, let's dive in!

Your Ably Account

First, you need to create an account on ably. After creating an account, you should see the Create a new app button and you can give your app any name. The name here has nothing to do with the name of the actual event, it is just an identifier for the app.

In my case, I have created an app, you should see something like this.

You can then click on the app and get to the Api keys tab to get your credentials.

Next, you should go to the settings tab to update the Protocol adapter settings . Make sure, the Pusher protocol support is checked.

The Laravel Backend

This article assumes that we already have a Laravel application ready and we just need to integrate WebSocket comms. If not, you can visit the Laravel documentation to learn how to set up a Laravel project.

If you need an application to play with you can clone the GitHub repository that I'll be using for the explanations here.

https://github.com/IDTitanium/property-api

Configuration

You need to install the Ably SDK on your Laravel project, using the command

composer require ably/ably-php

Next, you need to set the Ably private key in your Environment variable, on development, that would be your .env file.

You also, need to update your BROADCAST_DRIVER to ably

BROADCAST_DRIVER=ably
...
ABLY_KEY=Gm3wTg.lA:8T_LDPJzw_pDZKSk9QypSM (This is private 
you got key from the ably dashboard)

IMPORTANT: if your project's Laravel version is lesser than Laravel 9.x or you recently upgraded from a version lesser than 9.x, you need to include this in your broadcasting.php file

//broadcasting.php
...
'ably' => [
      'driver' => 'ably',
      'key' => env('ABLY_KEY'),
 ],
...

Next, we need to create the Event we want to trigger when there is an update we need to push to the client.

Using the property-api backend I shared earlier, we want to send new updates to the client whenever a new property is added.

The Laravel Event

So let's create the event. We'll call it NewPropertyUpdateEvent . And we'll make sure it implements the ShouldBroadcast interface.

Like this,

<?php

namespace App\Events;

use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class NewPropertyUpdateEvent implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    /**
     * Create a new event instance.
     *
     * @return void
     */
    public function __construct(public mixed $payload)
    {
        //
    }

    /**
     * Get the channels the event should broadcast on.
     *
     * @return \Illuminate\Broadcasting\Channel|array
     */
    public function broadcastOn()
    {
        return new Channel("property");
    }

    /**
     * The event's broadcast name.
     */
    public function broadcastAs(): string
    {
        return 'property.created';
    }
}

We have defined a $payload property in the class constructor as public and this will be present in the general payload when the listener receives the event. All public properties defined in the event class will be present in the listener payload.

Notice that in the broadcastOn method, we have defined the name of the channel as property and we defined it as an argument of the Channel class (full name: Illuminate\Broadcasting\Channel). This is how you defined the channel name for a public channel. If you want to create a private channel, you should do something like this instead.

use Illuminate\Broadcasting\PrivateChannel;

...
public function broadcastOn() {
        return new PrivateChannel("property.".$this->userId);
}

Appending the user id to the channel makes each user's channel unique, and you can use it to authorize the user listening to the channel, we'll discuss channel authorization later on.

The second thing to notice in the class is the broadcastAs method. This is used to define a custom name for the event. When we get to the client section you'd see how this is useful for listening to the event.

Triggering The Event

Now we can trigger this event in the portion of the code where a new property is stored.

/**
     * Store properties on database
     * @param mixed $requestData
     * @return mixed
     */
    public function storeProperties($requestData) {
        if (isset($requestData['address'])) {
            $requestData = $requestData['address'];
        }
        $property = Property::create(
            [
                'address_line_1' => $requestData['line_1'],
                'address_line_2' => $requestData['line_2'],
                'postcode' => $requestData['postcode']
            ]
        );

        event(new NewPropertyUpdateEvent($property));

        return $property;
    }

Channel Authentication

If you are working with a private channel that requires authentication, you can pass your auth middleware into the Broadcast::routes() call in the BroadcastServiceProvider . So in this case, if we had an auth middleware for the API, we can specify it like this.

<?php

namespace App\Providers;

use Illuminate\Support\Facades\Broadcast;
use Illuminate\Support\ServiceProvider;

class BroadcastServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        /** Added the auth:api middleware */
        Broadcast::routes(['auth:api']);

        require base_path('routes/channels.php');
    }
}

The sample project used in this article has no authentication for the WebSocket call because it's broadcasting on a public channel, therefore it's open.

Channel Authorization

If you have an application that has users, you may also want to send updates to specific users. To do this, you need to define your event channel as private, and you need to have the channel authorization set up, this is done by adding the channel name and authorization condition to this channels.php file.

As an example, if we had to do that for our sample API, we could do something like this.

<?php

use Illuminate\Support\Facades\Broadcast;

/*
|--------------------------------------------------------------------------
| Broadcast Channels
|--------------------------------------------------------------------------
|
| Here you may register all of the event broadcasting channels that your
| application supports. The given channel authorization callbacks are
| used to check if an authenticated user can listen to the channel.
|
*/

Broadcast::channel('property.{userId}', function ($user, $userId) {
    return (int) $user->id === (int) $id;
});

The $user object in the callback function above is based on the auth middleware specified in the BroadcastServiceProvider . And $userId is retrieved from the channel definition like a path parameter.

This concludes the work required on the backend. Let's move to the frontend.

The Vanilla JS Frontend

We will create a simple html project and bundle the js assets with Webpack.

Our directory structure will look like this,

/dist
/src
    index.html
    index.js

PS: this folder structure is influenced by Webpack. After building the js, it will store it in the dist folder. And it will by default look for the index.js file in the src folder.

After creating the files and folders, we can then run npm init to set up our package.json and follow the prompt.

After which we can install the npm packages below.

npm install --save-dev laravel-echo pusher-js

Next, we can add the content of the index.html file.

<!--Index.html-->
<!DOCTYPE html>

<html>
    <head>
        <!--to be gotten after js is bundled-->
        <script src="../dist/main.js"></script>
    </head>

    <body>
        <h1>Broadcast receiver working</h1>
        <h2>Listening for property added...</h2>
        <div id="property"></div>
    </body>


</html>

The main.js added to the html header will be generated by Webpack after we bundled the assets.

And our index.js will look like this,

//index.js
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';

window.Pusher = Pusher;

window.Echo = new Echo({
    broadcaster: 'pusher',
    key: 'GmT.2A',
    wsHost: 'realtime-pusher.ably.io',
    wsPort: 443,
    disableStats: true,
    encrypted: true,
    cluster: 'eu'
});

window.Echo.channel('property')
            .listen('.property.created', (e) => {
            displayItemsOnTheUI(e)
            console.log("new event", e)
});

function displayItemsOnTheUI(e) {
      let ul = document.createElement('ul')
      for (const key in e.payload) {
           let li = document.createElement('li')
           li.innerHTML = `${key} : ${e.payload[key]}`
           ul.appendChild(li)
           console.log(key, e.payload[key])
       }
      document.getElementById("property").appendChild(ul)
}

The key in this object, is the public key you got from the pusher dashboard. The public key is the portion of the private key before the : sign.

window.Echo = new Echo({
    broadcaster: 'pusher',
    key: 'GmT.2A',
    wsHost: 'realtime-pusher.ably.io',
    wsPort: 443,
    disableStats: true,
    encrypted: true,
    cluster: 'eu'
});

If you are working with a private channel, you need to include some extra properties like authEnpoint and if your authentication requires a bearer token, you can include these configs.

window.Echo = new Echo({
    ...
    authEndpoint: `your-api-host/broadcasting/auth`,
    auth: {
        headers: {
            Authorization: `Bearer ${token}`
        }
    }
});

Also, notice the listener code below; we have added the channel as property which is the name we gave our channel in the broadcastOn method in the Laravel event, and the .listen method is configured with the name of the event property.created which is what we defined in the broadcastAs method in the Laravel event class.

Notice that the name in the .listen method is prefixed with a dot . , this is important to include for all custom event names.

window.Echo.channel('property')
            .listen('.property.created', (e) => {
            displayItemsOnTheUI(e)
            console.log("new event", e)
});

Next, we can run npx webpack , this may require you to install the Webpack CLI, you can simply run npm install --save-dev webpack-cli and run npx webpack again.

You should now have the main.js file in your dist folder.

The complete frontend code can be found here https://github.com/IDTitanium/broadcast-receiver

Aaaand we are ready to test!

Now, on the Laravel side, you need to ensure your Queue is running. Because the event will get queued, unless you've set your queue connection to sync.

When you make the API call like this,

You should immediately get the entries on the client side, like this.

Conclusion

You can extend this to any part of your app that requires real-time updates.

If you have issues setting this up, you can leave a comment here, and I'll do my best to help.

O dabo.