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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
composer create-project laravel/laravel laravel_reverb_notifications
composer create-project laravel/laravel laravel_reverb_notifications
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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
DB_CONNECTION=sqlite
DB_CONNECTION=sqlite
DB_CONNECTION=sqlite

With

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
DB_CONNECTION=mysql
DB_CONNECTION=mysql
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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel_reverb_notification
DB_USERNAME=<db username>
DB_PASSWORD=<db password>
DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=laravel_reverb_notification DB_USERNAME=<db username> DB_PASSWORD=<db password>
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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
SESSION_DRIVER=file
CACHE_STORE=file
SESSION_DRIVER=file CACHE_STORE=file
SESSION_DRIVER=file
CACHE_STORE=file

 

Creating Migration

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
php artisan make:model Notification -m
php artisan make:model Notification -m
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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<?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');
}
};
<?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'); } };
<?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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
php artisan migrate
php artisan migrate
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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<?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"
];
}
<?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" ]; }
<?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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
php artisan install:broadcasting
php artisan install: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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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
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
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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
npm install -D tailwindcss postcss autoprefixer npx tailwindcss init -p
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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
content: [
"./resources/**/*.blade.php",
"./resources/**/*.js",
"./resources/**/*.vue",
]
content: [ "./resources/**/*.blade.php", "./resources/**/*.js", "./resources/**/*.vue", ]
content: [
      "./resources/**/*.blade.php",
      "./resources/**/*.js",
      "./resources/**/*.vue",
  ]

Then include the tailwind directives in app.css

resources/css/app.css

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
@tailwind base;
@tailwind components;
@tailwind utilities;
@tailwind base; @tailwind components; @tailwind utilities;
@tailwind base;
@tailwind components;
@tailwind utilities;

Run the build process:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
npm run dev
npm run dev
npm run dev

 

Installing Vuejs

Install vue 3 with npm:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
npm install vue@latest
npm install vue@latest
npm install vue@latest

Next install vue vite plugin:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
npm install --save-dev @vitejs/plugin-vue
npm install --save-dev @vitejs/plugin-vue
npm install --save-dev @vitejs/plugin-vue

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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()
],
});
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() ], });
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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
npm run dev
npm run dev
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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<!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>
<!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>
<!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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
Route::get('/', function () {
return view('home');
});
Route::get('/', function () { return view('home'); });
Route::get('/', function () {
    return view('home');
});

 

Create the App component in resources/js/components

resources/js/components/App.vue

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<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>
<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>
<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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import './bootstrap';
import { createApp } from "vue";
import App from "./components/App.vue";
createApp(App).mount("#app");
import './bootstrap'; import { createApp } from "vue"; import App from "./components/App.vue"; createApp(App).mount("#app");
import './bootstrap';
import { createApp } from "vue";

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

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

Now run again:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
npm run dev
npm run dev
npm run dev

and launch the app:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
php artisan serve
php artisan serve
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,

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
npm install --save vue3-toastify
npm install --save vue3-toastify
npm install --save vue3-toastify

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import {ref, onMounted} from "vue";
import { toast } from 'vue3-toastify';
import 'vue3-toastify/dist/index.css';
import {ref, onMounted} from "vue"; import { toast } from 'vue3-toastify'; import 'vue3-toastify/dist/index.css';
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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<script setup>
....
....
onMounted(() => {
toast("This is toast message", {
autoClose: 2000,
type: "info",
theme: "colored"
});
});
</script>
<script setup> .... .... onMounted(() => { toast("This is toast message", { autoClose: 2000, type: "info", theme: "colored" }); }); </script>
<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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
php artisan make:event NotificationSent
php artisan make:event NotificationSent
php artisan make:event NotificationSent

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

app/Events/NotificationSent.php

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<?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'),
];
}
}
<?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'), ]; } }
<?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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
Broadcast::channel('notifications_channel', function () {
return true;
});
Broadcast::channel('notifications_channel', function () { return true; });
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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<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>
<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>
<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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
Echo.channel(`notifications_channel`)
.listen("NotificationSent", (response) => {
})
Echo.channel(`notifications_channel`) .listen("NotificationSent", (response) => { })
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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
window.Echo = new Echo({
broadcaster: 'reverb',
key: import.meta.env.VITE_REVERB_APP_KEY,
...
...
});
window.Echo = new Echo({ broadcaster: 'reverb', key: import.meta.env.VITE_REVERB_APP_KEY, ... ... });
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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
toast(response.notification.content, {
autoClose: 3000,
type: "info",
theme: "colored"
});
markNotificationAsRead(response.notification.id);
toast(response.notification.content, { autoClose: 3000, type: "info", theme: "colored" }); markNotificationAsRead(response.notification.id);
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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<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>
<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>
<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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
php artisan make:controller NotificationsController
php artisan make:controller NotificationsController
php artisan make:controller NotificationsController

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<?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]);
}
}
<?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]); } }
<?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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
Route::get('notifications', [\App\Http\Controllers\NotificationsController::class, 'index']);
Route::put('notifications/{notification}', [\App\Http\Controllers\NotificationsController::class, 'update']);
Route::get('notifications', [\App\Http\Controllers\NotificationsController::class, 'index']); Route::put('notifications/{notification}', [\App\Http\Controllers\NotificationsController::class, 'update']);
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():

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
protected $appends = ['formatted_time'];
protected function getFormattedTimeAttribute()
{
return $this->created_at->diffForHumans();
}
protected $appends = ['formatted_time']; protected function getFormattedTimeAttribute() { return $this->created_at->diffForHumans(); }
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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
php artisan make:command SendNotificationCommand
php artisan make:command SendNotificationCommand
php artisan make:command SendNotificationCommand

app/Console/Commands/SendNotificationCommand.php

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<?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));
}
}
<?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)); } }
<?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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
npm run dev
npm run dev
npm run dev
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
php artisan serve
php artisan serve
php artisan serve

And also launch the reverb server:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
php artisan reverb:start
php artisan reverb:start
php artisan reverb:start

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
php artisan app:send-notification-command 'new user registration' user
php artisan app:send-notification-command 'new user registration' user
php artisan app:send-notification-command 'new user registration' user 
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
php artisan app:send-notification-command 'new client inquiry' question
php artisan app:send-notification-command 'new client inquiry' question
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

5 1 vote
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