Backend DevelopmentFrontend Development

Implementing Real-time Notifications With Laravel Reverb and Vue

Implementing Real-time Notifications With Laravel Reverb and Vue

In this article we will implement a Real-time notifications using Laravel Reverb package and Vue.

 

 

 

Laravel Reverb is a first party Websocket for laravel apps that can be used to real-time messaging between client and server. Laravel Reverb used for different kinds of real-time apps like chat applications, real-time graphs, notifications, etc.

In this article we will learn about it, how to configure, and setting up server and client logic. We will implement a simple real time notifications that can be used in dashboards and admin panels to trigger for specific actions.

 

Make sure you have PHP 8.2 or later installed. Let’s start by creating new laravel 11 project:

composer create-project laravel/laravel laravel_reverb_notifications

Next open the project in your IDE, we will make some configurations:

First open .env and replace this line:

DB_CONNECTION=sqlite

With

DB_CONNECTION=mysql

As i am using mysql as the database. Then un-comment the db related variables, set the db username and password like so:

DB_HOST=127.0.0.1
 DB_PORT=3306
 DB_DATABASE=laravel_reverb_notification
 DB_USERNAME=<db username>
 DB_PASSWORD=<db password>

you can also set the SESSION_DRIVER, CACHE_STORE to be file like so:

SESSION_DRIVER=file
CACHE_STORE=file

 

Creating Migration

Let’s generate a migration and model for our notifications table:

php artisan make:model Notification -m

This command generate the Notifications model class and the “-m” option creates the migration.

Open the newly created migration file and update it like so:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('notifications', function (Blueprint $table) {
            $table->id();
            $table->text('content');
            $table->string('icon')->nullable();
            $table->tinyInteger('seen')->default(1);
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('notifications');
    }
};

Next run the migrate command as shown:

php artisan migrate

You will be asked to create the database if not exists, just select Yes and continue.

 

Open the app/Models/Notification.php model add the $fillable property like so:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Notification extends Model
{
    use HasFactory;

    protected $fillable = [
        "content",
        "icon",
        "seen"
    ];
}

 

Installing Reverb

To use Reverb, we have to install laravel broadcasting:

php artisan install:broadcasting

Once running this command, the config/broadcasting.php config file will be generated

Then you will be asked to install Laravel Reverb, select “Yes”:

laravel-reverb-install1

 

This will install Laravel Reverb composer package. After this step you will see that config/reverb.php config file generated.

Next you will be asked to install the Node dependencies required for broadcasting:

laravel-reverb-install2

This step specifically run the “npm install” command internally and build the dependencies.

After installation successfully completes open your .env you will see a set of new env variables added:

REVERB_APP_ID=793430
REVERB_APP_KEY=<reverb app key>
REVERB_APP_SECRET=<reverb app secret>
REVERB_HOST="localhost"
REVERB_PORT=8080
REVERB_SCHEME=http

These env variables used to configure reverb connection. The first three variables (REVERB_APP_ID, REVERB_APP_KEY, REVERB_APP_SECRET) represent the application credentials and must exchanged between the client and server.

The REVERB_HOST and REVERB_PORT instruct laravel reverb where send broadcast messages. The REVERB_SCHEME specifies whether you are using ssl for the REVERB_HOST.

 

Installing Tailwindcss

Install tailwindcss into the project:

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

This will generate tailwind.config.js in the project root directory, open it and update the content section like shown:

content: [
      "./resources/**/*.blade.php",
      "./resources/**/*.js",
      "./resources/**/*.vue",
  ]

Then include the tailwind directives in app.css

resources/css/app.css

@tailwind base;
@tailwind components;
@tailwind utilities;

Run the build process:

npm run dev

 

Installing Vuejs

Install vue 3 with npm:

npm install vue@latest

Next install vue vite plugin:

npm install --save-dev @vitejs/plugin-vue

Add the Vue config part to the vite.config.js file:

import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from "@vitejs/plugin-vue";

export default defineConfig({
    plugins: [
        laravel({
            input: ['resources/css/app.css', 'resources/js/app.js'],
            refresh: true,
        }),
        vue()
    ],
});

To check this is working run:

npm run dev

 

Creating Home view

Let’s create the blade view that will be our landing page to the view app

resources/views/home.blade.php

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>Real time notifications</title>

    <!-- Fonts -->
    <link rel="preconnect" href="https://fonts.bunny.net">
    <link href="https://fonts.bunny.net/css?family=figtree:400,600&display=swap" rel="stylesheet" />

    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css" />

    @vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="font-sans antialiased dark:bg-black dark:text-white/50">

    <div id="app"></div>

</body>
</html>

Most notably in this view i added the @vite directive to load the compiled css and js. And the div#app that will be the mount point for the app view.

Update routes/web.php to load this view:

Route::get('/', function () {
    return view('home');
});

 

Create the App component in resources/js/components

resources/js/components/App.vue

<script setup>
    import {ref} from "vue";

    const showNotifications = ref(false);

    const toggleNotifications = () => {
        showNotifications.value = !showNotifications.value;
    }
</script>

<template>
    <header class="flex justify-between bg-gray-500 text-white px-3 pt-3 pb-3">
        <div class="brand">
            Real-time Notifications
        </div>
        <div class="notifications relative">
            <a href="#" @click.prevent="toggleNotifications" title="notifications">
                <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
                    <path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 0 0 5.454-1.31A8.967 8.967 0 0 1 18 9.75V9A6 6 0 0 0 6 9v.75a8.967 8.967 0 0 1-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 0 1-5.714 0m5.714 0a3 3 0 1 1-5.714 0M3.124 7.5A8.969 8.969 0 0 1 5.292 3m13.416 0a8.969 8.969 0 0 1 2.168 4.5" />
                </svg>
            </a>

            <div class="dropdown absolute top-[37px] right-0 bg-blue-400 w-[230px] px-2 py-2 max-h-[260px] overflow-y-auto" :class="{'hidden': !showNotifications}">
                <ul>
                    <li class="mb-3 pb-1 border-b bg-gray-200 rounded text-black px-2">
                        <div>
                            <span class="fa fa-info-circle pe-1"></span>
                            <span class="text-sm">lorem ipsum lorem ipsum</span>
                        </div>
                        <time>
                            <span class="fa fa-clock text-xs"></span> <span class="text-xs">Now</span>
                        </time>
                    </li>
                    <li class="mb-3 pb-1 border-b bg-gray-200 rounded text-black px-2">
                        <div>
                            <span class="fa fa-question-circle pe-1"></span>
                            <span class="text-sm">lorem ipsum lorem ipsum</span>
                        </div>
                        <time>
                            <span class="fa fa-clock text-xs"></span> <span class="text-xs">2024-09-13 04:20 pm</span>
                        </time>
                    </li>
                    <li class="mb-2 pb-1 bg-gray-200 rounded text-black px-2">
                        <div>
                            <span class="fa fa-user pe-1"></span>
                            <span class="text-sm">lorem ipsum lorem ipsum</span>
                        </div>
                        <time>
                            <span class="fa fa-clock text-xs"></span> <span class="text-xs">2024-09-5 12:00 am</span>
                        </time>
                    </li>

                </ul>
            </div>


        </div>
    </header>
    <div class="mx-4 my-5">
        <h3>Real time notifications with Laravel Reverb</h3>
        <p>Watch for notifications</p>
    </div>
</template>

In this code i have added a basic component html styled with tailwindcss classes. This component will display a header along with a notification icon at the top. When you click on the notification icon it will show a list of notifications.

Let’s initialize the vue app in the app.js file:

resources/js/app.js

import './bootstrap';
import { createApp } from "vue";

import App from "./components/App.vue";

createApp(App).mount("#app");

Now run again:

npm run dev

and launch the app:

php artisan serve

Go to the browser and inspect the app, you will see the notification icon.

 

Installing Vue Toast

To display the notification, we will install a beautiful vue package used for toast messages,

npm install --save vue3-toastify

Next include the toast and toast css at the top of the App.vue

import {ref, onMounted} from "vue";
import { toast } from 'vue3-toastify';
import 'vue3-toastify/dist/index.css';

We can check that it’s already working by adding a test code on the mounted hook like so:

<script setup>

    ....
    ....

     onMounted(() => {
        toast("This is toast message", {
            autoClose: 2000,
            type: "info",
            theme: "colored"
        });
    });
</script>

If you see the notification shown in the top of browser window, then it’s working fine. Now remove the code inside of the onMounted() hook.

 

Generating Notification Event

To start sending and subscribing to events we need to create a laravel event:

php artisan make:event NotificationSent

The NotificationSent event will be created in app/Events directory, update it as shown:

app/Events/NotificationSent.php

<?php

namespace App\Events;

use App\Models\Notification;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class NotificationSent implements ShouldBroadcastNow
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    /**
     * Create a new event instance.
     */
    public function __construct(public Notification $notification)
    {
        //
    }

    /**
     * Get the channels the event should broadcast on.
     *
     * @return array<int, \Illuminate\Broadcasting\Channel>
     */
    public function broadcastOn(): array
    {
        return [
            new Channel('notifications_channel'),
        ];
    }
}

First the event must implement ShouldBroadcastNow interface so that the event sent immediately. Then i updated the constructor to pass the $notification argument. On the broadcastOn() method we tell laravel the channel name to send the event to which is “notifications_channel” .

To register the channel name “notifications_channel”, open routes/channels.php and add the channel route:

Broadcast::channel('notifications_channel', function () {
    return true;
});

 

Listening for Notifications

I have updated the App.vue to include the logic for listening for notification events:

resources/js/components/App.vue

<script setup>
    import {ref, onMounted} from "vue";
    import { toast } from 'vue3-toastify';
    import 'vue3-toastify/dist/index.css';
    import axios from "axios";

    const showNotifications = ref(false);
    const notifications = ref([]);

    const toggleNotifications = () => {
        showNotifications.value = !showNotifications.value;
    }

    const getAllNotifications = () => {
        axios.get("/notifications").then(response => {
           if(response.data.data) {
               response.data.data.forEach(n => {
                   notifications.value = [n, ...notifications.value];
               });

           }
        });
    }

    const markNotificationAsRead = (id) => {
        axios.put(`/notifications/${id}`).then();
    }

    onMounted(() => {
        getAllNotifications();

        Echo.channel(`notifications_channel`)
            .listen("NotificationSent", (response) => {

                notifications.value = [{
                    ...response.notification
                }, ...notifications.value];

                toast(response.notification.content, {
                    autoClose: 3000,
                    type: "info",
                    theme: "colored"
                });

                markNotificationAsRead(response.notification.id);
            });


    });
</script>

<template>
    <header class="flex justify-between bg-gray-500 text-white px-3 pt-3 pb-3">
        <div class="brand">
            Real-time Notifications
        </div>
        <div class="notifications relative">
            <a href="#" @click.prevent="toggleNotifications" title="notifications">
                <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
                    <path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 0 0 5.454-1.31A8.967 8.967 0 0 1 18 9.75V9A6 6 0 0 0 6 9v.75a8.967 8.967 0 0 1-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 0 1-5.714 0m5.714 0a3 3 0 1 1-5.714 0M3.124 7.5A8.969 8.969 0 0 1 5.292 3m13.416 0a8.969 8.969 0 0 1 2.168 4.5" />
                </svg>
            </a>

            <div class="dropdown absolute top-[37px] right-0 bg-blue-400 w-[230px] px-2 py-2 max-h-[260px] overflow-y-auto" :class="{'hidden': !showNotifications}">
                <ul v-if="notifications.length">
                    <li v-for="notification in notifications" :key="notification.id" class="mb-3 pb-1 border-b bg-gray-200 rounded text-black px-2">
                        <div>
                            <span class="pe-1" :class="notification.icon"></span>
                            <span class="text-sm">{{notification.content}}</span>
                        </div>
                        <time>
                            <span class="fa fa-clock text-xs"></span> <span class="text-xs">{{notification.formatted_time}}</span>
                        </time>
                    </li>

                </ul>
                <ul v-else>
                    <li>No notifications</li>
                </ul>
            </div>


        </div>
    </header>
    <div class="mx-4 my-5">
        <h3>Real time notifications with Laravel Reverb</h3>
        <p>Watch for notifications</p>
    </div>
</template>

On the onMounted() hook i am listening for notification events using laravel Echo.channel method:

Echo.channel(`notifications_channel`)
            .listen("NotificationSent", (response) => {
})

The channel() method accepts the channel name argument we specified above. Then attaching the listen() method which accepts the event name and callback arguments. The event name is the class name “NotificationSent“.

The Echo object is configured inside of resources/js/echo.js file:

window.Echo = new Echo({
    broadcaster: 'reverb',
    key: import.meta.env.VITE_REVERB_APP_KEY,
    ...
    ...
});

As you see the broadcaster specified as “reverb” along with other config options like key, wsHost, wsPort and so on.

In the Echo.listen() callback argument i receive the response, using this response i appends the response object to the list notifications and then triggering the toast message and finally call function markNotificationAsRead() to make http request using axios to update notification to be seen:

toast(response.notification.content, {
                    autoClose: 3000,
                    type: "info",
                    theme: "colored"
                });

markNotificationAsRead(response.notification.id);

Next in the component html i replaced the dummy content for notification list to loop over notifications ref variable:

<li v-for="notification in notifications" :key="notification.id" class="mb-3 pb-1 border-b bg-gray-200 rounded text-black px-2">
                        <div>
                            <span class="pe-1" :class="notification.icon"></span>
                            <span class="text-sm">{{notification.content}}</span>
                        </div>
                        <time>
                            <span class="fa fa-clock text-xs"></span> <span class="text-xs">{{notification.formatted_time}}</span>
                        </time>
                    </li>

The getAllNotifications() called on app load to fetch all notifications

 

Create the NotificationsController

php artisan make:controller NotificationsController

Open app/Http/Controllers/NotificationsController.php and update as shown:

<?php

namespace App\Http\Controllers;

use App\Models\Notification;
use Illuminate\Http\Request;

class NotificationsController extends Controller
{
    public function index()
    {
        $notifications = Notification::where("seen", 1)->get();

        return response()->json(data: ['data' => $notifications]);
    }

    public function update(Notification $notification)
    {
        $notification->update(["seen" => 0]);

        return response()->json(['success' => true]);
    }
}

Add the routes for these two actions in routes/web.php:

Route::get('notifications', [\App\Http\Controllers\NotificationsController::class, 'index']);

Route::put('notifications/{notification}', [\App\Http\Controllers\NotificationsController::class, 'update']);

Also update Notification model to include getFormattedTimeAttribute():

protected $appends = ['formatted_time'];

    protected function getFormattedTimeAttribute()
    {
        return $this->created_at->diffForHumans();
    }

This snippet includes the “formatted_time” property along with the notification response object used in template above.

 

Emitting Notification Events

So far we added the logic to listen for events, now we need a way to send the notification. In real world application, this is coming from frontend app. But for simplicity i will make a a console command to send notification from:

php artisan make:command SendNotificationCommand

app/Console/Commands/SendNotificationCommand.php

<?php

namespace App\Console\Commands;

use App\Events\NotificationSent;
use App\Models\Notification;
use Illuminate\Console\Command;

class SendNotificationCommand extends Command
{
    protected $signature = 'app:send-notification-command {text} {type}';

    protected $description = 'Command description';

    /**
     * Execute the console command.
     */
    public function handle()
    {
        $icon = match($this->argument("type")) {
          'user' => "fa fa-user",
          'question' => "fa fa-question-circle",
          default => "fa fa-info-circle"
        };

        $notification = Notification::create([
           "content" => $this->argument("text"),
           "icon" => $icon
        ]);

        broadcast(new NotificationSent($notification));
    }
}

The command signature accepts two arguments, text (notification content) and type (notification icon). In the handle() method we check for the type argument to display the suitable icon css class using match expression.

Then we create the notification to be stored in the database. Finally we emit the event using laravel broadcast() function passing in an instance of NotificationSent object.

 

To check this run these commands in different terminals:

npm run dev
php artisan serve

And also launch the reverb server:

php artisan reverb:start

Open the browser and in other terminal window send notification command:

php artisan app:send-notification-command 'new user registration' user 
php artisan app:send-notification-command 'new client inquiry' question

If everything is working fine you will see the notification like this:

reverb notifications preview

Source Code

0 0 votes
Article Rating

What's your reaction?

Excited
0
Happy
0
Not Sure
0
Confused
0

You may also like

Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments