Understanding Laravel Broadcasting

Syed Sirajul Islam Anik
12 min readMay 30, 2020

--

Image from: https://photos.demandstudios.com/getty/article/228/230/140473643.jpg

In our applications, it sometimes happens to send realtime data without loading the webpage. These data can be broadcasted over the WebSocket. In this article, I will explain Laravel Broadcasting with Laravel-Echo-Server, which is a community-driven WebSocket server. I created a full project which sort of covers the chatting system. Check the repository here. It already comes with the docker. Read the readme.md file for installation.

Notice

The git repository I share in this article was made within a night. It bothers less about the security, coding practice, without thinking about the edge-case scenarios. It’s up to the readers how they want to use the code. It’s only for educational purposes. Cheers 🍻

Laravel Broadcasting

Leave the chatting service aside, let’s think about a ride-sharing application. You send an application to the server. The server receives your request and then returns a response. Then you communicate with the server over WebSocket which will send you data for the driver who accepted, where is your driver and when the driver ends the ride you get to see the invoice for the ride. It can be a typical use-case, right? So, let’s point out the actions we should take into consideration.

  • The user sends a request from source to destination. The server accepts the requests and returns a channel where the user should connect for the upcoming updates.
  • The driver was selected. He has possibly connected to a channel already. He gets a ping through his channel that he got a new ride. For complexity, let’s not consider other factors.
  • When the driver accepts the ride, then the user gets notified through his channel. He also gets updates on the driver’s location.
  • The driver starts the ride, the user gets a notification. Whenever the driver ends the ride, he then gets another notification. Then the invoice pops up.

In the above scenarios, we used channels to communicate for the driver’s event to the user. When using Laravel, we can notify back & forth using Broadcast. Now, let’s see what are the requirements.

To use broadcast, you’ll first need a driver. Laravel comes with several drivers. In this article, we’ll use Laravel-Echo-Server. You can use pusher as the driver if you want to use pusher service. Or like I did, you can use redis driver. In the above git repository, I used separate redis connection for broadcasting. Check the environment variable & config/databse.php. You can use whichever you want. Another prerequisite for this is to set up a queue driver. I used beanstalkd, you can choose whatever seems good to you. Even if you use redis as a queue driver, I already created another connection for redis. Please check the config/database.php and environment variables. And, last but not the least, you should uncomment the BroadcastServiceProvider from your config/app.php's providers array. The previously shared repository contains all the required packages. Dig deep to get a good grasp of it.

Keep in mind

  • Your broadcast redis instance and laravel-echo-server’s redis instance should be the same.
  • The PREFIX for laravel under REDIS_PREFIX should be identical to laravel-echo-server’s prefix.
  • This broadcasting follows pub-sub architecture. If you send a message (later in the article), and no client is listening to that publish message, the message/broadcast will be lost forever.

Let’s get our hand dirty

We have our echo server up and running. To connect to the echo server, we’ll need socket.io-client and laravel-echo for the front-end. socket.io-client connects to the socket server & laravel-echo implements Laravelish way on how the backend broadcasting works. In my git repository, I used no jquery as I lack myself reactjs or vuejs. The main idea is to instantiate the Echo instance.

import Echo from "laravel-echo"

window.io = require('socket.io-client');

window.Echo = new Echo({
broadcaster: 'socket.io',
host: window.location.hostname + ':6001'
});
  • In the broadcaster property, we’re saying the to use socket.io
  • In the host property, we’re giving the echo server’s IP and PORT.
  • These are already in the resources/js/bootstrap.js. After compiling the code ( yarn run dev or npm run dev) it’ll put a js file in public/js/app.js. All we need to do is to point to this file from our view file.
  • If everything goes well, it’ll then establish a WebSocket connection with the server.
Status 101, has successfully connected to the WebSocket server

Now, let’s work on our backend.

Backend

To broadcast a message from the backend to the WS server, you’ll need to know the following.

  • The messages are sent over channels. There are 3 types of channels. Public (Channel class) no auth required, Private (PrivateChannel class) — requires authentication to connect to this channel. Presence (PresenceChannel class) — requires authentication to connect to this channel.
  • The authentication is done via session & CSRF. Whenever the front-end tries to connect to the WebSocket server for a channel, if the channel requires authentication, then the echo server forwards the call to the backend to verify if the user is authenticated. If the server returns true, then the front-end can connect to the WS server for that channel and share data back and forth.

Let’s send a message to a public channel. To send a broadcast, you’ll need to create an event. Use php artisan make:event ActivityEvent to create a new event.

<?php

namespace App\Events;

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

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

private $type, $user;
/*public $queue, $connection;*/
public function __construct ($type, $user) {
/*$this->queue = 'broadcastable';
$this->broadcastQueue = 'broadcastable';
$this->connection = 'beanstalkd';*/
$this->type = $type;
$this->user = $user;
}
public function broadcastAs () {
return 'activity-monitor';
}
public function broadcastQueue () {
return 'broadcastable';
}

public function broadcastWith () {
return [
'id' => $this->user->id,
'name' => $this->user->name,
'username' => $this->user->username,
'action' => ucfirst(strtolower($this->type)),
'on' => now()->toDateTimeString(),
];
}

public function broadcastOn () {
return new Channel('activities');
}
}
  • If you want an event to be broadcasted, then you should implement the ShouldBroadcast interface. (ShouldBroadcastNow will also work)
  • Don’t bother about the commented part. We’ll look into that later.
  • Whenever the event is sent, it’ll send a broadcast to a public channel named activities, which is defined in the broadcastOn method. If the method returns an empty value, no message will be broadcasted. You can also send a broadcast to an array of channels like return [ new Channel('chan-1'), new PrivateChannel('priv-chan-1')]; So, you can send more than one value from this method.
  • By default, if you make your class variables public, then those variables will be sent. Otherwise, in our example, we returned data from broadcastWith method. This should return an array of data. This method has higher priority than public properties.
  • The broadcastQueue method means, on a queue connection, if we want a different queue, the queue name should be returned from broadcastable. This has the highest priority. Other than this, you can set public properties broadcastQueue or queue. The hierarchy is as mentioned.
  • If you want your queue connection to be different rather than the default queue driver (like beanstalkd, redis), you can define a public property connection which is where the broadcast message pushed to be queued.
  • If you want your broadcast name to be a custom one, you can use broadcastAs. If the method does not exist, then the event class name will be your broadcast name.

The class introduction is over. This is all you can have in your broadcast class as a variation. Now, let’s broadcast a message.

From your auth controller, whenever a user logs in or logs out, you can use the following snippet.

event(new ActivityEvent('login', $user)); // when logs in
event(new ActivityEvent('logout', $user)); // when logs out

And in your front-end (if you have the previous front-end code compiled) add that app.js to your welcome.blade.php file and the following snippet.

<script src="{{ asset('js/app.js') }}"></script><!-- add a new script tag, and listen for the upcoming event -->
<script type="text/javascript">
// Echo is available via window.Echo, in app.js file
Echo.channel('activities')
.listen('.activity-monitor', (e) => {
console.log(e);
});
</script>
  • .channel method listens to the same channel we defined in our ActivityEvent’s broadcastOn method.
  • .listen method listens for the name we defined in our broadcastAs method. Look at the . (dot) at the beginning of the name. This means that you used a custom event name rather than the class name. If you didn’t define the broadcastAs method, .listen('ActivityEvent') could be used to listen to the broadcast. Notice the . (dot) is not available if the name is not custom.

We’re all good to go. Our front-end is listening activity-monitor on the channel activities. All we need to run the queue command because the data will be processed from the queue as the queue is a prerequisite for broadcasting.

php artisan queue:listen --queue broadcastable
  • As we defined the broadcastQueue method in our ActivityEvent class, we should listen for the event in that queue and process it.
Socket Listener for activities
  • The console.log(e) prints out the JSON object of event data. You can now play with it. Do whatever you want.

So far, we have covered the Broadcaster class and public channel listening.

Private channel

Publishing your data to a private channel from a backend service doesn’t require a lot of changes. All you need to do is to use PrivateChannel instead of Channel as we did in our previous example.

// ActivityEvent's everything is unchanged.
// except the following
public function broadcastOn () {
return new PrivateChannel('activities');
}
  • The above method now returns a channel name activities, and the type of the channel is private.
  • In this article, I mentioned that the private channel needs the user to be authenticated. As you’re using laravel-echo package, you don’t need to do anything. It’ll do everything for you. You need a validator for that channel.
  • In your routes/channels.php file, add the following snippet.
Broadcast::channel('activities', function ($user) {
return !is_null($user);
});
// Broadcast::channel('name', 'callback');
  • The name part is same as the broadcastOn's channel name. The closure takes the first parameter User $user like Laravel Gate & Policy (I wrote an article on this topic. You may find it helpful). Now, you need to decide based on the $user variable that if you want to grant permission for this user to listen for the channel. Return true or false based on your decision. This is required for private-ish channels.

Now for the updated codebase of ActivityEvent, let’s change the front-end part.

<script type="text/javascript">
// Echo is available via window.Echo, in app.js file
Echo.private('activities')
.listen('.activity-monitor', (e) => {
console.log(e);
});
</script>
  • We just need to change the .channel method to .private method. That’s all. laravel-echo and laravel-echo-server will validate the user’s eligibility for the current user.
Listens to private channel activities. Socket ID: “ggTJxC6JB_D53GhbAAFA”

If the user is unauthenticated, then laravel-echo-server logs the error for the socket id (if debug is true). And even if any data is broadcasted to that channel, the unauthenticated user will not get the data

The user is unauthorized in routes/channels.php. Thus socket id: 9QndjVzfiepCzhRzAAFB won’t get any message.
An exception is thrown and showed in debug log for laravel-echo-server for socket: 9QndjVzfiepCzhRzAAFB

You now know about private channels. That’s all for the private channel.

Presence channel

Presence channels build on the security of private channels while exposing the additional feature of awareness of who is subscribed to the channel. This makes it easy to build powerful, collaborative application features such as notifying users when another user is viewing the same page.

The event for the presence channel doesn’t change except the broadcastOn method. The method will now return new PresenceChannel('channel-name'); Other than that, there is nothing to change.

As the presence channel is a type of private channel, this also requires having authentication. To authorize the channel, we do as like a private channel. But the private channel returns true or false. But these types of channels return an array if authorized or false/null value being unauthorized.

Broadcast::channel('presence-chan-{id}', function ($user, $id) {
return $user->is_admin ? [
'id' => $user->id,
'name' => $user->name,
'username' => $user->username,
] : null;
});
// only the admins have access to this channel.
// thus returns the array of info for admin
// null for non-admin user

So, in the front-end, we’ll do like,

Echo.join(`presence-chan-${id}`)
.here((users) => {
// whenever user joined the room
// get all the users at once.
})
.joining((user) => {
// when a new user joins the room
})
.leaving((user) => {
// when an existing user leaves the room
})
.listen('.broadcast-as', function (e) {
// gets data when a new data is broadcasted
});

Check THIS CODE to understand better. That’s all for the presence channel.

Client events

Have you ever seen that in Facebook’s comment section, if someone starts to write, then it pops up someone is commenting…? When you’re connected to a private or presence channel, you can send client-side events to others. To do that you can do like the following

let channel = Echo.private(`conversation-${cid}`);
channel.listenForWhisper('typing', function (e) {
console.log(e);
}).listen('.broadcast-name', function (e) {
console.log(e);
});
// and when someone types in a few keywords
$("#messages").on('keyup', function (e) {
channel.whisper('typing', {
user: user,
typing: true
});
// in .whisper() method, after the whisper event name, you can
// send whatever data you want. And, name used in whisper method
// is a string value, you can use whatever you want
// and the same name should be used with listenForWhisper method

Extra

  • Suppose, your private or presence channel is something like new PrivateChannel('conversation-'. $conversationId); To authorize the channel you will need to do like the following,
Broadcast::channel('conversation-{id}', function ($user, $id) {
// fetch conversation, check if user is authorized
return true;
// return false;
});
// $user will be the currently authenticated user
// $id will be the value when you try
// Echo.private('conversation-100') - 100 is the id
// it'll be passed to your closure automatically
// these placeholders are like route parameters
  • Sometimes, you may want to broadcast a message based on event data. So, in your Event class, if you define a method broadcastWhen and the message will only be broadcasted if the method returns true.
// event class
public function broadcastWhen () {
return $this->user->age > 18;
}
  • If you want to broadcast immediately and don’t want to queue your broadcast class, then you can implement ShouldBroadcastNow instead of ShouldBroadcast. It’ll now queue your class.
  • Like jobs, if you want to retry your Broadcast class to be tried for a few times or set timeout, you can put two public properties for them. $tries, $timeout
  • Empty channels from broadcastOn doesn’t broadcast any message.
  • Previously mentioned, still, if you publish a message and there is no subscriber for the channel, the message will be lost.
  • In some scenarios, you may not want to broadcast a message to yourself and should get everyone else. To achieve this, you can try calling the dontBroadcastToCurrentUser method from the event’s constructor. For this, your class must use InteractsWithSockets trait.
public function __construct () {
// some data
$this->dontBroadcastToCurrentUser();
}
// this only works with AJAX, or SPA's
// SPA's use ajax - that's why.
// Behind the scene, the method blacklists sender's x-socket-id
// you can't send custom header from browser except AJAX.
  • There are several ways to broadcast an event.
// event(new ActivityEvent('logout', $user));
// dispatch(new BroadcastEvent(new ActivityEvent('login', $user)));
// broadcast((new ActivityEvent('login', $user)));
// ActivityEvent is user defined class
// BroadcastEvent is Laravel's class from Illuminate\Broadcasting
  • Just like dontBroadcastToCurrentUser, if you use thebroadcast method then you can chain toOthers method. It’s still the same. Check Illuminate/Broadcasting/PendingBroadcast.php:42 for reference.
  • Laravel-Echo package requires the CSRF token. Make sure you have token in your document’s head tag.
<meta name="csrf-token" content="{{ csrf_token() }}">
  • To use redis broadcast driver, we had to use BROADCAST_DRIVER=redis in our .env file. And predis/predis is also required to be installed.
  • If you want to use classes for channel authorization instead of closures, then you’ll have to use like
Broadcast::channel('channel-name', 'Namespace\And\Class');
// and your class must have a join method which will be called.
// class ChannelAuthorizer
// {
// public function join ($user) {
// return !is_null($user);
// }
// }

Still thirsty!!!

If you still think you can dig more, you can check the following classes.

  • event forwards call to Illuminate/Events/Dispatcher.php:203
  • dispatch forwards call to Illuminate/Bus/Dispatcher.php:70
  • broadcast forwards call to Illuminate/Broadcasting/BroadcastManager.php:101 class.
  • If you want to know how all of them work, READ THIS ARTICLE.
  • Illuminate/Broadcasting/Broadcasters/RedisBroadcaster.php is used for broadcasting messages to redis.

I hope you now know a bit about Laravel Broadcasting. I tried my best to make you understand it. Best of luck.

--

--

Syed Sirajul Islam Anik

software engineer with "Senior" tag | procrastinator | programmer | !polyglot | What else 🙄 — Open to Remote