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. At this point, no more configuration is needed to use Reverb locally.
Reverb might not work in production or when you or your teammate clones a repository. The reason for that can be the BROADCAST_CONNECTION
value in the .env
. It must be set to reverb
.
.env:
// ... BROADCAST_CONNECTION=reverb FILESYSTEM_DISK=localQUEUE_CONNECTION=database // ...
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 return true;});
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
I'm so confused as to why I can never get Reverb to work properly - I have literally cloned the repo and ran the migrations, setup up the .env as per normal - where do you setup the reverb configs in this?
I currently get the following errors:
Has anyone managed to get this working for them?
Check if you have filled .env reverb values. For some reason they did not push to an example file... Will update them tomorrow morning, but for now you can generate them by running broadcasting install command
ok thanks i'll try doing this again maybe I missed a step thanks again
I am not quite sure why when I run
php artisan broadcasting install
this doesn't actually generate my REVERB env variables - can I simply manually add these to the .env and add any random values to it?Interesting. But yes, semi random values should work!
Tried some random Reverb variables in my .env and currently I get these errors in the Chrome console when going to the realtime dashboard in the browser (have already ran the db:seed)
Officially i am lost :) Didn't think this even used Pusher... hopefully you guys do a video tutorial for this soon as I seem to be getting no joy with Reverb for some reason :(
I suggest you to check other LaravelDaily repos with reverb. There is a few with variables in place