Laravel Volt realtime live stats

post-thumb

Hey! So Livewire 3 and Volt are out. I’ve been playing around with them and they’re pretty cool. My favorite part? Definitely the long polling feature in Livewire.

Together, let’s create a Volt component that shows the live users on the app

The Concept

To implement this feature, we need a method to track active users. One option is to log active users in a database table. Each entry would have a user_id and a timestamp of when they were active. You can then check activity within the last 5 minutes or any other desired timeframe. Another way is to have a “last_login_at” column in the users table. A third possibility is using a web-sockets server to get a real-time count of active users.

For this article, I’m going to focus on using Redis. I’m fond of Redis because it’s a straightforward in-memory database, yet so powerful. I’ve used this method before, and it’s capable of scaling to handle thousands or even millions of users.

  • First, we’ll create a middleware. For every web request, this middleware will set or update a Redis key for the current user that will expire in 5 minutes. You can adjust this duration to fit your needs.
  • Next, we’ll use a Livewire volt component that refreshes every 5 seconds and counts the number of keys in Redis.

By following these steps, we can easily track the number of active users on our site.

Setting Up Volt

First, we need to install Livewire 3 and Volt. As of writing this article, both are still in beta.

1composer require livewire/livewire "^3.0@beta" # Make sure you have Livewire v3.x installed...
2composer require livewire/volt "^1.0@beta"

After the installation, run the following command to set up Volt:

1php artisan volt:install

With that done, we’re set to dive into building our feature.

Note: I’m assuming that you already have a Laravel application with a users table in the database, and you’ve configured Redis.

Tracking User Activity with Middleware

To track user activity, we’ll create a middleware that logs each unique user’s activity to Redis every time they load a web page.

First, generate the middleware:

1php artisan make:middleware LogUserActivity

Now, let’s edit the LogUserActivity middleware:

 1namespace App\Http\Middleware;
 2
 3use Closure;
 4use Illuminate\Http\Request;
 5use Illuminate\Support\Facades\Redis;
 6use Symfony\Component\HttpFoundation\Response;
 7
 8class LogUserActivity
 9{
10    public function handle(Request $request, Closure $next): Response
11    {
12        if ($request->user()) {
13            $userId = $request->user()->id;
14            $duration = 300; // This represents 5 minutes
15
16            Redis::setex("live_users:$userId", $duration, 1);
17        }
18
19        return $next($request);
20    }
21}

In this middleware, we check if there’s an authenticated user. If so, we store a key in Redis with a prefix of “live_users:” followed by the user’s ID.

Lastly, remember to add this middleware to your Kernel.php file to make it active.

 1namespace App\Http;
 2
 3class Kernel extends HttpKernel
 4{
 5	protected $middlewareGroups = [
 6    'web' => [
 7        \App\Http\Middleware\EncryptCookies::class,
 8        \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
 9        \Illuminate\Session\Middleware\StartSession::class,
10        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
11        \App\Http\Middleware\VerifyCsrfToken::class,
12        \Illuminate\Routing\Middleware\SubstituteBindings::class,
13        LogUserActivity::class,
14    ],
15
16    'api' => [
17        // \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
18        \Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
19        \Illuminate\Routing\Middleware\SubstituteBindings::class,
20    ],
21];
22}

Creating a Volt Component for Live Users

Let’s construct a Volt component to display the number of active users in real-time.

1php artisan make:volt liveusers

This command will generate a file named liveusers.blade.php inside resources/views/livewire/.

To clarify the steps, I’ll break down the code explanation into two sections: the PHP code and the HTML content.

PHP Code Within Volt

Let’s start with the initial lines of our Volt component:

 1use function Livewire\Volt\{computed};
 2use Illuminate\Support\Facades\Redis;
 3
 4$liveUsers = computed(function () {
 5    $count = 0;
 6    $cursor = null;
 7    $pattern = 'live_users:*';
 8    $batchSize = 1000;
 9
10    do {
11        list($cursor, $keys) = Redis::scan($cursor, $pattern, $batchSize);
12        $count += count($keys ?? []);
13    } while ($cursor != 0);
14
15    return $count;
16});

First, we import the necessary functions. The computed function is imported from the Livewire\Volt namespace along with the Redis facade.

We then use the computed function to fetch the active user count. This approach is chosen over a typical state method because it allows for updating the value with long polling without a full page reload.

Inside the computed callback, we utilize the scan method, a more efficient way to retrieve all keys with the ’live_users:’ prefix. This method is preferred over the keys method when dealing with large data sets, as it allows us to iterate over vast quantities of items without overloading memory.

HTML Content

and that’s the html content for our component

 1<div>
 2    <div wire:poll.5s>
 3        <h3 class="text-base font-semibold leading-6 text-gray-900">Live stats</h3>
 4
 5        <dl class="mt-5 grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3">
 6            <div class="relative overflow-hidden rounded-lg bg-white px-4 pb-12 pt-5 shadow sm:px-6 sm:pt-6">
 7                <dt>
 8                    <div class="absolute rounded-md bg-indigo-500 p-3">
 9                        <svg class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
10                             stroke="currentColor" aria-hidden="true">
11                            <path stroke-linecap="round" stroke-linejoin="round"
12                                  d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z"/>
13                        </svg>
14                    </div>
15                    <p class="ml-16 truncate text-sm font-medium text-gray-500">Live Now</p>
16                </dt>
17                <dd class="ml-16 flex items-baseline">
18                    <p class="text-2xl font-semibold text-gray-900">{{ number_format($this->liveUsers, 0) }}</p>
19                </dd>
20            </div>
21        </dl>
22    </div>
23</div>

Notice in line 2 we have wire:poll.5s to refresh the component every 5 seconds to pull the data. and line 10 gets the computed value we defined and formats it

1number_format($this->liveUsers, 0)

So the entire component would look like

 1<?php
 2
 3use function Livewire\Volt\{state, computed};
 4use Illuminate\Support\Facades\Redis;
 5
 6$liveUsers = computed(function () {
 7    $count = 0;
 8    $cursor = null;
 9    $pattern = 'live_users:*';
10
11    do {
12        list($cursor, $keys) = Redis::scan($cursor, $pattern, 1000);
13        $count += count($keys ?? []);
14    } while ($cursor != 0);
15
16    return $count;
17});
18
19
20?>
21<div>
22    <div wire:poll.5s>
23        <h3 class="text-base font-semibold leading-6 text-gray-900">Live stats</h3>
24
25        <dl class="mt-5 grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3">
26            <div class="relative overflow-hidden rounded-lg bg-white px-4 pb-12 pt-5 shadow sm:px-6 sm:pt-6">
27                <dt>
28                    <div class="absolute rounded-md bg-indigo-500 p-3">
29                        <svg class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
30                             stroke="currentColor" aria-hidden="true">
31                            <path stroke-linecap="round" stroke-linejoin="round"
32                                  d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z"/>
33                        </svg>
34                    </div>
35                    <p class="ml-16 truncate text-sm font-medium text-gray-500">Live Now</p>
36                </dt>
37                <dd class="ml-16 flex items-baseline">
38                    <p class="text-2xl font-semibold text-gray-900">{{ number_format($this->liveUsers, 0) }}</p>
39                </dd>
40            </div>
41        </dl>
42    </div>
43</div>

That’s it for the volt component

Demo Time

For a hands-on demonstration, I’ve set up a console command that simulates user activity in the background. This will randomly mimic active users, letting you see the live count update in real-time on the site.

1collect(Redis::keys('live_users:*'))->each(function ($key) {
2    Redis::del($key);
3});
4
5foreach(range(1, rand(10, 50)) as $userId) {
6    Redis::setex("live_users:$userId", 500, 1);
7}
livewire-volt-live-stat.gif

Happy coding!

comments powered by Disqus

You May Also Like