NotificationUI Plugin
Introduction
The NotificationUI plugin provides UI components for the CakePHP Notification system with real-time broadcasting support. It includes a modern notification bell widget with dropdown or side panel display modes.
The plugin uses Alpine.js for reactive state management and provides a modern notification bell widget with dropdown or side panel display modes, automatic polling for new notifications, real-time broadcasting support, and a complete JavaScript API for managing notifications.
Installation
Requirements
- PHP 8.1+
- CakePHP 5.0+
- CakePHP Notification Plugin
- Alpine.js (automatically loaded by the plugin)
Load the Plugin
In config/plugins.php:
'Crustum/Notification' => [
'bootstrap' => true,
'routes' => true,
],
'Crustum/NotificationUI' => [],Run Migrations
bin/cake migrations migrate -p Crustum/NotificationAdd to Layout
Add CSRF token to your layout's <head>:
<meta name="csrfToken" content="<?= $this->request->getAttribute('csrfToken') ?>">Add notification bell to your navigation using the Cell:
<li class="nav-item">
<?= $this->cell('Crustum/NotificationUI.NotificationBell', [
'mode' => 'panel',
]) ?>
</li>Bell Widget
The NotificationBellCell automatically loads required CSS/JS (including Alpine.js) and initializes the reactive notification store. The widget uses Alpine.js for reactive state management and server-side PHP templates for rendering.
Basic usage:
<?= $this->cell('Crustum/NotificationUI.NotificationBell') ?>The Cell automatically calculates unread count from the database if not provided, making it ideal for server-side rendering scenarios.
With options:
<?= $this->cell('Crustum/NotificationUI.NotificationBell', [
'mode' => 'panel',
'position' => 'left',
'theme' => 'dark',
'pollInterval' => 60000,
]) ?>Display Modes
Dropdown Mode (Default)
Traditional dropdown menu attached to the bell icon:
<?= $this->cell('Crustum/NotificationUI.NotificationBell', [
'mode' => 'dropdown',
'position' => 'right',
]) ?>Panel Mode
Sticky side panel (like Filament Notifications):
<?= $this->cell('Crustum/NotificationUI.NotificationBell', [
'mode' => 'panel',
]) ?>Configuration Options
<?= $this->cell('Crustum/NotificationUI.NotificationBell', [
'mode' => 'panel',
'position' => 'right',
'theme' => 'dark',
'pollInterval' => 60000,
'enablePolling' => true,
'perPage' => 20,
]) ?>Options:
mode- 'dropdown' or 'panel' (default: 'dropdown')position- 'left' or 'right' (default: 'right', dropdown only)theme- 'light', 'dark', ornullfor auto-detect (default: auto-detect)pollInterval- Poll every N milliseconds (default: 30000)enablePolling- Enable/disable database polling (default: true)perPage- Notifications per page (default: 10)unreadCount- Initial unread count (default:null- automatically calculated by Cell from database)markReadOnClick- Mark notification as read when clicked (default: true)userId- User ID for broadcasting (default:null- extracted from authenticated identity)userName- User name for broadcasting (default:null)broadcasting- Broadcasting configuration array orfalseto disable (default:false)
Real-Time Broadcasting
Enable WebSocket broadcasting for instant notification delivery. Supports both Pusher and Mercure broadcasters.
Pusher Broadcasting
NOTE
Pusher Broadcasting requires the crustum/notification-broadcasting and crustum/broadcasting plugins. The broadcaster option defaults to 'pusher'.
<?php $authUser = $this->request->getAttribute('identity'); ?>
<?= $this->cell('Crustum/NotificationUI.NotificationBell', [
'mode' => 'panel',
'enablePolling' => true,
'broadcasting' => [
'broadcaster' => 'pusher',
'userId' => $authUser->getIdentifier(),
'userName' => $authUser->username ?? 'User',
'pusherKey' => 'app-key',
'pusherHost' => '127.0.0.1',
'pusherPort' => 8080,
'pusherCluster' => 'mt1',
],
]) ?>Mercure Broadcasting
NOTE
Mercure Broadcasting requires the crustum/notification-broadcasting and crustum/mercure-broadcasting plugins.
<?php $authUser = $this->request->getAttribute('identity'); ?>
<?= $this->cell('Crustum/NotificationUI.NotificationBell', [
'mode' => 'panel',
'enablePolling' => true,
'broadcasting' => [
'broadcaster' => 'mercure',
'userId' => $authUser->getIdentifier(),
'userName' => $authUser->username ?? 'User',
'mercureUrl' => '/.well-known/mercure',
'authEndpoint' => '/broadcasting/auth',
],
]) ?>This mode combines database persistence with real-time WebSocket delivery for the best user experience.
Broadcasting Only (No Database)
Pusher:
<?= $this->cell('Crustum/NotificationUI.NotificationBell', [
'enablePolling' => false,
'broadcasting' => [
'broadcaster' => 'pusher',
'userId' => $authUser->getIdentifier(),
'userName' => $authUser->username ?? 'User',
'pusherKey' => env('PUSHER_APP_KEY'),
'pusherHost' => env('PUSHER_HOST', '127.0.0.1'),
'pusherPort' => env('PUSHER_PORT', 8080),
'pusherCluster' => env('PUSHER_CLUSTER', 'mt1'),
],
]) ?>Mercure:
<?= $this->cell('Crustum/NotificationUI.NotificationBell', [
'enablePolling' => false,
'broadcasting' => [
'broadcaster' => 'mercure',
'userId' => $authUser->getIdentifier(),
'mercureUrl' => env('MERCURE_PUBLIC_URL', '/.well-known/mercure'),
'authEndpoint' => '/broadcasting/auth',
],
]) ?>Notification Data Fields
When creating notifications, use these fields in your toDatabase() method:
Required:
title- Notification titlemessage- Notification message
Optional:
action_url- Makes notification clickable (redirects on click)icon- Built-in SVG icon:bell,post,user,message,alert,check,infoicon_class- CSS class:fa fa-bell,bi bi-bell,ti ti-bellactions- Array of action objects withname,label,url,event,icon, etc.
Action Object Structure:
name- Action identifierlabel- Display texturl- URL to navigate to (or useeventfor custom events)event- Custom event name to dispatchicon- Icon class (e.g.,fa fa-check)colorortype- Button style (success,danger,warning,info)openInNewTab- Open URL in new tab (default: false)shouldClose- Close dropdown/panel after action (default: false)isDisabled- Disable the action button
Example:
public function toDatabase(): array
{
return [
'title' => 'Order Shipped',
'message' => "Your order #{$this->order->id} has been shipped!",
'action_url' => "/orders/{$this->order->id}",
'icon' => 'post',
'actions' => [
[
'name' => 'view',
'label' => 'View Order',
'url' => "/orders/{$this->order->id}",
'icon' => 'fa fa-eye',
],
[
'name' => 'track',
'label' => 'Track Package',
'url' => "/orders/{$this->order->id}/track",
'icon' => 'fa fa-truck',
'openInNewTab' => true,
],
],
];
}API Endpoints
All endpoints return JSON and require authentication:
GET /notification/notifications.json- List all (paginated)GET /notification/notifications/unread.json- Get unreadGET /notification/notifications/{id}.json- Get singlePATCH /notification/notifications/{id}/read.json- Mark as readPATCH /notification/notifications/mark-all-read.json- Mark all as readDELETE /notification/notifications/{id}.json- DeleteDELETE /notification/notifications.json- Delete all
Query Parameters
page- Page number (default: 1)limit- Results per page (default: 20)status- Filter:all,read,unread(default: all)type- Filter by notification class name
JavaScript Events
Listen for notification events. The system dispatches custom events for notification lifecycle:
window.addEventListener('notification:marked-read', (e) => {
console.log('Notification marked as read:', e.detail.notificationId);
});
window.addEventListener('notification:all-marked-read', () => {
console.log('All notifications marked as read');
});
window.addEventListener('notification:received', (e) => {
console.log('New notification:', e.detail);
});
window.addEventListener('notification:deleted', (e) => {
console.log('Notification deleted:', e.detail.notificationId);
});Alpine.js Store Events
The Alpine.js store also provides reactive updates. Access the store to observe changes:
// Watch for store changes
Alpine.effect(() => {
const store = Alpine.store('notifications');
console.log('Unread count:', store.unreadCount);
console.log('Items:', store.items);
});Template Overloading
The notification system uses server-side PHP templates with Alpine.js directives for reactive rendering. The NotificationBellCell renders elements internally, so customization is done by overriding element templates.
PHP Element Templates
The notification system uses server-side PHP templates with Alpine.js directives for reactive rendering. Override templates by creating files in your app's templates/element/ directory:
templates/element/Crustum/NotificationUI/notifications/
├── templates.php # Alpine.js notification templates (loading, empty, items, load more)
└── bell_icon.php # Bell icon element (used by Cell)Override notification templates:
Copy plugins/NotificationUI/templates/element/notifications/templates.php to:
templates/element/Crustum/NotificationUI/notifications/templates.phpThis template contains Alpine.js directives for:
- Loading state (
x-if="isLoading") - Empty state (
x-if="!isLoading && items.length === 0") - Notification items (
x-for="notification in items") - Load more button (
x-if="hasMore && !isLoading")
Example override:
<?php
/**
* Custom notification templates
*/
?>
<template x-if="isLoading">
<div class="custom-loading">Loading notifications...</div>
</template>
<template x-for="notification in items" :key="notification.id">
<div class="custom-notification-item"
x-data="notificationItem(getNotificationData(notification))">
<div class="custom-title" x-text="title"></div>
<div class="custom-message" x-text="message"></div>
</div>
</template>Alpine.js Store Access
Access the notification store programmatically:
// Get the Alpine.js store
const store = Alpine.store('notifications');
// Add a notification
store.addNotification({
id: 123,
title: 'New Notification',
message: 'This is a test',
read_at: null,
created_at: new Date().toISOString()
});
// Mark as read
await store.markAsRead(123);
// Mark all as read
await store.markAllAsRead();
// Load more notifications
await store.loadMore();
// Toggle dropdown/panel
store.toggle();Using CakeNotification Builder
The CakeNotification fluent API still works and integrates with the Alpine.js store:
CakeNotification.make()
.title('Order Shipped')
.message('Your order has been shipped!')
.actionUrl('/orders/123')
.send();Alpine.js Architecture
The notification system is built on Alpine.js for reactive state management. The architecture consists of:
Alpine.js Store
The notifications store (Alpine.store('notifications')) provides:
items- Array of notification objectsunreadCount- Reactive unread countisLoading- Loading statehasMore- Whether more notifications are availableisOpen- Whether dropdown/panel is opencurrentPage- Current pagination page
Alpine.js Components
notificationBell- Bell icon component with theme detectionnotificationList- List container with paginationnotificationItem- Individual notification item with actions
Broadcasting Modules
Broadcasting modules (PusherModule, MercureModule) extend BroadcastingBase and automatically integrate with the Alpine.js store for real-time updates.