Laravel has out-of-the-box (well, almost out-of-the-box) support for sending messages over websockets, which can be quite useful. For example, when the frontend asks the server to perform a long-running operation, and we can't, or don't want to, wait for it to finish. But knowing when it finishes — that we want very much. For which we need to get feedback from the server. In the Paleolithic era, every client polled the server once a second until it got a response (or until the server was completely bewildered). But now we have websockets, and things are easier. You can send the result of a deferred operation from the server, you can make notifications about other users' actions, or even just build a little chat. You can read about it in the Laravel Broadcasting documentation.
What's especially nice is that this broadcasting is so neatly designed that, once you've set it up, all you need is to add an implementation of the broadcastOn method to an Event class to specify which channels the message should fly to:
class PDFGenerated implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(public Document $document, public $source_id) {}
public function broadcastOn(): array
{
return [
new PrivateChannel('orders.' . $this->document->order_id),
];
}
}
and after that any call to PDFGenerated::dispatch($document, $source_id) will automatically send the event via websockets to the right channel.
And all it takes is writing:
this.channel = window.Echo.private('orders.' + order.id)
.listen('PDFGenerated', (e) => {
this.insertDocument(e.document);
});
to subscribe to the orders.id channel and react to the PDFGenerated event, which carries the data along with it. And this code will work for Vue, for React, and for plain JS.
All in all, using broadcasting is much simpler and faster than even setting it up. So once you've set it up, you immediately want to go wild. But first you still have to set it up, and that's what this post is about. Because the configuration of broadcasting and of the Reverb server specifically is split across two pages, and some nuances aren't mentioned there.
So, I have a local Docker container with LAMP inside. Let's set it up to work with Laravel Reverb (a free server implementing the push protocol for websocket exchanges). The Laravel docs have instructions, but below we'll look at some points in more detail.
First, of course, we need to install everything. For the backend:
php artisan install:broadcasting --reverb
for the frontend:
npm install --save-dev laravel-echo pusher-js
Then go to resources/js/bootstrap.js and paste this in (if the auto-installer didn't):
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Pusher = Pusher;
window.Echo = new Echo({
broadcaster: 'reverb',
key: import.meta.env.VITE_REVERB_APP_KEY,
wsHost: import.meta.env.VITE_REVERB_HOST,
wsPort: import.meta.env.VITE_REVERB_PORT ?? 80,
wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
enabledTransports: ['ws', 'wss'],
});
and build the frontend with npm run build. The values of VITE_REVERB_APP_KEY and the rest will be taken from your .env file. Note that if you're using Mix rather than Vite, the .env data is passed through a bit differently:
window.Echo = new Echo({
broadcaster: 'reverb',
key: process.env.MIX_REVERB_APP_KEY,
wsHost: process.env.MIX_REVERB_HOST,
wsPort: process.env.MIX_REVERB_PORT,
wssPort: process.env.MIX_REVERB_PORT,
forceTLS: (process.env.MIX_REVERB_SCHEME ?? 'https') === 'https',
enabledTransports: ['ws', 'wss'],
});
that's exactly my case, since the project has gone all the way from Laravel 5 to Laravel 12, and I didn't switch from Mix to Vite along the way.
Now let's look at the .env itself:
REVERB_APP_ID=123456
REVERB_APP_KEY=abcdefghijklmnopqrstu
REVERB_APP_SECRET=vwxyz1234567890
REVERB_HOST=172.17.0.2
REVERB_PORT=443
REVERB_SCHEME=https
REVERB_SERVER_PORT=0.0.0.0
REVERB_SERVER_PORT=8090
REVERB_CLIENT_VERIFY_SSL=false
# для передачи во фронт
MIX_REVERB_APP_KEY="${REVERB_APP_KEY}"
MIX_REVERB_HOST="${REVERB_HOST}"
MIX_REVERB_PORT="${REVERB_PORT}"
MIX_REVERB_SCHEME="${REVERB_SCHEME}"
The first three variables were most likely already generated for you by the installer. But when running in production, it's worth regenerating them separately, for example like this:
echo "REVERB_APP_ID=$(openssl rand -hex 8)" >> .env
echo "REVERB_APP_KEY=$(openssl rand -hex 16)" >> .env
echo "REVERB_APP_SECRET=$(openssl rand -hex 32)" >> .env
Next, REVERB_HOST and REVERB_PORT are the host and port the client browser will connect to. For local development, for me it's the IP of the Docker container (172.17.0.2), but it can also be a host name if the container is named. And for production, it's your domain. The port, accordingly, is 443 for https and 80 for http (see below).
Next you need to specify the protocol, http or https, and here it's simple: if you use https with self-signed certificates in local development (and I always do), always specify https. Otherwise the browser's security policy won't let the connection happen.
Next we specify REVERB_SERVER_HOST and REVERB_SERVER_PORT. These are the host and port on which Reverb itself runs on the server. Your web server must forward the websocket connection to this host and port.
Note that I have port 8090. By default Reverb uses 8080, but the same port is used by default by the nginx+apache combo that forwards incoming http and https connections to Apache's port 8080. You can end up with this configuration on a server automatically if, for example, you install the ispmanager panel.
Of course, websocket connection forwarding still needs to be set up. If you have nginx at the front, add this to the config:
location /app {
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header Scheme $scheme;
proxy_set_header SERVER_PORT $server_port;
proxy_set_header REMOTE_ADDR $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_pass http://127.0.0.1:8090;
}
/app is the default path on which Reverb accepts connections.
If, on the other hand, you have Apache at the front (as in many of my local project setups), you can add this for forwarding:
ProxyPreserveHost On
RewriteEngine On
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
RewriteRule /app/(.*) ws://localhost:8090/app/$1 [P,L]
ProxyPass /app http://localhost:8090/app
ProxyPassReverse /app http://localhost:8090/app
Finally, in .env we see this variable: REVERB_CLIENT_VERIFY_SSL=false. This is my own addition, useful for local development. As I said, locally I always use https with self-signed certificates. But the Guzzle library, which is used to establish the connection, doesn't allow such liberties by default. So to make it work, you need to disable SSL verification in the library. But only locally, so in the config/broadcasting.php config we add client_options — these parameters are passed to the Guzzle Client, and the default true keeps SSL verification in production:
'reverb' => [
'driver' => 'reverb',
'key' => env('REVERB_APP_KEY'),
'secret' => env('REVERB_APP_SECRET'),
'app_id' => env('REVERB_APP_ID'),
'options' => [
'host' => env('REVERB_HOST', 'localhost'),
'port' => env('REVERB_PORT', 8080),
'scheme' => env('REVERB_SCHEME', 'http'),
'useTLS' => env('REVERB_SCHEME', 'https') === 'https',
],
'client_options' => [
'verify' => env('REVERB_CLIENT_VERIFY_SSL', true),
],
],
And now we can restart the server (so the port forwarding settings kick in) and start Reverb:
php artisan reverb:start --debug
locally, the --debug flag lets you see all messages in the console. Don't forget that event broadcasting goes through the queue, so you must either have it configured, or set QUEUE_DRIVER=sync to run jobs synchronously. And in production, run Reverb without the --debug flag, but via Supervisor. And don't forget to restart it with reverb:restart if you've changed the code.
What other nuances should you keep in mind.
First, when we build the frontend, our variables like MIX_REVERB_APP_KEY (or VITE_REVERB_APP_KEY) are embedded into the scripts at build time. So if we build the frontend for production locally, rather than in production or in a CI/CD pipeline, we must remember to specify the production environment variables during the build. Otherwise the frontend will try to connect using the local settings.
And second, if you write tests, then as soon as you set up broadcasting, calling MyEvent::dispatch() during tests for an event that is supposed to be sent to the specified channels will cause an error. So in this situation it makes sense to add mocking with Event::fake() followed by Event::assertDispatched() to check that the event was dispatched. For example, like this:
#[Test]
public function when_pdf_is_generated_it_dispatches_pdf_generated_event()
{
$document = Document::factory()->create([
'path' => 'documents/contracts/generic.docx',
]);
Event::fake();
$response = $this->json('POST', '/api/documents/' . $document->id . '/pdf');
$response->assertStatus(200);
Event::assertDispatched(function (PDFGenerated $event) use ($document) {
return ($document->name == $event->document->name) &&
(str_replace('.docx', '.pdf', $document->original_name) == $event->document->original_name) &&
(str_replace('.docx', '.pdf', $document->path) == $event->document->path) &&
('application/pdf' == $event->document->mime_type);
});
}
And if you don't write tests, then why, exactly, don't you write them? Time to start.