Back to Course |
Laravel Reverb: Four "Live" Practical Examples

Dashboard with Real-Time Data Refresh

Our clients love dashboards with charts and tables, but it's even better if data is refreshed in real-time as a new order comes in. In this lesson, we will convert a static dashboard to a dynamic real-time one with Laravel Reverb.

I often see it done by polling the new data every minute or so, but it's usually a bad decision for performance reasons. A better way is to refresh the page parts only when the new data comes in. This is exactly what we'll implement here.

What we'll cover in this tutorial:

  • Install and Run the Reverb Server
  • Configure Laravel Broadcasting
  • Update Front-end JS: Real-time table, Chart, and Status Updates

So, are you ready? Let's dive in!


The Project

As a starting point, we currently have a project with a static dashboard like this:

As this tutorial is about real-time Reverb and not about Laravel fundamentals, I will just briefly summarize this starting project, with links to the repository for you to explore the full source code.

All of its data come from our Controller:

app/Http/Controllers/DashboardController.php

class DashboardController extends Controller
{
public function __invoke(OrdersService $ordersService)
{
$totalRevenue = $ordersService->getTotalRevenue();
$thisMonthRevenue = $ordersService->getThisMonthRevenue();
$todayRevenue = $ordersService->getTodayRevenue();
$latestOrders = $ordersService->getLatestOrders(5);
$orderChartByMonth = $ordersService->orderChartByMonth();
$orderChartByDay = $ordersService->orderChartByDay();
 
return view(
'dashboard',
compact(
'totalRevenue',
'thisMonthRevenue',
'todayRevenue',
'latestOrders',
'orderChartByDay',
'orderChartByMonth'
)
);
}
}

In there, we are using the OrdersService to load the data into a Blade View with static variables and Chart.js:

resources/views/dashboard.blade.php

{{-- ... --}}
 
<div class="...">
Total Revenue:
</div>
<div class="...">
$ <span id="box_total_revenue">{{ Number::format($totalRevenue) }}</span>
</div>
 
{{-- ... --}}
 
<div class="...">
Revenue This Month:
</div>
<div class="...">
$ <span id="box_revenue_this_month">{{ Number::format($thisMonthRevenue) }}</span>
</div>
 
{{-- ... --}}
 
<div class="...">
Revenue Today:
</div>
<div class="...">
$ <span id="box_revenue_today">{{ Number::format($todayRevenue) }}</span>
</div>
 
{{-- ... --}}
 
@foreach($latestOrders as $order)
<tr class=" [@media(hover:hover)]:transition [@media(hover:hover)]:duration-75">
<td class="...">
{{ $order->created_at->format('M d, Y h:i A') }}
</td>
<td class="...">
{{ $order->user->email }}
</td>
<td class="...">
$ {{ Number::format($order->total) }}
</td>
</tr>
@endforeach
 
{{-- ... --}}
 
<canvas id="revenueByDay"></canvas>
 
{{-- ... --}}
 
<canvas id="revenueByMonth"></canvas>
 
{{-- ... --}}
 
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
 
<script>
const revenueByDay = document.getElementById('revenueByDay');
const revenueByMonth = document.getElementById('revenueByMonth');
 
let revenueByDayChart = new Chart(revenueByDay, {
type: 'bar',
data: {
labels: @json($orderChartByDay['labels']),
datasets: [{
data: @json($orderChartByDay['totals']),
borderWidth: 1
}]
},
options: {
plugins: {
legend: {
display: false,
}
},
scales: {
y: {
beginAtZero: true
}
}
}
});
 
let revenueByMonthChart = new Chart(revenueByMonth, {
type: 'bar',
data: {
labels: @json($orderChartByMonth['labels']),
datasets: [{
data: @json($orderChartByMonth['totals']),
borderWidth: 1
}]
},
options: {
plugins: {
legend: {
display: false,
}
},
scales: {
y: {
beginAtZero: true
}
}
}
});
</script>

You can see the complete starting code of this Blade file here.

But this version requires us to reload the page whenever we want to see new data. Let's change that with Reverb and make it real-time!


Install and Run Reverb

To install Reverb, we have to call this command:

php artisan install:broadcasting

This will ask us if we want to install Reverb, choose yes and press Enter.

Once the package installs, it will ask if we want Node dependencies to be installed. Choose yes and press Enter.

That's it! Reverb is installed and ready to be used. No more configuration is needed at this point.

Now that we have our dashboard and Reverb installed, we need to connect them. Here's a quick rundown of the plan:

  • We will create a new Event for the Orders
  • We will broadcast the Event when a new Order is created
  • We will listen for that Event in JS
  • We will run a script to update the dashboard when the Event is received

Let's do this!


Creating the Event

The first thing we need to do is create an Event for the fact that the Order has been created. This Event will be broadcasted when a new Order is created.

php artisan make:event OrderCreatedEvent

This will create a new Event in the app/Events directory. We need to modify it to load the data we need for the dashboard. For this, we will re-use our Service:

app/Events/OrderCreatedEvent.php

namespace App\Events;
 
use App\Models\Order;
use App\Services\OrdersService;
use Illuminate\Support\Number;
 
class OrderCreatedEvent implements ShouldBroadcast
{
// ...
 
public string $totalRevenue;
public string $thisMonthRevenue;
public string $todayRevenue;
public array $latestOrders;
public array $orderChartByMonth;
public array $orderChartByDay;
 
public function __construct(public Order $order)
{
$ordersService = new OrdersService();
 
$this->totalRevenue = Number::format($ordersService->getTotalRevenue());
$this->thisMonthRevenue = Number::format($ordersService->getThisMonthRevenue());
$this->todayRevenue = Number::format($ordersService->getTodayRevenue());
$this->latestOrders = $ordersService->getLatestOrders(1)->map(function(Order $order) {
return [
'id' => $order->id,
'user' => $order->user->toArray(),
'total' => Number::format($order->total),
'created_at' => $order->created_at->format('M d, Y h:i A')
];
})->toArray();
$this->orderChartByMonth = $ordersService->orderChartByMonth(0);
$this->orderChartByDay = $ordersService->orderChartByDay(0);
}
// ...
}

Here are a few things to note:

  • We are using the Number class to format the numbers - this will remove the need to format them in javascript
  • We are getting the latest order and formatting it for the dashboard (total and created_at)
  • We are updating the chart data and taking the last 0 days/months (this will get all data for the last day/month)

All of these were why we created the service - to re-use the code with simple parameter changes.


Broadcasting the Event

Next, we need to broadcast the Event. To do so, we must create a channel and broadcast the Event.

routes/channels.php

// ...
 
Broadcast::channel('order-dashboard-updates', function () {
// Public Channel
});

Next, we need to open our Event and update the broadcastOn method to broadcast on the new channel:

// ...
public function broadcastOn(): array
{
return [
new PrivateChannel('order-dashboard-updates')
];
}
// ...

Once this is done, we can trigger the Event when a new Order is created. We will do this in the OrderController:

app/Http/Controllers/OrderController.php

use App\Events\OrderCreatedEvent;
 
// ...
 
public function store(StoreOrderRequest $request)
{
$order = Order::create($request->validated());
 
event(new OrderCreatedEvent($order));
 
return redirect()->route('dashboard');
}

That's it! We can switch our attention to the dashboard now and to the JavaScript part.


Listening for the Event

To listen for the Event, we need to add a new script to our dashboard:

resources/views/dashboard.blade.php

{{-- ... --}}
 
<script>
{{-- ... --}}
 
window.addEventListener('DOMContentLoaded', function () {
let channel = window.Echo.private('order-dashboard-updates');
channel.listen('OrderCreatedEvent', function (e) {
console.log(e)
});
});
</script>

Now, we should start two terminal windows. One with:

php artisan reverb:start --debug

And another with a queue worker:

php artisan queue:listen

Once that is done, open the browser with the dashboard page. In that window, open a console.

Note: It should be empty at this point.

Next, open another window and create an order. You should see the Event in the console:

This means that the Event is being broadcasted and received by the dashboard. Next, we will work on updating the data!


Updating the Dashboard

Let's start by updating the simple things - the total revenue, revenue this month, and revenue today:

resources/views/dashboard.blade.php

{{-- ... --}}
 
window.addEventListener('DOMContentLoaded', function () {
let totalRevenue = document.getElementById('box_total_revenue');
let revenueThisMonth = document.getElementById('box_revenue_this_month');
let revenueToday = document.getElementById('box_revenue_today');
 
let channel = window.Echo.private('order-dashboard-updates');
channel.listen('OrderCreatedEvent', function (e) {
console.log(e)
// Update the revenue widgets
totalRevenue.innerText = e.totalRevenue;
revenueThisMonth.innerText = e.thisMonthRevenue;
revenueToday.innerText = e.todayRevenue;
});
});
 
{{-- ... --}}

Now, let's test it. Our initial values are these:

And we have created a new order with 5000 as the total. After the Event is received, the values should update:

It worked! Now, let's update the latest orders table:

resources/views/dashboard.blade.php

{{-- ... --}}
 
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
 
<script type="text/html" id="table-row-template">{{-- [tl! add:start] --}}
<tr class=" [@media(hover:hover)]:transition [@media(hover:hover)]:duration-75 bg-green-100">
<td class=" p-0 first-of-type:ps-1 last-of-type:pe-1 sm:first-of-type:ps-3 sm:last-of-type:pe-3 ">
<div class="flex w-full disabled:pointer-events-none justify-start text-start">
<div class=" grid gap-y-1 px-3 py-4">
<div class="flex max-w-max">
<div class=" inline-flex items-center gap-1.5 text-sm text-gray-950 dark:text-white "
style="">
_DATE_
</div>
</div>
</div>
</div>
</td>
<td class=" p-0 first-of-type:ps-1 last-of-type:pe-1 sm:first-of-type:ps-3 sm:last-of-type:pe-3 .email">
<div class="flex w-full disabled:pointer-events-none justify-start text-start">
<div class=" grid gap-y-1 px-3 py-4">
<div class="flex max-w-max">
<div class=" inline-flex items-center gap-1.5 text-sm text-gray-950 dark:text-white "
style="">
_EMAIL_
</div>
</div>
</div>
</div>
</td>
<td class=" p-0 first-of-type:ps-1 last-of-type:pe-1 sm:first-of-type:ps-3 sm:last-of-type:pe-3 ">
<div class="flex w-full disabled:pointer-events-none justify-start text-start">
<div class=" grid gap-y-1 px-3 py-4">
<div class="flex max-w-max">
<div class=" inline-flex items-center gap-1.5 text-sm text-gray-950 dark:text-white "
style="">
$ _TOTAL_
</div>
</div>
</div>
</div>
</td>
</tr>
</script>
 
{{-- ... --}}
 
window.addEventListener('DOMContentLoaded', function () {
let totalRevenue = document.getElementById('box_total_revenue');
let revenueThisMonth = document.getElementById('box_revenue_this_month');
let revenueToday = document.getElementById('box_revenue_today');
let latestOrdersTable = document.getElementById('latest-orders-table');
let tableRowTemplate = document.getElementById('table-row-template').innerHTML;
 
let channel = window.Echo.private('order-dashboard-updates');
channel.listen('OrderCreatedEvent', function (e) {
console.log(e)
// Update the revenue widgets
totalRevenue.innerText = e.totalRevenue;
revenueThisMonth.innerText = e.thisMonthRevenue;
revenueToday.innerText = e.todayRevenue;
 
// Insert the new row at the top of the table
let newRow = tableRowTemplate
.replace('_DATE_', e.latestOrders[0].created_at)
.replace('_EMAIL_', e.latestOrders[0].user.email)
.replace('_TOTAL_', e.latestOrders[0].total);
latestOrdersTable.querySelector('tbody').insertAdjacentHTML('afterbegin', newRow);
 
setTimeout(function () {
// Remove the green background from the rows
let lines = latestOrdersTable.querySelectorAll('tbody tr');
lines.forEach(function (line) {
line.classList.remove('bg-green-100');
});
}, 2500)
// remove the last row of the table
latestOrdersTable.querySelector('tbody tr:last-child').remove();
});
});
 
{{-- ... --}}

Now, let's reload the dashboard page and try to create a new order. This is how it looked before creation:

Once we create an order with 4343 as the value, we should see it appear as:

Note: For 2.5 seconds, the rows should glow green and then return to normal. This is done via the setTimeout().

Last on our list - the charts. Let's add the code to update the charts:

resources/views/dashboard.blade.php

{{-- ... --}}
 
window.addEventListener('DOMContentLoaded', function () {
let totalRevenue = document.getElementById('box_total_revenue');
let revenueThisMonth = document.getElementById('box_revenue_this_month');
let revenueToday = document.getElementById('box_revenue_today');
let latestOrdersTable = document.getElementById('latest-orders-table');
let tableRowTemplate = document.getElementById('table-row-template').innerHTML;
 
 
let channel = window.Echo.private('order-dashboard-updates');
channel.listen('OrderCreatedEvent', function (e) {
console.log(e)
// Update the revenue widgets
totalRevenue.innerText = e.totalRevenue;
revenueThisMonth.innerText = e.thisMonthRevenue;
revenueToday.innerText = e.todayRevenue;
 
// Insert the new row at the top of the table
let newRow = tableRowTemplate
.replace('_DATE_', e.latestOrders[0].created_at)
.replace('_EMAIL_', e.latestOrders[0].user.email)
.replace('_TOTAL_', e.latestOrders[0].total);
latestOrdersTable.querySelector('tbody').insertAdjacentHTML('afterbegin', newRow);
 
setTimeout(function () {
// Remove the green background from the rows
let lines = latestOrdersTable.querySelectorAll('tbody tr');
lines.forEach(function (line) {
line.classList.remove('bg-green-100');
});
}, 2500)
// remove the last row of the table
latestOrdersTable.querySelector('tbody tr:last-child').remove();
 
if (!revenueByDayChart.data.labels.includes(e.orderChartByDay.labels[0])) {
// If there is no data for the day, add it
revenueByDayChart.data.labels.push(e.orderChartByDay.labels[0]);
revenueByDayChart.data.datasets[0].data.push(e.orderChartByDay.totals[0]);
revenueByDayChart.update();
} else {
// If there is data for the day, update it
let index = revenueByDayChart.data.labels.indexOf(e.orderChartByDay.labels[0]);
revenueByDayChart.data.datasets[0].data[index] = e.orderChartByDay.totals[0];
revenueByDayChart.update();
}
 
 
if (!revenueByMonthChart.data.labels.includes(e.orderChartByMonth.labels[0])) {
// If there is no data for the month, add it
revenueByMonthChart.data.labels.push(e.orderChartByMonth.labels[0]);
revenueByMonthChart.data.datasets[0].data.push(e.orderChartByMonth.totals[0]);
revenueByMonthChart.update();
} else {
// If there is data for the month, update it
let index = revenueByMonthChart.data.labels.indexOf(e.orderChartByMonth.labels[0]);
revenueByMonthChart.data.datasets[0].data[index] = e.orderChartByMonth.totals[0];
revenueByMonthChart.update();
}
});
});
 
{{-- ... --}}

Once again, after refreshing, we should see the charts update. Here's the state before the order creation:

And then, we will create an order for 150000 to see a significant change:

That's it! We have a real-time dashboard that updates when a new order is created!

Note: Don't forget to run npm run build to build the assets!


Full repository for the project: laravel-reverb-real-time-dashboard