Complete guide to achieve WebSocket real-time communication with Laravel Broadcast

Mina Ayoub
10 min readFeb 14, 2018

--

Laravel integrates many out-of-the-box features, although it’s really “fat”, but it doesn’t matter if it’s a good framework. This article will use Laravel 5.6 to demonstrate from the shallower to the deeper: How to use the built-in Broadcast ( The broadcast function enables real-time communication with the client.

1. Preparation

Because PHP itself does not support WebSocket, we need a layer of indirection to send “server” data to the “client”. In other words, real-time communication can be roughly divided into two steps:

  1. “Laravel” -> “Indirect Layer”
  2. “Indirect Layer” -> (via WebSocket) -> “Client”

As for the implementation of the indirect layer, we will talk about it later.

2. Configuration

According to the above Broadcasting systemdocument, we first need to do the following configuration work.

(1)

First, modify config/broadcasting.phpor .envfile. Make sure the Broadcast Default Driver is logto turn this feature on and it's easy for us to debug.

(2)

To use broadcast broadcasts, you must understand Laravel’s Event system, they are interdependent. Next we create an event that can be "broadcast".

Modify app/Providers/EventServiceProvider.php, $listenappend within the member variable :

'App\Events\OrderShipped' => [
'App\Listeners\SendShipmentNotification',
],

We named this event here OrderShipped (the order has been settled); execution php artisan event:generate generates an event class and its listeners.

If you need to modify the event execution (that is, broadcast to the client) to asynchronous, please refer to the above Queue system document.

(3)

In order for the event to be “broadcasted”, we need to have the event class inherit the ShouldBroadcast interface.

Open app/Events/OrderShipped.php, modify the class definition as:

class OrderShipped implements ShouldBroadcast

(4)

ShouldBroadcastThe interface requires an implementation broadcastOnmethod to tell the framework which channel to which this event should be sent.

Laravel’s broadcast system allows for multiple channels to exist. You can use a username to distinguish between different channels so that different users can listen to different channels and get different messages, as well as separate communication with different clients.

Of course, you can also name the channel arbitrarily, but it is better to have certain rules.

Since we just used the event code generated by the artisan command, you can already see broadcastOnthe definition of the method at the bottom of the file . We made a slight modification:

public function broadcastOn()
{
return new Channel('orderStatus');
}

Here we name the channel: orderStatusand return. This means that when this event is broadcast, it will be broadcast to orderStatusthe channel named .

This is a “public channel” where anyone can listen to this channel and receive a broadcast message. Laravel also provides a “private channel” that can be successfully monitored after being authenticated. We will talk about it later.

(5)

By default, Laravel will use the “event name” as the “message name” of the broadcast without any data. We can add any member variable to the event class and modify the constructor to send the data to the client.

//Can add any member variable
public $id;
//Event constructor
public function __construct($id)
{
$this->id = $id;
}
//Custom broadcast message name
public function broadcastAs()
{
return 'anyName';
}

(6)

As above, we have basically established the basic mechanism of broadcasting. Next we need an interface that can “trigger events” (ie send broadcasts).

In routes/api.phpadd the following code:

Route::get('/ship', function (Request $request)
{
$id = $request->input('id');
event(new OrderShipped($id)); // trigger event
return Response::make('Order Shipped!');
});

(7)

All right! Open Postman, type: http://***/api/ship?id=1000, send.

Open storage/logs/laravel.log, you will find a few more lines:

[2018-03-02 01:41:19] local.INFO: Broadcasting [App\Events\OrderShipped] on channels [orderStatus] with payload:
{
"id": "1000",
"socket": null
}

Congratulations, you have successfully configured the logDriver for Broadcast .

3. Broadcast

In the previous section, we used logas the Broadcast Driver, which means that broadcast messages will be recorded in the log, so how do you actually communicate with the client? This requires the "indirect layer" mentioned at the beginning.

(1)

First, change the Driver logto pusher.

In order to save the steps of installing Redis, we will use the HTTP protocol to push directly to a compatible “local Pusher server” (ie, the indirect layer).

(2)

Since Laravel does not carry the Broadcast Pusher Driver, you need to install the Pusher PHP SDK using Composer:

composer require pusher/pusher-php-server

(3)

Registration App\Providers\BroadcastServiceProvider.

For Laravel 5.6, you only need to cancel the corresponding comment in the config/app.phpinner providersarray.

(4)

Next, you need to install and configure the “indirect layer” where the server communicates with the client.

We recommend the use of official documents: tlaverdure/laravel-echo-server. This is a WebSocket server implemented using Node.js + Socket.IO.

It is compatible with the Pusher HTTP API, so as above, you can directly modify the Driver to Pusher instead of Redis.

npm install -g laravel-echo-server

Initialize the configuration file and follow the prompts.

laravel-echo-server init

Open laravel-echo-server.jsonand check if some key configuration items are correct:

"authHost": "http://xxx" //Make sure you have access to your Laravel project
"port": "6001" //It is recommended not to modify, this is the port that communicates with the client.
"protocol": "http" //protocol for communicating with the client, supporting HTTPS

Copy two values: clients.appId as well clients.key, and modify config/broadcasting.php.

'pusher' => [
'driver' => 'pusher',
'key' => env('PUSHER_APP_KEY'),
'secret' => null,
'app_id' => env('PUSHER_APP_ID'),
'options' => [
'host' => 'localhost',
'port' => 6001,
],
],

As the name suggests, to appIdand keyare charged into the corresponding item or modify the configuration .envfiles.

Next, we can use the command laravel-echo-server startto start the server, listen for "broadcast" requests from Laravel, and "listen" requests from the client, and forward the corresponding broadcast message.

(5)

From here we will configure the front end section.

First, you need to configure the CSRF Token.

If you use the Blade template engine, execute the php artisan make:authcommand to join the Token resources/views/layouts/app.blade.php.

If you do not use this template file, you can directly write the Meta value directly in the first line of the Blade template file.

In order to facilitate testing, we have resources/views/welcome.blade.phpwithin add:

<meta name="csrf-token" content="{{ csrf_token() }}">

CSRF Token verification can be turned off for items separated at the front and back ends.

(6)

Second, you need to reference the client JS file for Socket.IO, in the same location.

<script src="//{{ Request::getHost() }}:6001/socket.io/socket.io.js"></script>

(7)

Here we use the official Laravel Echo extension package to listen to the server broadcast. Open resources/assets/js/bootstrap.jsand modify the last few lines.

import Echo from 'laravel-echo'
window.Echo = new Echo({
broadcaster: 'socket.io',
host: window.location.hostname + ':6001'
});

Then write the “listening channel” code, which is very simple:

window.Echo.channel(`orderStatus`) // Broadcast channel name
.listen('OrderShipped', (e) => { // Message name
console.log(e); // The operation performed by the message, the parameter e is the data carried
}
);

(8)

Install dependencies and compile the frontend files. Execute the following command:

npm install
npm install --save laravel-echo
npm run dev

Finally, reference the JS script we just wrote within the Blade template. Since the app.jsdefault is already referenced bootstrap.js, we only need to reference it app.js.

We add in the file in step (5):

<script src="{{ asset('/js/app.js') }}"></script>

(9)

All right! Before viewing the effect, don’t forget to execute the last command in step (4) to start Laravel Echo Server.

Laravel has defined the home route rendering welcome.blade.phptemplate by default , so just use the browser to access the app URL.

If you view the Chrome console, no errors are generated; check the command line window for no error output; it means that the client and server seem to have established a WebSocket connection.

At this point, you can reopen Postman and send the request from the previous section.

Looking at the two windows above again, there will be surprises.

4. Private Channel

In the previous section, we successfully implemented a “common channel” broadcast that can be listened to by anyone. How do you communicate with some clients? Just relying on front-end verification is not enough. We need to create a “private channel” with “Authentication”.

(1)

First, open app/Providers/BroadcastServiceProvider.php, we have already registered this service provider in the previous section, and now we need to uncomment some of the code.

public function boot()
{
Broadcast::routes(); // Remember the authEndpoint configuration item for laravel-echo-server.json?
require base_path('routes/channels.php');
}

Broadcast::routes()Used to register authentication routes (ie /broadcasting/auth), when the client listens to the channel, Laravel Echo Server accesses this route to verify that the client meets the "authentication" condition.

(2)

The Broadcast certification is divided into two parts:

  1. Use Laravel’s built-in Auth system for authentication.
  2. Partial channel authentication based on custom rules.

(3)

First, you need to configure the Auth authentication system.

After modifying .envthe database configuration item of the file according to the situation , you only need to execute the php artisan make:authrequired file and then php artisan migratecreate the required database structure.

For an in-depth understanding, please refer to the User Authentication documentation.

Next we open your app in the browser, you will find more login and registration in the upper right corner, we are free to register a test user for use.

(4)

Next, configure channel authentication.

Remember the step (1) routes/channels.php, we open this file. And add a channel certification rule.

Note: Although this file is located in the routesdirectory, it is not a routing file! After this definition, it is not accessible, and grouping and middleware cannot be used. All authentication routes are already Broadcast::routes()defined in it.

Broadcast::channel('orderStatus', function ($user, $value) {
return true; // or false
});

Since Broadcast has used Auth for user login authentication, we only need to return unconditionally true: any logged in user can listen to this channel.

(5)

We have configured the authentication part, and then changed the definition of the shared channel to private.

Modify the event class on which the broadcast message is based app/Events/OrderShipped.php:

public function broadcastOn()
{
return new PrivateChannel('orderStatus'); // Private channel
}

Modify the client listening code resources/assets/js/bootstrap.js.

window.Echo.private(`orderStatus`) // Private channel
.listen('OrderShipped', (e) => {
console.log(e);
});

(6)

Next, run Laravel Echo Server again. Use your browser to open your app homepage. If you are not logged in, you can see that Echo Server outputs an AccessDeniedHttpExceptionexception from Laravel , prompting the user to fail authentication and unable to listen to the channel.

Once logged in, you will get the same expected results as in the previous section.

(7)

As above, we have successfully implemented a private channel for all logged in users. So how to achieve a certain part of the user can listen, some users can not listen to the channel? For example, a user has his or her own channel and he can only listen to his own channel. please watch the following part.

First, modify the event class on which the broadcast message is based app/Events/OrderShipped.php. You need to change the channel naming to a dynamic value.

public $userId; // Add member variable userId, don't forget to assign it to the constructor
public function broadcastOn()
{
return new PrivateChannel('orderStatus-' . $this->userId); // Dynamically naming private channels
}

Second, modify the routes/channels.phpfile in step (4) . Broadcast supports the use of wildcards to match "a certain type of channel" for authentication.

Broadcast::channel('orderStatus-{userId}', function ($user, $value) {
// $user User model instance passed by Auth authentication
// $value The userId value to which the channel rule matches
return $user->id == $value; // Can use any criteria to verify that this user can listen to this channel
});

Finally, don’t forget to modify the channel name that the front end listens to.

Open the browser test again and you will find that in this example, if the channel that the client listens to does not match the current user ID, an error will be reported.

(8)

As above, we have successfully implemented custom rules for private channels; but how do we work if we don’t use the Auth authentication system or use our own user authentication middleware?

After some source debugging, I found that vendor/laravel/framework/src/Illuminate/Broadcasting/Broadcasters/the Broadcasterinternal calls defined by the folder under the folder were $request->user()authenticated by the user.

For example, we used PusherBroadcaster.php:

/**
* Authenticate the incoming request for a given channel.
*
* @param \Illuminate\Http\Request $request
* @return mixed
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
*/
public function auth($request)
{
if (Str::startsWith($request->channel_name, ['private-', 'presence-']) &&
! $request->user()) {
throw new AccessDeniedHttpException;
}
$channelName = Str::startsWith($request->channel_name, 'private-')
? Str::replaceFirst('private-', '', $request->channel_name)
: Str::replaceFirst('presence-', '', $request->channel_name);
return parent::verifyUserCanAccessChannel(
$request, $channelName
);
}

As a result, we have two ways to achieve it.

The first

Comment directly throw new AccessDeniedHttpExceptionand modify app/Providers/BroadcastServiceProvider.php.

Broadcast::routes()Can receive one parameter. You vendor/laravel/framework/src/Illuminate/Broadcasting/BroadcastManager.phpcan view its definition:

/**
* Register the routes for handling broadcast authentication and sockets.
*
* @param array|null $attributes
* @return void
*/
public function routes(array $attributes = null)
{
if ($this->app->routesAreCached()) {
return;
}
$attributes = $attributes ?: ['middleware' => ['web']];
$this->app['router']->group($attributes, function ($router) {
$router->post('/broadcasting/auth', '\\'.BroadcastController::class.'@authenticate');
});
}

According to the source code, this parameter is equivalent to Route::group()the first parameter. So we just need to modify it to the following form:

Broadcast::routes(['middleware' => ['yourMiddleware'])

And perform user authentication in the middleware; if not logged in, throw AccessDeniedHttpExceptionan exception as usual .

Second

In vendor/laravel/framework/src/Illuminate/Http/Request.phpcan view the $request->user()definition.

/**
* Get the user making the request.
*
* @param string|null $guard
* @return mixed
*/
public function user($guard = null)
{
return call_user_func($this->getUserResolver(), $guard);
}

As you can see, it uses $this->userResolverthe anonymous function inside to get the user model. So we only need to replace the AuthServiceProvider after the Broadcast authentication userResolver.

For example: inherit Illuminate\Auth\AuthServiceProvider( vendor/laravel/framework/src/Illuminate/Auth/AuthServiceProvider.php), and override the registerRequestRebindHandlermethod and constructor, add the following code.

$request->setUserResolver(function ($guard = null) use ($app) {
// Here to determine the user login status
// If you log in, please return to App\User or other user model instance
// not logged in, please return null
});

Modify config/app.php, AuthServiceProviderreplace the original service provider with the modified class.

5. Summary

At this point, you have established a basic understanding of Laravel Boardcast and successfully launched the “Broadcast System”.

In addition, Laravel 5.6 adds a new feature about Broadcast that avoids routes/channels.phpthe difficulty of maintaining numerous closures in the file.

In fact, this article is just a slap in the face. There are still many problems with deploying to production environments, such as:

  • Unlike common PHP applications, broadcasts are based on WebSocket long connections; how do you ensure communication stability and how to eliminate dead links?
  • To ensure application access speed, broadcasts are usually executed asynchronously; how do you configure event queues? How do I configure a queue to be asynchronous?
  • How do I ensure that the Laravel Echo Server process runs steadily? How to handle exceptions? Do I need to turn off Debug mode?
  • How to ensure that Laravel Echo and server connection is stable? Do you need a regular check? How to capture and fully handle all exceptions?
  • The client listens to the private channel. If it is rejected by the server, do you need to disconnect? How to give users a friendly reminder?
  • ……

Of course, these issues are not mentioned in this article, but there are already many mature and convenient solutions on the network.

--

--

Mina Ayoub

I'm enthusiastic about being part of something greater than myself and learning from more experienced people every time I meet them.