Display all online users in Laravel
Sep 11, 2020 12:30 PM (3 months ago)

# Display all online users in Laravel

We are working on re-building the Admin Dashboard at Viewber (opens new window).

One of the requirements is to display all the logged in admins on a top bar on top of the page. There are many ways to achieve this but my aim was to implement something simple (no WebSockets) and to reduce the number of DB calls to a minimum.

Reading online about how to implement this, many will suggest to have a last_activity_at field on the user table and update that through a middleware anytime an user visits a route.

It is definitely possible to do so but it would be a bit too heavy on the Database IMO. Especially when you'll want to retrieve all the users who had the last_activity_at column updated in the last 5 minutes, at every page refresh. Imagine running that query on a 100k+ users DB, at every user click. Definitely a no go!

# Implementation 🔥

The requirement is to have all users whom have interacted with the system in the last 10 minutes showing on a top bar. We would also like to know if those users have stopped clicking or navigating since more than 3 minutes, which may indicate they are away and consequently we'd like to display their away status, as you can see below.

Online Users

The idea is to create a middleware and store all active users on the Cache instead of the DB.

Since it's possible to store arrays in the cache, then we can create an array for each user who gets added with the last active and the id. Once you have those information for each one of the users, you can actually do the time comparison though the code, and keep updating a small array in the cache which contains only the last activity for each user.

See below the handle() method of the middleware.

    public function handle($request, Closure $next)
    {
        // This works only if users are logged in
        if(Auth::check()) {
            // Get the array of users from the cache
            $users = Cache::get('online-users');
            // If it's empty create it with the user who triggered this middleware call
            if(empty($users)) {
                Cache::put('online-users', [['id' => Auth::user()->id, 'last_activity_at' => now()]], now()->addMinutes(10));
            } else {
                // Otherwise iterate over the users stored in the cache array
                foreach ($users as $key => $user) {

                    // If the current iteration matches the logged in user, unset it because it's old
                    // and we want only the last user interaction to be stored (and we'll store it below)
                    if($user['id'] === Auth::user()->id) {
                        unset($users[$key]);
                        continue;
                    }

                    // If the user's last activity was more than 10 minutes ago remove it
                    if ($user['last_activity_at'] < now()->subMinutes(10)) {
                        unset($users[$key]);
                        continue;
                    }
                }
                
                // Add this last activity to the cache array
                $users[] = ['id' => Auth::user()->id, 'last_activity_at' => now()];
                
                // Put this array in the cache
                Cache::put('online-users', $users, now()->addMinutes(10));
            }
        }
        return $next($request);
    }

Now make sure you add this Middleware you just created in app/Http/Kernel.php in the $middlewareGroups array, under the web or api key, depending on your project, so that at every page click or api call, the cache array is refreshed.

The next step is to create a method in a controller that is called at every page click or refresh, and it returns the list of online users with all the relevant details.

The code looks like this 👇🏻.

    public function onlineUsers() {
        // Get the array of users
        $users = Cache::get('online-users');
        if(!$users) return null;
        
        // Add the array to a collection so you can pluck the IDs
        $onlineUsers = collect($users);
        // Get all users by ID from the DB (1 very quick query)
        $dbUsers = User::find($onlineUsers->pluck('id')->toArray());
        
        // Prepare the return array
        $displayUsers = [];

        // Iterate over the retrieved DB users
        foreach ($dbUsers as $user){
            // Get the same user as this iteration from the cache
            // so that we can check the last activity.
            // firstWhere() is a Laravel collection method.
            $onlineUser = $onlineUsers->firstWhere('id', $user['id']) ;
            // Append the data to the return array
            $displayUsers[] = [
                'id' => $user->id,
                'first_name' => $user->first_name,
                'last_name' => $user->last_name,
                'photo' => $user->photo,
                // This Bool operation below, checks if the last activity
                // is older than 3 minutes and returns true or false,
                // so that if it's true you can change the status color to orange.
                'away' => $onlineUser['last_activity_at'] < now()->subMinutes(3),
            ];
        }
        return collect($displayUsers);
    }

The beauty of this implementation is that it uses only 1 DB call and pushes all the heavy lifting to the cache which is much quicker and cheaper than the DB.