Laravel Real-Time Notifications
HTTP is stateless. Usually, the client asks for a URL, and therefore the server returns the data. Only a refresh of the page can load new information. Most of the time, this is often enough, but sometimes we'd like more.
Today, many tasks happen on the backend, and that we sometimes need to inform the user about it right away. So there's a use-case for triggering an action from the server instead of the client. consider messaging in a chat or notification messages that pop up on the top of your dashboard.
To achieve this, our client could ask the server every second if something new happened, otherwise you could make use of long polling. But the simplest solution is to create a new communication channel through WebSockets which works in both ways.
Today we are visiting build an application with real-time messaging. we'll use a WebSocket solution called Laravel Websockets, inbuilt PHP.
Installation
We start by creating a replacement Laravel 8 application. I always recommend using the Laravel Installer for this purpose.
To achieve our goal of sending real-time notifications, we'd like to make three parts work together:
- Sending/broadcasting notifications from our Laravel application.
- A WebSockets server that receives those notifications.
- Our front-end that listens to the WebSockets server.
Let's start with the WebSockets server.
Installing Laravel Websockets
Require the Laravel Websockets package. It works as a replacement for external services like Pusher. Many settings will refer to Pusher today but be reminded that we are not using it. We want our own solution.
composer require beyondcode/laravel-websockets
We also need a package by Pusher.
composer require pusher/pusher-php-server
Next, adapt your .env file. We want the BROADCAST_DRIVER to be pusher.
BROADCAST_DRIVER=pusher
Note: Again I want to mention that we do not use the Pusher service. Our websockets server just has the same API.
And we need to set the Pusher credentials.
PUSHER_APP_ID=12345
PUSHER_APP_KEY=12345
PUSHER_APP_SECRET=12345
PUSHER_APP_CLUSTER=mt1
We can define all those values ourselves. We are not actually using Pusher, but the package uses these keys, so it makes replacing Pusher (if you use it) as simple as possible.
Note: Make sure to use more random values when using in production.
The Laravel Websockets package comes with a migration file for storing statistics and a config file we need to adapt. Let's publish them.
php artisan vendor:publish --provider="BeyondCode\LaravelWebSockets\WebSocketsServiceProvider" --tag="migrations"
This will create a new migration file that we can run. Make sure you have set up a database for this project and defined the DB credentials in the .env file. Afterward, we can run the migration.
php artisan migrate
And here, we publish the config file of Laravel Websockets.
php artisan vendor:publish --provider="BeyondCode\LaravelWebSockets\WebSocketsServiceProvider" --tag="config"
Now we are ready to start the WebSockets server.
php artisan websockets:serve
To test that it is running, we can check the debugging dashboard under the endpoint /laravel-websockets. You can click connect to see if the dashboard can connect to the WebSockets server.
After clicking connect
, you should see that the dashboard is subscribed to some debugging channels like private-websockets-dashboard-api-message
. This will tell you that the server is set up correctly.
Broadcast Notifications From Our Laravel Application
There are two ways we can send messages from our backend to the WebSockets server:
- Laravel Events
- Laravel Notifications
We will start with events because this is a little easier. Later we will check notifications as well.
Let's create a new event with artisan.
php artisan make:event RealTimeMessage
Here is what we need to change:
- use the
ShouldBroadcast
interface - add a message property which we will pass through the constructor
- return a new
Channel
instead of aPrivateChannel
- give the channel the name
events
instead ofchannel-name
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Queue\SerializesModels;
class RealTimeMessage implements ShouldBroadcast
{
use SerializesModels;
public string $message;
public function __construct(string $message)
{
$this->message = $message;
}
public function broadcastOn(): Channel
{
return new Channel('events');
}
}
Before we can try sending this event, please adapt your broadcasting.php
config file to use the following options:
'options' => [
'cluster' => env('PUSHER_APP_CLUSTER'),
'encrypted' => false,
'host' => '127.0.0.1',
'port' => 6001,
'scheme' => 'http'
],
With these options, we make sure that when we broadcast
something from our Laravel application, it gets sent to our WebSockets server.
Now we can trigger our created event RealTimeMessage
with the global event
helper. I recommend using tinker
or Tinkerwell
to do that for our demo, but you could also create a route and run the command there.
event(new App\Events\RealTimeMessage('Hello World'));
Of course, in a real application, you would run this inside a controller or action class. After running this command, you should see a new entry on the debug dashboard.
Listen To Messages From Our Front-end
We have made sure that our sent event is broadcasted to the WebSockets server. But now we want to listen to it so that we can use the message on our front-end. We are going to use the JavaScript library Laravel Echo
for that purpose.
Let's start by installing it together with the pusher library, which we will need as well.
npm install --save-dev laravel-echo pusher-js
The resouces/js/bootstrap.js
file of Laravel already contains a code snippet for creating a new instance of Laravel Echo we can use. Comment it in and add the wsHost
and wsPort
.
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,
forceTLS: false,
wsHost: window.location.hostname,
wsPort: 6001,
});
The bootstrap.js
file is required by Laravel's main resources/js/app.js
file. This means we need to run npm run dev
to bundle our Echo code with the main JS file.
Now import the script. We are going to use Laravel's welcome
view for our tutorial. So add the app.js
file into your view, right before the end of the body tag.
<script src="{{ asset('js/app.js') }}"></script>
Also, open a new script tag where we create our Echo listener. We listen to the channel events
and for the class name of the event we created. If we receive an update, we write a message to the console.
<script>
Echo.channel('events')
.listen('RealTimeMessage', (e) => console.log('RealTimeMessage: ' + e.message));
</script>
When you refresh the browser's welcome page, you should also get an update on the debug dashboard.
It shows that we are successfully subscribed to the events
channel. This means we are ready to give it a real try!
Sending Real-Time Messages
Now that we prepared our event, the WebSockets server, and our JavaScript listener, we can give a real try. So back inside tinker
or Tinkerwell
, we can trigger our event again.
event(new App\Events\RealTimeMessage('Hello World'));
Now we not only see the message in the debug dashboard but also in the console output. So we were able to receive data on our front-end, without refreshing the page or making an ajax call. This is our first real-time message. 🥳
Let's Talk Privately
Next to sending messages to a public
channel, we can also use a private
one with events. Let's change our RealTimeMessage
event to use a private channel.
public function broadcastOn(): Channel
{
return new PrivateChannel('events');
}
Now send the event again.
As you can see in the debug dashboard, our message was now sent on a private channel
. Nice! But now we also need to listen to a private channel. Luckily, Laravel Echo lets us also change the channel
method to private
.
Echo.private('events')
.listen('RealTimeMessage', (e) => console.log('Private RealTimeMessage: ' + e.message));
But when you refresh the welcome page, you see that we get an error.
The problem here is that private channels need to be authenticated. That's what Laravel Echo is trying to do by requesting the /broadcasting/auth
endpoint. But the endpoint is not defined. We need to include it by uncommenting the BroadcastServericeProvider
in our app.php
config file.
/*
* Application Service Providers...
*/
App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class,
App\Providers\BroadcastServiceProvider::class, // We enabled this class
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
Refresh the page again, and you will see the error from before is gone, but of course, there is a new one :-)
The endpoint is now defined, but it returns a 419
error. This one is about the csrf
token. We need it to verify this request. Here we don't have a form where we usually use it, but we can add it to our HTML head in a meta tag.
<meta name="csrf-token" content="{{ csrf_token() }}">
Again we were able to solve a problem, and also we see a new error. But trust me, we are getting close now :-)
This time we received a 403
error telling us we are not allowed to listen to the private channel. This is good because we haven't told Laravel yet who is allowed to listen to this private channel.
Next to the broadcast routes, the BroadcastServiceProvider also activates the routes/channels.php
file. Here is where we define who is allowed to access a private channel. There is already an example, but we add our own channel check now.
Broadcast::channel('events', function ($user) {
return true;
});
We tell Laravel that we do not have specific rules for authenticating who can subscribe to our private channel. But when you refresh the welcome page, you will still see a 403 error. This is because one thing is the same for every private channel: There must be a logged-in user
.
So let's add a new user to the database. I'm using Tinkerwell again here. We do not care about the user details.
User::create([
'name' => 'Test user',
'email' => 'test@test.at',
'password' => bcrypt('test'),
]);
We can log this user into our application before we return the welcome view in the web.php
route file. Also, make sure to import the User class before you use it.
Route::get('/', function () {
auth()->login(User::first());
return view('welcome');
});
This will log in the first user from our users table. Reload the welcome page, and you will no longer find an error in the console. You will also see that we are now subscribed to the private events channel in the debug dashboard.
Trigger the event once again, and we will receive the private message output in the console.
event(new App\Events\RealTimeMessage('Hello World'));
Notifications
The title of this blog contains Notifications
, so what about them? You might know I'm a big fan of Laravel's notification system, so for sure, we are talking about them too.
So next to events, we can use notifications to send data to our WebSockets server. So let's create a new one.
php artisan make:notification RealTimeNotification
Here is what we need to change:
- use the
ShouldBroadcast
interface - add a message property which we will pass through the constructor
- use the
broadcast
channel in thevia
method - add a
toBroadcast
method to define the message
<?php
namespace App\Notifications;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Notifications\Messages\BroadcastMessage;
use Illuminate\Notifications\Notification;
class RealTimeNotification extends Notification implements ShouldBroadcast
{
public string $message;
public function __construct(string $message)
{
$this->message = $message;
}
public function via($notifiable): array
{
return ['broadcast'];
}
public function toBroadcast($notifiable): BroadcastMessage
{
return new BroadcastMessage([
'message' => "$this->message (User $notifiable->id)"
]);
}
}
Since notifications are always connected to a notifiable
model (like a user), we have access to it inside the toBroadcast
method. This way I can add the user id to the message we sent.
Alright, time to trigger our notification. Again, we are using the one user we have in our database.
$user = User::first();
$user->notify(new App\Notifications\RealTimeNotification('Hello World'));
You probably noticed that we did not define a channel name with our notification as we did in our event. This is because there is a default pattern for the channel name of a notification notifiable-class.key
. In our case, this would be App.Models.User.1
. And when you take a look at the Web dashboard, you find a message triggered by our notification to the channel Channel: private-App.Models.User.1
.
Next, let's subscribe to this channel in our front-end. Laravel echo has a notification
method we can make use of.
Echo.private('App.Models.User.1')
.notification((notification) => {
console.log(notification.message);
});
You should be able to refresh the welcome page now without seeing any errors. Since we are using a private channel again, Laravel Echo will try to authenticate our subscribing request to this channel. This is where the other code-snippet from the channels.php
file comes into play.
Broadcast::channel('App.Models.User.{id}', function ($user, $id) {
return (int) $user->id === (int) $id;
});
It will make sure when you try to subscribe to a specific user's private channel that the provided id
is the same as the logged-in user
. You can easily force an error when you try to listen to a different user id like:
Echo.private('App.Models.User.99')
.notification((notification) => {
console.log(notification.message);
});
When you refresh the welcome page, you will see that we get a 403
error again. But now, let's finish this example by triggering the notification also now.
And this is how we can broadcast real-time messages with notifications as well. The benefit of notifications is that you can send them through multiple channels. So you can send a real-time message to the user's dashboard about a newly created invoice and send an email while only using one notification class.
More Tips & Tricks
SSL Support
Of course, you will use a secure connection with your live applications. Our example will work there, too, with some additions. Therefore, I have created a new branch called feature/useTls. These settings helped me to make this app work on a secure locally Laravel Valet site.
I saw many users struggling with the SSL support setup with Laravel Websockets and Laravel Echo, and I have to admit it can be tricky because of different SSL settings. I hope my branch will give you a good start. Additionally, here are the docs for SSL support for Laravel Websockets.
Events Or Notifications
As I showed you, both options give you a lot of flexibility when sending real-time messages. I'd recommend notifications if you already work with logged-in users in your application.
If you do not work with users/notifiables in your application, events give you an excellent way of sending messages to a channel you can define.
Conclusion
I hope this long article could give you a great start into real-time messaging with Laravel. Am I missing more details? What are your thoughts on Laravel and real-time messaging? Let me know on Twitter.
As always, make sure to check the official docs for all the tools we used today:
All the code for our demo application can be found in this repository.
Comments