A guide to using WebSockets in Laravel

honeybadger_staff

Honeybadger Staff

Posted on February 19, 2024

A guide to using WebSockets in Laravel

This article was originally written by Ashley Allen on the Honeybadger Developer Blog.

Many websites and web applications that you interact with on a daily basis present information in real time. For example, when using a messaging application, messages appear on the screen as soon as they are sent to you without requiring the page to be refreshed in your browser.

Typically, this type of real-time functionality is implemented in web applications by using WebSockets.

In this article, we'll look at what WebSockets are, where you might want to use them, and alternative approaches to using WebSockets. Then, we’ll explain how to implement WebSockets in Laravel applications using Pusher. We'll cover how to set up the backend to send broadcasts via WebSockets, as well as how to set up the frontend to listen for these broadcasts. Finally, we’ll cover how to use private channels, presence channels, channel classes, and client events.

What are WebSockets?

WebSockets are a technology that you can use to send and receive data between a client and a server in real time. They allow you to send data from the server to the client without the client needing to request it. Therefore, you can send data to your users as soon as it is available, without needing users to refresh the page in their browser.

WebSockets are useful for implementing features that require instant feedback without any interval, such as real-time notifications and chat applications.

Before we delve further into the topic, it's important that we understand two key concepts used when working with WebSockets: "events" and "channels".

Channels can be thought of as a way of grouping events. For example, in a chat application, you might have a channel for each chat room, such as chat.1 and chat.2 (where 1 and 2 are the IDs of the chats). These channels would be subscribed to (or joined) by the users of the chat rooms, and only events related to these specific chat rooms (such as a message being sent, or a user joining or leaving) would be broadcast to the users.

Events are the actual data broadcast to the users on a channel. For example, in the chat application, you could have events for when a message is sent (that contains the message itself), when a user joins the chat, and when a user leaves the chat.

Typically, a user subscribes to a channel and listens for events.

To give an example of how WebSockets might be used in your application, let's imagine that you have a button that builds a report and then provides a link to download it. We'll assume that the report can sometimes take a few minutes to build, so you don't want to have to wait for the report to be built before the application allows the user to do something else. Instead, pressing the button could trigger the report to be built using the queue. Once the report has been built, the application could broadcast an event via WebSockets to the user so that a notification window pops up letting the user know the report is ready to be downloaded.

Choosing a WebSockets server

Typically, to send data to the client using WebSockets, you need to use a WebSockets server capable of handling connections between the client and server. Depending on your application, you can set up these servers yourself using something like "laravel-websockets" or "Soketi". However, this can sometimes lead to additional complexity in your system's infrastructure because you need to maintain and manage the server yourself.

Instead, you can use managed services, such as Pusher or Ably. You can rely on these services to handle the broadcasting of your data to your users so that you can focus on building your application.

The benefit of using a managed service, such as Pusher, is that you don't need to worry about the complexity of maintaining your own WebSockets server. However, this means that you need to send data to a third party which can sometimes be a security concern depending on the types of data you're sending. Thankfully, Pusher allows you to enable end-to-end encryption for WebSockets, so only the intended recipient can read the data.

As you'd imagine, you'll need to decide on a project-by-project basis whether it’s best to use a managed service or set up your own WebSockets server. For the purpose of this tutorial, we'll be using Pusher. However, the general principles we'll cover can still be implemented with other WebSocket services.

WebSockets vs. polling

As we've already covered, WebSockets allow for data to be sent in real time to the user when an event occurs. Therefore, this approach only sends data to the user when it's available.

However, similar approaches can be implemented without requiring WebSockets, such as "polling". Polling refers to the process of regularly sending a request from the client's browser to the server at a set interval to fetch a new copy of the data. For example, you might send a request to the server every five seconds to check if there are any new notifications to display for the user. Therefore, it can be used to provide a close to real-time experience.

To compare the two approaches, let's take a look at some examples.

First, let's imagine that we have a chat application. In this application, as soon as a message is sent, we want to display it in the recipient's browser. If WebSockets are used, a single broadcast is made from the server to the recipient's browser, and the message is displayed instantly. However, if polling is used, and the browser is set to poll every five seconds, there could be a potential delay of up to five seconds before the message is displayed to the user. Of course, in this scenario, polling wouldn't be very suitable because it could become frustrating for the user to have to wait five seconds before receiving the message.

Second, let's imagine that we have a dashboard displaying some sales figures and charts for your business. Again, if WebSockets are used, as soon as a sale is made, the dashboard updates instantly. However, if polling is used, the dashboard updates after five seconds. In this type of scenario, the dashboard's sales figures aren't as time-sensitive as a chat application, so it's not as important that the dashboard updates instantly. Therefore, in this case, although WebSockets would be nice to have, polling would also be a suitable approach. In fact, you may even want to increase the polling interval to something like thirty seconds or one minute to reduce the strain on the server.

It's important to remember that WebSockets only send data when it's available, so if there's no new data to send, then there's no need to process anything on the server and send the data to the browser. However, polling can potentially put extra strain on the server when constantly sending requests to check if there's any new data available.

Therefore, you'll need to decide on a feature-by-feature basis whether to use WebSockets or an alternative approach, such as polling.

Now that we've covered what WebSockets are, let's take a look at how we can use them in our Laravel applications using Pusher.

Setting up Pusher

To get set up with Pusher, you'll first need to create an account. You can do this by visiting the Pusher website.

After registering, you'll want to create a new "Channels" app. For the sake of this article, we'll be using the following details for the app we create:

  • Name your app: "laravel-websockets-test"
  • Select a cluster: "eu (EU (Ireland))"
  • Choose your tech stack (optional) - Frontend: "Vanilla JS"
  • Choose your tech stack (optional) - Backend: "Laravel"

You may also wish to check the "Create apps for multiple environments?" checkbox, which creates separate apps for your local, staging, and production environments. This can be useful if you want to use separate Pusher apps so that your data are completely separate for each environment. However, for the purposes of this tutorial, we'll just be using a single app.

After creating the app, you can then click the "App Keys" navigation option to view the keys that you'll need to add to your Laravel application.

Setting up the backend

Now that we've created an app in Pusher, let's take a look at how we can use it in our Laravel application.

To register the broadcasting authorization features in your application, Laravel ships with a App\Providers\BroadcastServiceProvider provider out of the box. By default, this provider isn't automatically registered in new Laravel applications. Therefore, you'll need to go to your config/app.php file and uncomment the line containing the App\Providers\BroadcastServiceProvider::class provider.

You'll also need to ensure that you have a queue worker running to process the broadcast events. By default, Laravel processes all event broadcasts on the queue to prevent responses from being returned to the users' requests.

Because we're using Pusher Channels in this article, we'll also need to install the Pusher Channels PHP SDK. You can install it using Composer by running the following command in your terminal:

composer require pusher/pusher-php-server
Enter fullscreen mode Exit fullscreen mode

Depending on which WebSocket service you're using, you may need to install a different package or no package at all. For example, if you're using Ably, you'll need to install the Ably PHP SDK.

You can then add the keys that you received from Pusher to your project's .env file:

PUSHER_APP_ID=pusher-app-id-goes-here
PUSHER_APP_KEY=pusher-key-goes-here
PUSHER_APP_SECRET=pusher-secret-goes-here
PUSHER_APP_CLUSTER=pusher-cluster-goes-here
Enter fullscreen mode Exit fullscreen mode

If you're using the default Laravel .env file, you may have noticed the following values:

VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
VITE_PUSHER_HOST="${PUSHER_HOST}"
VITE_PUSHER_PORT="${PUSHER_PORT}"
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
Enter fullscreen mode Exit fullscreen mode

The environment variables starting with VITE_ are used by Vite so that we can access their values in our JavaScript code. For example, VITE_PUSHER_APP_KEY can be accessed in our JavaScript code using import.meta.env.VITE_PUSHER_APP_KEY. If these environment variables aren't already in your .env file (and you're using Vite for compiling your JavaScript), you can add them. Using this approach allows us to keep our keys in a single place (the .env file) and prevents us from adding hardcoded values to our application code. This makes maintenance easier if we need to change the values in the future when rotating keys, such as in the event of a breach.

It's important to remember that you shouldn't make the PUSHER_APP_SECRET variable accessible to the frontend using one of the VITE_ variables. This is purely used on the backend and should never be exposed to the frontend.

You'll also need to update your .env file to set the BROADCAST_DRIVER to pusher:

BROADCAST_DRIVER=pusher
Enter fullscreen mode Exit fullscreen mode

These environment variables are all used inside the config/broadcasting.php config file. Therefore, if you wish to make any changes to your broadcasting-related config, you can change them within this file.

The backend should now be configured and ready to send broadcasts on WebSockets. Let's take a look at how to configure the frontend.

Setting up the frontend

Laravel provides a handy JavaScript library that you can use to subscribe to WebSocket channels and listen for events called Laravel Echo. Laravel Echo is quite handy because it provides a simple API that you can use to subscribe to your channels without needing to understand the underlying WebSocket protocol.

You can install it in your project using NPM by running the following command in your terminal:

npm install --save-dev laravel-echo pusher-js
Enter fullscreen mode Exit fullscreen mode

You might notice that we're also installing the pusher-js library. We are required to install this because we're using Pusher for broadcasting our events.

After installing Laravel Echo, we can then create a new instance of it in our JavaScript code.

For the purposes of this example, we'll assume that we're using Vite to build our application's assets, so we can access the environment variables that we added to our .env file earlier on. If you're using a different tool, such as Webpack (whether directly or through Laravel Mix), you may need to use a different approach to access your environment variables.

If you're working on a fresh Laravel application, the resources/js/bootstrap.js file that ships with the default installation already has the code included to work with Laravel Echo. However, you need to remember to uncomment it so that it can be used. If you're not working with a fresh Laravel application, the contents of the resources/js/bootstrap.js file are as follows:

/**
 * We'll load the axios HTTP library which allows us to easily issue requests
 * to our Laravel back-end. This library automatically handles sending the
 * CSRF token as a header based on the value of the "XSRF" token cookie.
 */

import axios from 'axios';
window.axios = axios;

window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';

/**
 * Echo exposes an expressive API for subscribing to channels and listening
 * for events that are broadcast by Laravel. Echo and event broadcasting
 * allows your team to easily build robust real-time web applications.
 */

import Echo from 'laravel-echo';

import Pusher from 'pusher-js';
window.Pusher = Pusher;

window.Echo = new Echo({
    broadcaster: 'pusher',
    key: import.meta.env.VITE_PUSHER_APP_KEY,
    cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER ?? 'mt1',
    wsHost: import.meta.env.VITE_PUSHER_HOST ? import.meta.env.VITE_PUSHER_HOST : `ws-${import.meta.env.VITE_PUSHER_APP_CLUSTER}.pusher.com`,
    wsPort: import.meta.env.VITE_PUSHER_PORT ?? 80,
    wssPort: import.meta.env.VITE_PUSHER_PORT ?? 443,
    forceTLS: (import.meta.env.VITE_PUSHER_SCHEME ?? 'https') === 'https',
    enabledTransports: ['ws', 'wss'],
});
Enter fullscreen mode Exit fullscreen mode

As you might have noticed, we're working with the VITE_PUSHER_* environment variables (such as VITE_PUSHER_APP_KEY and VITE_PUSHER_APP_CLUSTER) and passing them to the Echo instance. These are the same environment variables that we previously added to our .env file.

After creating the Echo instance, you can then compile assets by running the following command in your terminal

npm run dev
Enter fullscreen mode Exit fullscreen mode

Although nothing will happen yet in your browser, we're now ready to write the code to listen for events. If everything is configured correctly, Vite should be able to compile your assets without any errors.

Listening to public channels

Now that we have Pusher set up and our application configured, let's take a look at how we can use it to listen to public channels.

In this example, we'll create a simple Blade view that contains a button. When the button is clicked, it will trigger an event to be broadcast on a public channel. We'll listen for that event in our JavaScript code and use the alert function when it's received to display the message passed in the event.

The Blade view may look something like this:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">

        <title>WebSockets Test</title>

        @vite('resources/js/app.js')
    </head>

    <body>
        <button id="submit-button" type="button">
            Press Me!
        </button>
    </body>
</html>
Enter fullscreen mode Exit fullscreen mode

We'll now want to create a new route in our routes/web.php file. When we click the button, we'll send a POST request to this route, which will trigger the broadcast:

use App\Http\Controllers\ButtonClickedController;
use Illuminate\Support\Facades\Route;

Route::post('button/clicked', ButtonClickedController::class);
Enter fullscreen mode Exit fullscreen mode

We can then create the ButtonClickedController by running the following command in our terminal:

php artisan make:controller ButtonClickedController -i
Enter fullscreen mode Exit fullscreen mode

You may have noticed that we are passing -i to the command. This is done so that we can create a single-use controller that only has an __invoke method since we don't need any other methods in our controller. However, you can remove the -i flag if you want to create a full controller.

We can then update our controller so that it dispatches an event when called and returns a JSON response:

declare(strict_types=1);

namespace App\Http\Controllers;

use App\Events\ButtonClicked;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

final class ButtonClickedController extends Controller
{
    public function __invoke(Request $request): JsonResponse
    {
        ButtonClicked::dispatch(message: 'Hello world!');

        return response()->json(['success' => true]);
    }
}
Enter fullscreen mode Exit fullscreen mode

As you can see inside our controller, we've referenced an event called ButtonClicked and passed a message parameter to it. This is the event that we'll broadcast via WebSockets. We can create the event by running the following command in our terminal:

php artisan make:event ButtonClicked
Enter fullscreen mode Exit fullscreen mode

Running the above command will create a new app/Events/ButtonClicked.php file.

I've made some changes to the class generated. Let's take a look at the class and then break down what's happening:

declare(strict_types=1);

namespace App\Events;

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

final class ButtonClicked implements ShouldBroadcast
{
    use Dispatchable;
    use InteractsWithSockets;
    use SerializesModels;

    public function __construct(
        private readonly string $message
    ) {
        //
    }

    public function broadcastAs(): string
    {
        return 'button.clicked';
    }

    public function broadcastWith(): array
    {
        return [
            'message' => $this->message,
        ];
    }

    public function broadcastOn(): array
    {
        return [
            new Channel('public-channel'),
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

First, we have ensured the class is implementing the Illuminate\Contracts\Broadcasting\ShouldBroadcast interface. This is needed so that Laravel knows to broadcast the event over a WebSocket connection. By default, this interface will cause the event to be processed and broadcast on the queue. However, if you'd prefer to broadcast the event synchronously (before returning the response for the request), you can use the Illuminate\Contracts\Broadcasting\ShouldBroadcastNow interface instead.

This will force us to add a broadcastOn method to our event. This method should return an array containing the channel(s) on which we want to broadcast the event. In this case, we'll broadcast the event on a channel called public-channel.

We will also add an optional broadcastAs method to our event. This will return the name of the event that we can to broadcast. We'll call it button.clicked. If we don’t specify the method, Laravel will fall back to the name of the event class. In this case, it would be App\Events\ButtonClicked. It's important to remember that if you specify the broadcastAs method, you'll need to add a . to the beginning of the event name in Laravel Echo. Otherwise, it will append the expected namespace (App\Events) to the event name. In this case, in our JavaScript, want to listen for .button.clicked instead of button.clicked.

We can also specify a broadcastWith method. This will return an array of data that we want to broadcast with the event. In this case, we'll broadcast the message property that we passed to the event's constructor and will be displayed in the JavaScript alert box. However, you can pass whatever data best suits your feature here. For example, if you were building a chat application, you might want to broadcast the message that was sent and some other meta information about the chat.

This is all that's needed on the backend to send the broadcasts. We can now update the frontend to listen for the broadcasts and display the alert box.

We'll need to add an event listener that sends a POST request to the /button/clicked route whenever the button is clicked. We know that our event is called button-clicked and that it's being broadcast on the public-channel channel. We can update our reousrces/js/app.js file using the following code:

import './bootstrap';

// Create an event listener that will send a POST request to the
// server when the user clicks the button.
document.querySelector('#submit-button').addEventListener(
    'click',
    () => window.axios.post('/button/clicked')
);


// Subscribe to the public channel called "public-channel"
Echo.channel('public-channel')

    // Listen for the event called "button.clicked"
    .listen('.button.clicked', (e) => {

        // Display the "message" in an alert box
        alert(e.message);
    });
Enter fullscreen mode Exit fullscreen mode

We can now test that this works. If you click the button on the page, you should see an alert box appear in your browser. To prove that this alert box is being displayed using WebSockets, you can open two different browsers and go to the same page in each browser. Then, click the button in one browser. The alert box should appear in both browsers.

Listening to private channels

So far, we've covered how to send broadcasts over public WebSocket channels. However, there are times when you might want to send broadcasts over private channels so that only authorized users can listen to them. For example, if you were building a chat application, you wouldn't want your messages to be broadcast on a public channel when you sent them; this would allow anyone that knows the name of the channel to listen to your messages. Instead, you'd want to send your messages over a private channel so that only the intended recipients can listen to them.

Let's take a look at how we can use private channels in our application.

By default, Laravel Echo will make an HTTP request to a /broadcasting/auth endpoint in your application when a user attempts to subscribe to a private channel. This endpoint is defined in the app/Providers/BroadcastServiceProvider class. We can customize this endpoint by overriding the broadcastingAuthEndpoint method on the BroadcastServiceProvider class and updating our Laravel Echo configuration. However, for the purposes of this guide, we'll use the default route.

Let's imagine that we have a chat application, and we want to use private channels. To define the authorization logic for our private channel, we'll need to use the Broadcast::channel method in our routes/channels.php.

This method accepts two arguments: the name of the channel and a callback that will be executed when a user attempts to subscribe to the channel. If the user is authorized to subscribe to the channel, the callback should return true or an array of data that will be sent to the client (we'll cover this part when discussing presence channels later). If the user is not authorized to subscribe to the channel, the callback should return false.

We’ll assume that the Chat model has an isMember method that will return true if the user is a member of the chat and false if they are not.

We can define the private channel authorization for our chat application using the following code in the routes/channels.php file:

use App\Models\Chat;
use App\Models\User;
use Illuminate\Support\Facades\Broadcast;

Broadcast::channel(
    'chats.{chat}',
    fn (User $user, Chat $chat): bool => $chat->isMember($user)
);
Enter fullscreen mode Exit fullscreen mode

As we can see in the example above, we've defined a chats.{chat} private channel. Then, we checked whether the user is a member of the chat to which they're attempting to subscribe. Similar to route model binding for routes in Laravel, by defining {chat} in the channel name, we can type hint the Chat model in the callback. This will automatically resolve the Chat model instance from the parameter for us.

We can now create an event that will broadcast on a private channel. We'll call the event App\Events\MessageSent. Let's take a look at the event class, and then we'll look at what's been done:

declare(strict_types=1);

namespace App\Events;

use App\Models\Chat;
use App\Models\ChatMessage;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

final class MessageSent implements ShouldBroadcast
{
    use Dispatchable;
    use InteractsWithSockets;
    use SerializesModels;

    public function __construct(
        private readonly Chat $chat,
        private readonly ChatMessage $chatMessage
    ) {
        //
    }

    public function broadcastAs(): string
    {
        return 'message.sent';
    }

    public function broadcastWith(): array
    {
        return [
            'message' => $this->chatMessage->message,
            'sentBy' => [
                'id' => $this->chatMessage->sentBy->id,
                'name' => $this->chatMessage->sentBy->name,
            ]
        ];
    }

    public function broadcastOn(): array
    {
        return [
            new PrivateChannel('chats.'.$this->chat->id),
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

As we can see, the broadcastOn method is returning an array that contains a PrivateChannel object rather than a Channel object. If we were to assume the Chat model has an id of 1, the event would be broadcast on the chats.1 private channel.

You may have also noticed that the constructor includes the Chat model so that we can access it. To dispatch such an event, our code may look something like this:

use App\Models\Chat;
use App\Events\MessageSent;

MessageSent::dispatch(chat: $chat, chatMessage: $chatMessage);
Enter fullscreen mode Exit fullscreen mode

Let's now write some example JavaScript code that could be used to subscribe to a private channel. We'll assume that a displayChatMessage function handles displaying the chat message in the browser. We'll also assume that a getChatId function returns the id of the chat that the user is currently viewing.

const chatId = getChatId();

Echo.private(`chats.${chatId}`)
    .listen('.message.sent', (e) => {
        displayChatMessage(e.message, e.sentBy);
    });
Enter fullscreen mode Exit fullscreen mode

As you can see, it's very similar to subscribing to a public channel. The only difference is that we're using the private method instead of the channel method. Laravel Echo handles the authorization logic for us behind the scenes so that we don't need to worry about how it's done.

Listening to presence channels

Presence channels are private, but they allow you to have some awareness of who is subscribed to a given channel. This type of feature could be used in a chat application to know which users are present in the chat. They give us access to events that provide visibility of when someone is joining or leaving the channel.

Similar to private channels, you must authorize the user when attempting to join the channel. Rather than just returning a simple true or false from the method, we can return some data in an array. For example, let's take our previous private channels example for the chat room. When authorizing a user for the chats.{chat} channel, we could return an array containing the user's id and name. This data would then be broadcast to the other users already subscribed to this channel. This could be used for displaying information, such as "John Smith joined the chat", or "John Smith left the chat", or maybe even highlighting the users currently online in a list.

To get started with using a presence channel, we'll first need to define the authorization logic for the channel. We'll do this in the routes/channels.php file:

use App\Models\Chat;
use App\Models\User;
use Illuminate\Support\Facades\Broadcast;

Broadcast::channel('chats.{chat}', function (User $user, Chat $chat): bool {
    return $chat->isMember($user)
        ? ['id' => $user->id, 'name' => $user->name]
        : false;
});
Enter fullscreen mode Exit fullscreen mode

Now if we wanted to broadcast an event to this channel, it would be very similar to broadcasting to a private channel. The only difference is that we need to return a PresenceChannel object in the array from the broadcastOn method rather than a PrivateChannel:

use Illuminate\Broadcasting\PresenceChannel;

public function broadcastOn(): array
{
    return [
        new PresenceChannel('chats.'.$this->chat->id),
    ];
}
Enter fullscreen mode Exit fullscreen mode

Now we can write the JavaScript that handles the presence channel logic.

We'll assume that a getChatId function returns the id of the chat the user is currently viewing. We'll also assume that we have four other functions:

  • highlightActiveUsers - This function will highlight the users currently online in the chat.
  • displayUserJoinedMessage - This function will display a message to the user that another user has joined the chat. It will also highlight the user in the list of active users.
  • displayUserLeftMessage - This function will display a message to the user that another user has left the chat. It will also remove the user from the list of active users.
  • displayChatMessage - This function will display the chat message that has just been sent in the browser.

To join a presence channel, we can use the join method in Laravel Echo:

const chatId = getChatId();

Echo.join(`chat.${chatId}`)
    .here((users) => {
        // This is run when you first join the channel.
        highlightActiveUsers(users);
    })
    .joining((user) => {
        // This is run when other users join the channel.
        displayUserJoinedMessage(user);
    })
    .leaving((user) => {
        // This is run when users are leaving the channel.
        displayUserLeftMessage(user);
    })
    .listen('.message.sent', (e) => {
        // This is run when a message is sent to the channel.
        displayChatMessage(e.message, e.sentBy);
    })
    .error((error) => {
        // This is run if there's a problem joining the channel.
        console.error(error);
    });
Enter fullscreen mode Exit fullscreen mode

You might have noticed that five methods are being used here. Let's take a look at what each one does:

  • join - This function allows us to join a presence channel. It also handles the authorization logic for us.
  • here - This function is called when you first join the channel. It receives an array of all the users currently subscribed to the channel. In a chat application, this could be used to display a list of users currently online when you first join the channel.
  • joining - This function is called whenever other users join the channel and receives the data (in the format we defined in the routes/channels.php file). In a chat application, this could be used to display a message to the user that another user joined the chat or highlight the users in a list of active users.
  • leaving - This function is called whenever other users leave the channel and receives the data (in the format we defined in the routes/channels.php file). In a chat application, this could be used to display a message to the user that another user has left the chat or remove the user from the list of active users.
  • listen - This function is the same as the listen function that we used in our public and private channel examples earlier. It allows us to listen for events broadcast to the channel, such as when new messages are sent.
  • error - This function is called if there are any issues with authorizing the user when trying to join the channel or if the JSON response returned from the authorization endpoint is invalid.

As you can see, presence channels can help to provide a more interactive experience for the user without adding too much extra complexity to your code.

Taking it further

Now that we've covered the three main types of channels and how we can use them to broadcast data to the client, let's take a look at some other handy WebSockets-related features that we can use in our Laravel applications.

Using broadcast channel classes

As your project grows, you may find that your routes/channels.php file becomes quite large, especially with the authorization logic for private channels. This can sometimes make it difficult to read and maintain. To keep this file clean and improve its maintainability, we can make use of a feature that Laravel provides called "channel classes".

Channel classes are PHP classes that allow us to encapsulate the authorization logic for our private channels within a class. This class can then be used in our routes/channels.php file to define the authorization logic for the channel.

For example, let's take the following private channel definition from the routes/channels.php file:

use App\Models\Chat;
use App\Models\User;
use Illuminate\Support\Facades\Broadcast;

Broadcast::channel(
    'chats.{chat}',
    fn (User $user, Chat $chat): bool => $chat->isMember($user)
);
Enter fullscreen mode Exit fullscreen mode

We could create a new channel class by running the following command:

php artisan make:channel ChatChannel
Enter fullscreen mode Exit fullscreen mode

This would create an app\Broadcasting\ChatChannel class with a join method. We can add our authorization logic to this method:

declare(strict_types=1);

namespace App\Broadcasting;

use App\Models\Chat;
use App\Models\User;

final class OrderChannel
{
    public function join(User $user, Chat $chat): bool
    {
        return $chat->isMember($user);
    }
}
Enter fullscreen mode Exit fullscreen mode

Alternatively, if we wanted to return an array of data (like for using in a presence channel), the join method may look like this instead:

public function join(User $user, Chat $chat): bool
{
    return $chat->isMember($user)
        ? ['id' => $user->id, 'name' => $user->name]
        : false;
}
Enter fullscreen mode Exit fullscreen mode

We can then update our channel route in the routes/channel.php file to use this new channel class:

use App\Broadcasting\ChatChannel;
use Illuminate\Support\Facades\Broadcast;

Broadcast::channel('chats.{chat}', ChatChannel::class);
Enter fullscreen mode Exit fullscreen mode

Broadcasting to others

There may be times when you only want to send a broadcast to other users but not yourself.

For example, let's imagine that you are using a chat application and send a message. In your application's JavaScript, you might instantly display the message on your screen as soon as you send it. You may also have Laravel Echo set up to listen for any new messages sent. When it receives one of these events, Laravel Echo will display the message on the page. This would work perfectly for displaying the messages in the recipient's browser. However, it would result in the message being displayed twice in the sender's browser (once at the time of sending and again at the time of receiving the WebSocket event).

To prevent scenarios like this from happening, we can use the toOthers method when dispatching the event that will be broadcast. For example, if we wanted to broadcast a MessageSent event to other users in the chat, we could do the following:

use App\Events\MessageSent;

broadcast(
    new MessageSent(chat: $chat, message: $chatMessage)
)->toOthers();
Enter fullscreen mode Exit fullscreen mode

Broadcasting on multiple channels per event

So far, in all the examples above, we've only broadcast events to single channels. However, there may be times when you want to broadcast an event on multiple channels simultaneously. For example, let's say that your application has a private channel set up for each user; you may want to send a notification to several of the users at once.

To do this, you can return an array of channels in the broadcastOn method of your event. For instance, let's say that we have three users with their own private notifications channel enabled (with the channels being called notifications.1, notifications.2, and notifications.3). We could send the broadcast to each of the users by returning an array of PrivateChannel classes from the broadcastOn method:

declare(strict_types=1);

namespace App\Events;

use App\Models\User;
use Illuminate\Support\Collection;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

final class NewNotification implements ShouldBroadcast
{
    use Dispatchable;
    use InteractsWithSockets;
    use SerializesModels;

    /**
     * @param Collection<User> $users
     */
    public function __construct(
        private readonly Collection $users,
    ) {
        //
    }

    // ...

    public function broadcastOn(): array
    {
        return $this->users->map(function (User $user): PrivateChannel {
            return new PrivateChannel('notifications.'.$user->id);
        })->all();
    }
}
Enter fullscreen mode Exit fullscreen mode

In the example above, we're passing a Collection of User models to the event's constructor. In the broadcastOn method, we're then mapping through the Collection and returning an array of PrivateChannel instances (one PrivateChannel instance per user).

Whispering

Laravel Echo allows you to use "client events". These events don't hit your application's server and can be really useful for providing functionality, such as typing indicators in a chat application.

Let's imagine that we have a chat application, and we want to send a client event to other members of the chat when someone is typing. We'll assume that there is a getUserName method that returns the name of the logged-in user. We'll also use a very naive and basic approach for detecting whether a user is typing. In a real-life application, you may want to also add debouncing, listen for users pasting text into the text box, etc. However, for the purposes of this guide, we'll just send a client event whenever a user types into the message box.

To enable client events, you can use the whisper and listenForWhisper methods in your JavaScript:

// Resolve the channel instance.
let channel = Echo.private('chat');

// Add handling when a client event is detected. This will
// output "John Smith is typing..." to the console.
channel.listenForWhisper('typing', (e) => {
    console.log(e.name + ' is typing...');
})

// Add an event listener that will trigger a "typing" client
// event whenever the user types into the message box.
document.querySelector('#message-box')
    .addEventListener('keydown', function (e) {
        channel.whisper('typing', {
            name: getUserName(),
        });
    });
Enter fullscreen mode Exit fullscreen mode

The whisper method will be called whenever the user types and will send the data to the other users listening on the channel.

The listenForWhisper method will be called on the receiving users' browsers and be passed the data sent by the whisper method.

It's important to note that if you're using Pusher (as shown in this guide), you'll need to enable the "Client Events" option in the "App Settings" of your Pusher Channels app.

Conclusion

This article should have given you some insight into what WebSockets are, where you might want to use them, and how to set them up in Laravel using Pusher. You should now be able to use WebSockets in your own Laravel applications to add real-time functionality using public channels, private channels, and presence channels. You should also be able to use concepts such as channel classes and client events to build robust WebSockets integrations.

💖 💪 🙅 🚩
honeybadger_staff
Honeybadger Staff

Posted on February 19, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related