Realtime status update, particularly without polling our application for any updates, has always been a challenge. To that end, Laravel released its 5.4 version recently with a new broadcasting library: Echo.
Instant messaging is the minimum feature requirement in any social network. With Laravel Echo and its broadcasting features, it is possible to implement features with realtime status updates like online or offline in matter of a few minutes.
We need to sign up on Pusher and create a new app:
First, we will grab a fresh copy of Laravel:
laravel new user-status-update-laravel-pusher
This will install the latest version of the Laravel framework and download the necessary dependencies. Next, we will install the Pusher PHP SDK using Composer:
composer require pusher/pusher-php-server
Next, we will install the JavaScript dependencies:
npm install
Now, we need to install two JavaScript libraries necessary for realtime event broadcasting: Laravel Echo and Pusher JS
npm install --save laravel-echo pusher-js
We require some form of user authentication mechanism to demonstrate the functionality. Let us use the default authentication scaffolding provided by Laravel:
php artisan make:auth
First, we need to set the APP_ID
, APP_KEY
, APP_SECRET
and APP_CLUSTER
in the environment file. We can get these details in our Pusher app dashboard:
1# .env 2 3BROADCAST_DRIVER=pusher 4 5PUSHER_APP_ID=your-pusher-app-id 6PUSHER_APP_KEY=your-pusher-app-key 7PUSHER_APP_SECRET=your-pusher-app-secret 8PUSHER_APP_CLUSTER=your-pusher-app-cluster
Next, we need to create a fresh Echo instance in our applications's JavaScript. We can do so at the bottom of our resources/assets/js/bootstrap.js
file:
1import Echo from "laravel-echo" 2 3window.Echo = new Echo({ 4 broadcaster: 'pusher', 5 key: 'your-pusher-app-key', 6 cluster: 'ap2', 7 encrypted: true 8});
We will implement a feature where a user can see whether their friend is online or offline (similar to what WhatsApp does) and update the status of a user in realtime. We will concentrate on the code necessary for implementing the status update feature in realtime - we will not cover anything relating to implementing a chat functionality using Laravel. The code is available on a Github repository for cloning and understanding purposes.
We need to add some extra fields to the default migration of the users
table:
online
or offline
Below is the migration for our users
table:
1use Illuminate\Support\Facades\Schema; 2use Illuminate\Database\Schema\Blueprint; 3use Illuminate\Database\Migrations\Migration; 4 5class CreateUsersTable extends Migration 6{ 7 /** 8 * Run the migrations. 9 * 10 * @return void 11 */ 12 public function up() 13 { 14 Schema::create('users', function (Blueprint $table) { 15 $table->increments('id'); 16 $table->string('name'); 17 $table->string('email')->unique(); 18 $table->string('password'); 19 $table->string('api_token'); 20 $table->string('status')->default('online'); 21 $table->rememberToken(); 22 $table->timestamps(); 23 }); 24 } 25 26 /** 27 * Reverse the migrations. 28 * 29 * @return void 30 */ 31 public function down() 32 { 33 Schema::dropIfExists('users'); 34 } 35}
Whenever a user is active on the page, we subscribe to the chat
channel and assume that a user is online. In the joining
callback, we trigger an API request to update the user's status to online:
1Echo.join('chat') 2 .joining((user) => { 3 axios.put('/api/user/'+ user.id +'/online?api_token=' + user.api_token, {}); 4 });
join
is used when we want to join a presence channel. Presence channels are automatically private channels. We do need to authenticate them.
joining
will be executed whenever a new user joins the chat
channel.
Next, we broadcast the UserOnline
event:
1# api.php 2 3Route::middleware('auth:api')->put('user/{user}/online', 'UserOnlineController'); 4 5# UserOnlineController.php 6 7namespace App\Http\Controllers; 8 9use App\User; 10use App\Events\UserOnline; 11use Illuminate\Http\Request; 12 13class UserOnlineController extends Controller 14{ 15 public function __invoke(User $user) 16 { 17 $user->status = 'online'; 18 $user->save(); 19 20 broadcast(new UserOnline($user)); 21 } 22}
Whenever a user comes online, we need to fire an event which will be broadcast over Pusher to a specific presence channel. For broadcasting an event, it should implement the ShouldBroadcast
interface. Let us first create the UserOnline
event:
php artisan make:event UserOnline
The event should implement a broadcastOn
method. This method should return the presence channel to which the event should be broadcast. All public properties in the event will be automatically broadcast to every user listening on the chat
channel.
1namespace App\Events; 2 3use App\User; 4use Illuminate\Broadcasting\Channel; 5use Illuminate\Broadcasting\InteractsWithSockets; 6use Illuminate\Broadcasting\PresenceChannel; 7use Illuminate\Broadcasting\PrivateChannel; 8use Illuminate\Contracts\Broadcasting\ShouldBroadcast; 9use Illuminate\Foundation\Events\Dispatchable; 10use Illuminate\Queue\SerializesModels; 11 12class UserOnline implements ShouldBroadcast 13{ 14 use Dispatchable, InteractsWithSockets, SerializesModels; 15 16 public $user; 17 18 public function __construct(User $user) 19 { 20 $this->user = $user; 21 } 22 23 public function broadcastOn() 24 { 25 return new PresenceChannel('chat'); 26 } 27}
Next, we need to start our queue to actually listen for jobs and broadcast any events that are recorded. We can use the database queue listener on our local environment:
php artisan queue:listen
In our JavaScript, we need to listen to the UserOnline
event to update the status in realtime:
1Echo.join('chat') 2 .listen('UserOnline', (e) => { 3 this.friend = e.user; 4 })
Now, whenever a user joins the chat
channel, the status of user will be updated in the database and be broadcast over Pusher to other friends present on the same channel.
Similarly, whenever a user leaves the chat
channel, we will update the user's status to offline:
1Echo.join('chat') 2 .leaving((user) => { 3 axios.put('/api/user/'+ user.id +'/offline?api_token=' + user.api_token, {}); 4 })
Next, we update the status in the database and broadcast the UserOffline
event:
1# api.php 2Route::middleware('auth:api')->put('user/{user}/offline', 'UserOfflineController'); 3 4# UserOfflineController.php 5 6namespace App\Http\Controllers; 7 8use App\Events\UserOffline; 9use App\User; 10use Illuminate\Http\Request; 11 12class UserOfflineController extends Controller 13{ 14 public function __invoke(User $user) 15 { 16 $user->status = 'offline'; 17 $user->save(); 18 19 broadcast(new UserOffline($user)); 20 } 21}
Let us now create the UserOffline
event:
php artisan make:event UserOffline
Below is our UserOffline.php
event:
1namespace App\Events; 2 3use App\User; 4use Illuminate\Broadcasting\Channel; 5use Illuminate\Broadcasting\InteractsWithSockets; 6use Illuminate\Broadcasting\PresenceChannel; 7use Illuminate\Broadcasting\PrivateChannel; 8use Illuminate\Contracts\Broadcasting\ShouldBroadcast; 9use Illuminate\Foundation\Events\Dispatchable; 10use Illuminate\Queue\SerializesModels; 11 12class UserOffline implements ShouldBroadcast 13{ 14 use Dispatchable, InteractsWithSockets, SerializesModels; 15 16 public $user; 17 18 public function __construct(User $user) 19 { 20 $this->user = $user; 21 } 22 23 public function broadcastOn() 24 { 25 return new PresenceChannel('chat'); 26 } 27}
We need to listen to the UserOffline
event to update the status in realtime:
1Echo.join('chat') 2 .listen('UserOffline', (e) => { 3 this.friend = e.user; 4 });
Below is our Example
component which handles the realtime update of online and offline status of a user we are chatting with:
1props: ['user', 'user2'], 2data() { 3 return { 4 friend: this.user2 5 } 6}, 7mounted() { 8 this.listen(); 9}, 10methods: { 11 listen() { 12 Echo.join('chat') 13 .joining((user) => { 14 axios.put('/api/user/'+ user.id +'/online?api_token=' + user.api_token, {}); 15 }) 16 .leaving((user) => { 17 axios.put('/api/user/'+ user.id +'/offline?api_token=' + user.api_token, {}); 18 }) 19 .listen('UserOnline', (e) => { 20 this.friend = e.user; 21 }) 22 .listen('UserOffline', (e) => { 23 this.friend = e.user; 24 }); 25 }, 26}
Every presence channel is a private channel. Laravel Echo will automatically call the specified authentication route. However, we still need to write the authentication logic which will actually authorize the user to listen to a particular channel.
Authorization logic is written in the routes/channels.php
. The authorization logic for our chat
channel is:
1Broadcast::channel('chat', function ($user) { 2 return $user; 3});
We are not going to return true
or false
. If the user is authenticated to listen on this presence channel, we will return an array of data that we want to be returned to that callback in the listen
method.
We need to write the actual authorization logic which will check whether the user is allowed to access the specified chat room. If we don't write proper authorization logic, then anyone can listen to a chat
channel and hijack our conversations.
The friend's status (online or offline) is shown at the top of the chat box in realtime as demonstrated below:
That's it! Now, the user's status will be broadcast and we can listen using our presence channel in realtime without the need to constantly poll our application.
Below is the JavaScript part of our Example component written using Vue.js
1Status - {{ friend.status }} 2<input type="text" @keyup="whisper()" />
1<script> 2 export default { 3 props: ['user', 'user2'], 4 data() { 5 return { 6 friend: this.user2 7 } 8 }, 9 mounted() { 10 this.listen(); 11 }, 12 methods: { 13 listen() { 14 Echo.join('chat') 15 .joining((user) => { 16 axios.put('/api/user/'+ user.id +'/online?api_token=' + user.api_token, {}); 17 }) 18 .leaving((user) => { 19 axios.put('/api/user/'+ user.id +'/offline?api_token=' + user.api_token, {}); 20 }) 21 .listen('UserOnline', (e) => { 22 this.friend = e.user; 23 }) 24 .listen('UserOffline', (e) => { 25 this.friend = e.user; 26 }); 27 } 28 } 29 } 30</script>
In this article, we have covered how to update the user's status in realtime using Laravel and Pusher. We have covered the configuration options necessary to get started, and the example above should help you fill in the gaps and give an overview of some of the other configuration options available to you.