Build online presence into your Laravel app

Introduction

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.

Setup an app on Pusher

We need to sign up on Pusher and create a new app:

online-presence-laravel-create-pusher-app

Install Laravel, Pusher SDK and Echo

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

Configuration

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});

Our application

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.

Migrations

We need to add some extra fields to the default migration of the users table:

  • A field to store the status of user - online or offline
  • A field to store the token for authenticating API requests

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}

Online Status

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

broadcastOn method

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

Listening for online status

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.

Offline Status

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}

Listening for offline status

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    });

Bringing it all together

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}

Authorization

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:

online-presence-laravel-demo

Vue.js component

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>

Conclusion

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.