WebSockets in Laravel
John Braun
Posted on January 26, 2020
Freek Van der Herten (from Spatie ) and Marcel Pociot (from BeyondCode ) have published a package called Laravel WebSockets which provides an alternative to services like Pusher .
In this post I want to explain how to get started with realtime broadcasting in Laravel using WebSockets. The stepwise guide will explain how to setup basic broadcasting. The first part covers using Pusher, the second part dives into switching to the Laravel WebSockets package.
When to use WebSockets?
WebSockets is a relatively young TCP protocol, finalized in 2011, which enables two-way communication between the server and a user's browser. They are nowadays commonly used in applications that want to communicate changes in realtime. Chatrooms are probably the most well known example along with online multiplayer games (quizzes), social media streams and sports tickers.
Our demo application
Let's assume our application needs to do some heavy lifting (which we will mimic using PHP's sleep()
function). For example, imagine that a user can upload a video, which then needs to undergo a time intensive post-procesing task. Since we don't want to keep our user waiting, we'll put a job on the queue to execute that work behind the scenes. In the meantime, our user will see a "waiting to be processed" status until the job is processed. Within the job, we'll emit an event upon completion that should be broadcasted to the specific user only. That should in turn update the status for that user without requiring a "hard refresh" of the webpage.
Both implementations, Pusher as well as Laravel WebSockets, require the following prerequisites:
-
Laravel Echo: a javascript library that makes it a breeze to listen to broadcasted events from our front end. You can install Laravel Echo via npm:
npm install --save laravel-echo pusher-js
. After installation, uncomment the Laravel Echo specific code in yourresources/js/bootstrap.js
file:
import Echo from 'laravel-echo'
window.Pusher = require('pusher-js');
window.Echo = new Echo({
broadcaster: 'pusher',
key: process.env.MIX_PUSHER_APP_KEY,
cluster: process.env.MIX_PUSHER_APP_CLUSTER,
encrypted: true
});
-
Redis: In this example we'll use Redis as our queue driver, which can be installed using composer:
composer require predis/predis
. Now, in the.env
file setQUEUE_CONNECTION=redis
.
Tips and tricks before starting
If you are new to broadcasting, from my experience some things might be a bit tricky at first. Therefore I wanted to dedicate a small section to hopefully clear things up.
Uncomment the BroadcastingServiceProvider
I've been tricked more than once, in debugging where things go wrong only to find out I forgot to uncomment this line in the providers array of config/app.php. Make this the first thing you do! (if you're like me).
// config/app.php:
/*
* Application Service Providers...
*/
App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class,
App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\TelescopeServiceProvider::class,
App\Providers\RouteServiceProvider::class,
],
Always recompile your assets
If you ever change the PUSHER_*
credentials in the .env
file, always recompile your assets using npm run dev
(or watch / prod). This is necessary as Laravel Echo needs them and compiles them down from your environment variables, as you can see in the instantiation of Echo in resources/js/bootstrap.js
:
({
key: process.env.MIX_PUSHER_APP_KEY,
cluster: process.env.MIX_PUSHER_APP_CLUSTER
});
Pusher
The easiest way to get started is using the Pusher implementation. We'll first require the pusher specific package using composer.
composer require pusher/pusher-php-server "~3.0"
Now, we need to get our credentials from Pusher.com . If you don't have an account yet, you first need to register. They offer a free account, which should suffice for small to medium applications. On their website, create a new app and select the cluster that's closest to you. Copy the credentials on the "App Keys" page to the .env
file:
PUSHER_APP_ID=(app_id)
PUSHER_APP_KEY=(app_key)
PUSHER_APP_SECRET=(app_secret)
PUSHER_APP_CLUSTER=(cluster)
Building a demo application
In this section, let's walk through a minimum setup to get our basic demo application working.
First, we'll scaffold the default Laravel authentication and a Video model with a migration and a controller.
php artisan make:auth
php artisan make:model Video -mc
Add a user to the Videos
table migration:
public function up()
{
Schema::create('videos', function (Blueprint $table) {
$table->bigIncrements('id');
$table->bigInteger('user_id');
$table->timestamps();
});
}
Now, perform the migrations: php artisan migrate
.
We have a post route to store a new video, and a show route which will let the user know the current status of his/her uploaded video. In routes/web.php
:
Auth::routes();
Route::get('/videos/{video}', 'VideoController@show');
Route::post('/videos', 'VideoController@store');
In the VideoController
, we'll add the corresponding methods:
// app/Http/Controllers/VideoController.php:
use App\Video;
use App\Jobs\ProcessVideo;
use Illuminate\Http\Request;
class VideoController extends Controller
{
public function store(Request $request)
{
// here we would the uploaded video from $request
// and store it along with its path
$video = Video::create([]);
// Then, pass the heavy lifting to our job
ProcessVideo::dispatch($video);
return redirect('/videos/' . $video->id);
}
public function show(Video $video)
{
return view('videos.show', compact('video'));
}
}
To reach the post route, we add a form to our welcome.blade.php:
welcome.blade.php:
<form action="/videos" method="POST">
@csrf
<!-- some upload and other form fields -->
<button>Upload some video</button>
</form>
After posting this form, the user will hit the store()
method of our controller and a ProcessVideo
job will be dispatched before the user gets redirected to the video's show page. We haven't created any of these yet, so let's start by scaffolding the ProcessVideo
job:
php artisan make:job ProcessVideo
Since we want to have a reference to the video that needs processing, accept the $video
in the constructor. In the handle()
method, the application wil sleep for 10 seconds and then emit an event called VideoWasProcessed
. Don't forget to import the Video
and VideoWasProcessed
classes.
// app/Jobs/ProcessVideo.php:
use App\Video;
use App\Events\VideoWasProcessed;
class ProcessVideo implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $video;
public function __construct(Video $video)
{
$this->video = $video;
}
public function handle()
{
sleep(10);
event(new VideoWasProcessed($this->video));
}
}
Since the job implements the ShouldQueue
interface, Laravel will always try to queue this job. Then, our Laravel queue workers (using the redis connection) in the background will make sure the job is executed. When completed (in our case after 10 seconds), the VideoWasProcessed
event will be fired.
We haven't created this event yet, so run:
php artisan make:event VideoWasProcessed
In addition to accepting the reference to the video in the constructor, we also want to implement the ShouldBroadcast
protocol. The event will not be broadcasted unless you implement the ShouldBroadcast
interface. Also, don't forget to import the Video
class.
// app/Events/VideoWasProcessed.php:
use App\Video;
class VideoWasProcessed implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $video;
public function __construct(Video $video)
{
$this->video = $video;
}
public function broadcastOn()
{
return new PrivateChannel(
"videos.{$this->video->id}"
);
}
}
The broadcastOn()
method specifies which channel to broadcast on. Since we don't want everyone to get an update on the processing status of this video we broadcast on a PrivateChannel
. Who can access to this channel is configured in the routes/channels.php
file. If the closure as defined below returns true, the user has access. If the closure returns false, the user fails authorization. We add a constraint that only allows the owner of the video to see updates in the routes/channels.php
file:
// routes/channels.php:
Broadcast::channel('videos.{id}', function ($user, $id) {
return $user->id === Video::find($id)->user_id;
});
Reflecting updates on the front end
The magic revolves around the show page of our video. For the javascript side of things, we'll levarage Vue JS. In resources/views/videos/show.blade.php
:
<html>
<head>
<title>Video's page</title>
<!-- always include your CSRF token -->
<meta name="csrf-token" content="{{ csrf_token() }}">
</head>
<body>
<div id="app">
<!-- our Vue component -->
<video-progress :video="{{ $video }}" />
</div>
<!-- include our compiled javascript -->
<script src="{{ asset('js/app.js') }}"></script>
</body>
</html>
The VideoProgress.vue
component consists of a data variable processing
which holds information on the processing status of the video. In the created()
lifecycle hook of Vue, we defined a private listener which passes through the video's ID and listens for a VideoWasProcessed
event. When it gets that event, the closure is executed and in this case the processing
data variable will be set to false, reactively updating the <span>
in our template.
resources/js/components/VideoProgress.vue
:
<template>
<div>
Your video with ID {{ video.id }} is currently:
<span style="color: red;" v-if="processing">
Processing...
</span>
<span style="color:green" v-else>
Finished!
</span>
</div>
</template>
<script>
export default {
props: ['video'],
data() {
return {
processing: true,
}
},
created() {
window.Echo.private(`videos.${this.video.id}`)
.listen("VideoWasProcessed", (e) => {
this.processing = false;
});
},
}
</script>
Lastly, we need to register our Vue component in resources/js/app.js
:
Vue.component(
'video-progress',
require('./components/VideoProgress.vue').default
);
Now, after recompilation of all assets using npm run dev
(or watch / prod), start a queue worker (php artisan queue:work
) and visit the demo app in the website.
In the welcome view, click the submit button and (if all went well) you should see the status updating from 'running' to 'finished' automatically, after the job was processed.
Switching to Laravel WebSockets
The Laravel WebSockets package makes it easy to smoothly transition from using Pusher to using your own WebSockets server.
Installation
- We first need to require the package via composer
composer require beyondcode/laravel-websockets
- Then, publish the migrations:
php artisan vendor:publish --provider="BeyondCode\LaravelWebSockets\WebSocketsServiceProvider" --tag="migrations"`
- * Publish the configuration file:
php artisan vendor:publish --provider="BeyondCode\LaravelWebSockets\WebSocketsServiceProvider" --tag="config"
Configuration
The package will use the pusher
driver, but we don't actually want to use Pusher. Therefore we add our own host
and port
configuration to 'pusher' section in config/broadcasting.php
:
'pusher' => [
'driver' => 'pusher',
'key' => env('PUSHER_APP_KEY'),
'secret' => env('PUSHER_APP_SECRET'),
'app_id' => env('PUSHER_APP_ID'),
'options' => [
'cluster' => env('PUSHER_APP_CLUSTER'),
'encrypted' => true,
'host' => '127.0.0.1',
'port' => 6001,
'scheme' => 'http'
],
],
In our bootstrap.js
file we need to tell Laravel Echo to use the alternative host and port (wsHost
, wsPort
):
import Echo from 'laravel-echo'
window.Pusher = require('pusher-js');
window.Echo = new Echo({
broadcaster: 'pusher',
key: process.env.MIX_PUSHER_APP_KEY,
wsHost: window.location.hostname,
wsPort: 6001,
disableStats: true,
});
Although we are not using Pusher, we still need to supply some Pusher configuration in our .env
file:
PUSHER_APP_ID=testapp
PUSHER_APP_KEY=websocketkey
PUSHER_APP_SECRET=somethingsecret
Compiling assets
We need to recompile bootstrap.js
, as discussed in the Tips and Tricks section, by running npm run dev
(or watch / prod).
Starting the WebSockets server
Finally, boot up the WebSockets server and start a queue worker (in a separate terminal window):
php artisan queue:work
php artisan websockets:serve
Follow the documentation to learn which other options are available to you.
Now, you should see that our demo application still works but now handling the WebSockets locally.
Running the server in the background
Just like queue workers, we don't want to run the WebSockets server ourself on our real server. The solution is the same as for our queue workers: let supervisord take care of it. To keep this post focused, please review the docs:
Dashboard
The Laravel WebSockets package even comes with a dashboard, comparable to Pusher's Debug Console. The default location of the dashboard is /laravel-websockets
from your root path of the demo application and is automagically available. Follow the documentation to learn more about the dashboard.
Conclusion
I hope that I've convinced you that it's easier than ever to get started with realtime broadcasting. Two of the many possible implementations were discussed in this post.
Pusher is a great option to get up and running fast and easy. They offer a free account which offers a maximum of 100 simultaneous connections, 200 000 messages a day and unlimited channels. For small applications, it's a no brainer and I would always choose for the convenience of Pusher.
However, if your application needs a bit more room to breathe the Laravel WebSockets package offers a great alternative which let you handle all WebSocket connections yourself and even comes with a dashboard! On top of that, switching over from Pusher to Laravel WebSockets is a breeze.
Happy broadcasting!
Posted on January 26, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.