Documentation Index
Fetch the complete documentation index at: https://docs.mejik.web.id/llms.txt
Use this file to discover all available pages before exploring further.
This is an excellent use case for Server-Sent Events (SSE). It’s much more efficient than polling, as it keeps a single, long-lived HTTP connection open for the server to push updates to the client.
Here is a complete step-by-step implementation for your Laravel 10 and Vue 2 application.
The Flow
- A user visits a page with the notification component.
- The Vue component opens an SSE connection to a specific Laravel route (e.g.,
/notifications/stream).
- The Laravel controller keeps this connection open.
- When a new
Notification is created in your MongoDB collection, the NotificationObserver fires.
- The observer’s
created method will write the new notification data to the cache with a user-specific key.
- The
SseController, in its long-running loop, detects the new cache entry, sends it down the stream to the browser, and then deletes the cache entry.
- Vue’s
EventSource listener receives the data and updates the UI in real-time.
- The “Refresh” button will call a separate, standard API endpoint to fetch all notifications, just like your old polling mechanism.
Step 1: Backend - The Model and Observer
First, let’s set up the model and the observer that will react to new database entries.
1a. Notification Model
I’ll assume you are using the jenssegers/laravel-mongodb package. Your model might look something like this.
app/Models/Notification.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Jenssegers\Mongodb\Eloquent\Model;
class Notification extends Model
{
use HasFactory;
// Use 'mongodb' connection if you have it configured in config/database.php
protected $connection = 'mongodb';
// The collection associated with the model.
protected $collection = 'notifications';
protected $fillable = [
'user_id', // The user who should receive the notification
'message',
'read_at',
'link',
];
protected $casts = [
'read_at' => 'datetime',
'user_id' => 'integer', // Cast if your user IDs are integers
];
/**
* Get the user that owns the notification.
*/
public function user()
{
// Assuming your User model is a standard Eloquent model
return $this->belongsTo(User::class);
}
}
1b. Create the Observer
This is the core of the trigger mechanism.
Generate the observer using Artisan:
php artisan make:observer NotificationObserver --model=Notification
Now, let’s implement the created method. It will write the new notification to the cache. The cache acts as a fast, temporary message queue between the web request that created the notification and the SSE process.
app/Observers/NotificationObserver.php
<?php
namespace App\Observers;
use App\Models\Notification;
use Illuminate\Support\Facades\Cache;
class NotificationObserver
{
/**
* Handle the Notification "created" event.
*
* We will cache the new notification with a unique key for the user.
* The SSE controller will be watching for this cache key.
*/
public function created(Notification $notification): void
{
// We use a user-specific key.
// The 'pull' method in the controller will retrieve and delete this.
// We add a unique suffix to handle multiple notifications created in quick succession.
$key = 'user.' . $notification->user_id . '.new_notification.' . uniqid();
// Cache for 10 minutes, just in case the SSE process is down.
Cache::put($key, $notification->toJson(), 600);
}
}
Note: We use uniqid() to ensure that if two notifications are created for the same user before the SSE loop runs, both are stored in the cache under different keys.
1c. Register the Observer
You must register the observer in a service provider. The best place is EventServiceProvider.
app/Providers/EventServiceProvider.php
<?php
namespace App\Providers;
use App\Models\Notification;
use App\Observers\NotificationObserver;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
{
// ...
/**
* Register any events for your application.
*/
public function boot(): void
{
Notification::observe(NotificationObserver::class);
}
// ...
}
Step 2: Backend - The SSE Controller and Route
This controller will handle the persistent connection.
2a. Create the Controller
php artisan make:controller SseController
2b. Implement the SSE Logic
We’ll use Laravel’s StreamedResponse to send data without closing the connection.
app/Http/Controllers/SseController.php
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Symfony\Component\HttpFoundation\StreamedResponse;
class SseController extends Controller
{
public function stream()
{
$response = new StreamedResponse(function() {
$user = Auth::user();
while (true) {
// 1. Check if the connection is aborted by the client
if (connection_aborted()) {
break;
}
// 2. Check for new notification keys in the cache for this user
$cachePrefix = 'user.' . $user->id . '.new_notification.';
$keys = Cache::getStore()->getPrefix() . $cachePrefix . '*';
// In Redis, you'd use Cache::keys('...'), but for file/db cache we scan
// Note: For high-traffic sites, use Redis for this pattern.
// For simplicity, this example will work with standard file/db cache.
$notificationKeys = collect(\Illuminate\Support\Facades\DB::table('cache')
->where('key', 'like', $keys.'%')
->pluck('key')
->map(fn($key) => str_replace(Cache::getStore()->getPrefix(), '', $key))
);
foreach ($notificationKeys as $key) {
// Use Cache::pull to get the item and delete it atomically
$notificationJson = Cache::pull($key);
if ($notificationJson) {
// 3. Format the data as an SSE message
echo "event: new-notification\n"; // Event name
echo "id: " . uniqid() . "\n"; // Event ID
echo "data: " . $notificationJson . "\n\n"; // The JSON data
// 4. Flush the output buffer to send the data to the client
ob_flush();
flush();
}
}
// 5. Sleep for a short duration to avoid high CPU usage
sleep(2);
}
});
// Set the headers for SSE
$response->headers->set('Content-Type', 'text/event-stream');
$response->headers->set('X-Accel-Buffering', 'no'); // Nginx-specific, important
$response->headers->set('Cache-Control', 'no-cache');
$response->headers->set('Connection', 'keep-alive');
return $response;
}
}
Important: The cache scanning part is not very performant with the file or database cache drivers. For production, you should strongly consider using redis:
CACHE_DRIVER=redis in your .env file. With Redis, you could use Cache::keys($cachePrefix . '*') which is much more efficient.
2c. Define the Route
This route must be protected by authentication middleware.
routes/web.php
use App\Http\Controllers\SseController;
use App\Http\Controllers\NotificationController; // We'll create this next
// ... other routes
Route::middleware(['auth'])->group(function () {
// The SSE endpoint
Route::get('/notifications/stream', [SseController::class, 'stream'])->name('notifications.stream');
// The manual-refresh endpoint (your old polling endpoint)
Route::get('/notifications', [NotificationController::class, 'index'])->name('notifications.index');
});
Step 3: Backend - Manual Refresh Endpoint
This is the standard endpoint that the “Refresh” button will use.
3a. Create the Controller
php artisan make:controller NotificationController
3b. Implement the Index Method
This method simply fetches all notifications for the current user.
app/Http/Controllers/NotificationController.php
<?php
namespace App\Http\Controllers;
use App\Models\Notification;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class NotificationController extends Controller
{
/**
* Fetch all notifications for the authenticated user.
* This is used for the initial load and manual refresh.
*/
public function index()
{
$notifications = Notification::where('user_id', Auth::id())
->latest() // Order by most recent
->take(10) // Limit the number
->get();
return response()->json($notifications);
}
}
Step 4: Frontend - The Vue 2 Component
Now let’s create the Vue component that consumes the SSE stream and provides the manual refresh button.
resources/js/components/NotificationBell.vue
<template>
<div class="notification-container">
<button @click="toggleDropdown" class="bell-button">
<i class="fa fa-bell"></i> <!-- Assuming you use FontAwesome -->
<span v-if="notificationCount > 0" class="notification-badge">{{ notificationCount }}</span>
</button>
<div v-if="isOpen" class="notification-dropdown">
<div class="dropdown-header">
<span>Notifications</span>
<button @click.prevent="refreshNotifications" class="refresh-button">Refresh</button>
</div>
<ul>
<li v-if="notifications.length === 0" class="no-notifications">
No new notifications
</li>
<li v-for="notification in notifications" :key="notification._id">
{{ notification.message }}
</li>
</ul>
</div>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'NotificationBell',
data() {
return {
notifications: [],
isOpen: false,
eventSource: null,
};
},
computed: {
notificationCount() {
// You might want to filter for unread notifications here
return this.notifications.length;
}
},
methods: {
toggleDropdown() {
this.isOpen = !this.isOpen;
},
/**
* Manual refresh method using Axios (your old polling logic).
*/
refreshNotifications() {
console.log('Manually fetching notifications...');
axios.get('/notifications')
.then(response => {
this.notifications = response.data;
})
.catch(error => {
console.error("Error fetching notifications:", error);
});
},
/**
* Sets up the Server-Sent Events connection.
*/
connectToSse() {
// Make sure we don't have an existing connection
if (this.eventSource) {
this.eventSource.close();
}
console.log('Connecting to SSE stream...');
this.eventSource = new EventSource('/notifications/stream');
this.eventSource.onopen = () => {
console.log('SSE Connection opened.');
};
// Listen for our custom 'new-notification' event
this.eventSource.addEventListener('new-notification', (event) => {
console.log('New notification received via SSE:', event.data);
const newNotification = JSON.parse(event.data);
// Add the new notification to the top of the list
this.notifications.unshift(newNotification);
});
this.eventSource.onerror = (error) => {
console.error('SSE Error:', error);
// The browser will automatically try to reconnect, but you might want
// to close it after several failed attempts.
this.eventSource.close();
};
}
},
mounted() {
// 1. Fetch initial notifications on component mount
this.refreshNotifications();
// 2. Connect to the SSE stream for real-time updates
this.connectToSse();
},
beforeDestroy() {
// VERY IMPORTANT: Close the connection when the component is destroyed
// to prevent memory leaks and orphaned server processes.
if (this.eventSource) {
console.log('Closing SSE connection.');
this.eventSource.close();
}
}
};
</script>
<style scoped>
/* Add some basic styling for the component */
.notification-container {
position: relative;
display: inline-block;
}
.bell-button {
position: relative;
background: none;
border: none;
font-size: 24px;
cursor: pointer;
}
.notification-badge {
position: absolute;
top: -5px;
right: -5px;
background-color: red;
color: white;
border-radius: 50%;
padding: 2px 6px;
font-size: 12px;
}
.notification-dropdown {
position: absolute;
right: 0;
top: 100%;
width: 300px;
background: white;
border: 1px solid #ccc;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
z-index: 1000;
}
.dropdown-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
border-bottom: 1px solid #eee;
}
.refresh-button {
font-size: 12px;
}
ul {
list-style: none;
padding: 0;
margin: 0;
max-height: 400px;
overflow-y: auto;
}
li {
padding: 10px 15px;
border-bottom: 1px solid #eee;
}
li:last-child {
border-bottom: none;
}
.no-notifications {
color: #888;
text-align: center;
}
</style>
You can now use the <notification-bell></notification-bell> component anywhere in your Vue application. You’ve successfully replaced polling with a real-time SSE push system while retaining the ability to manually refresh.