tobento / app-notifier
App notifier support.
Requires
- php: >=8.4
- psr/container: ^2.0
- symfony/vonage-notifier: ^7.3
- tobento/app: ^2.0
- tobento/app-database: ^2.0
- tobento/app-encryption: ^2.0
- tobento/app-http: ^2.0
- tobento/app-language: ^2.0
- tobento/app-mail: ^2.0
- tobento/app-migration: ^2.0
- tobento/app-queue: ^2.0
- tobento/app-translation: ^2.0
- tobento/app-user: ^2.0
- tobento/app-view: ^2.0
- tobento/js-notifier: ^1.0
- tobento/service-autowire: ^2.0
- tobento/service-clock: ^2.0
- tobento/service-collection: ^2.0
- tobento/service-notifier: ^2.0
- tobento/service-repository-storage: ^2.0
Requires (Dev)
- mockery/mockery: ^1.6
- phpunit/phpunit: ^12.3
- tobento/app-testing: ^2.0
- tobento/service-console: ^2.0
- tobento/service-container: ^2.0
- tobento/service-filesystem: ^2.0
- tobento/service-routing: ^2.0
- tobento/service-translation: ^2.0
- vimeo/psalm: ^6.13
README
App Notifier provides powerful and flexible notification support for Tobento applications.
It builds on the underlying Notifier Service and adds:
- application-level integration
- browser notifications (pull, SSE, guest)
- storage notifications
- unified formatting
- feature-based configuration
- view integration and assets
- channel discovery and availability helpers
Whether you need real-time browser updates, guest notifications, or persistent storage-based messages, App Notifier gives you a consistent API and a clean integration layer for your Tobento App.
Table of Contents
- Getting Started
- Documentation
- Credits
Getting Started
Add the latest version of the app notifier project running this command.
composer require tobento/app-notifier
Requirements
- PHP 8.4 or greater
Documentation
App
Check out the App Skeleton if you are using the skeleton.
You may also check out the App to learn more about the app in general.
Notifier Boot
The Notifier boot integrates the notifier system into your application.
It performs the following tasks:
- loads and applies the notifier configuration
- registers and implements all notifier-related interfaces
- boots the configured features
Below is an example of how to initialize the app with the Notifier boot:
use Tobento\App\AppFactory; use Tobento\App\Notifier\AvailableChannelsInterface; use Tobento\App\Notifier\Formatting\NotificationFormattersInterface; use Tobento\App\Notifier\GuestResolverInterface; use Tobento\App\Notifier\NotificationsInterface; use Tobento\App\Notifier\ReadNotificationResolverInterface; use Tobento\Service\Notifier\ChannelsInterface; use Tobento\Service\Notifier\NotifierInterface; use Tobento\Service\Notifier\QueueHandlerInterface; // Create the app $app = new AppFactory()->createApp(); // Add directories: $app->dirs() ->dir(realpath(__DIR__.'/../'), 'root') ->dir(realpath(__DIR__.'/../app/'), 'app') ->dir($app->dir('app').'config', 'config', group: 'config') ->dir($app->dir('root').'public', 'public') ->dir($app->dir('root').'vendor', 'vendor'); // Adding boots $app->boot(\Tobento\App\Notifier\Boot\Notifier::class); $app->booting(); // Service-related interfaces: $notifier = $app->get(NotifierInterface::class); $channels = $app->get(ChannelsInterface::class); $queueHandler = $app->get(QueueHandlerInterface::class); // App-related interfaces: $availableChannels = $app->get(AvailableChannelsInterface::class); $notificationFormatters = $app->get(NotificationFormattersInterface::class); $notifications = $app->get(NotificationsInterface::class); $guestResolver = $app->get(GuestResolverInterface::class); $readNotificationResolver = $app->get(ReadNotificationResolverInterface::class); // Run the app $app->run();
Notifier Config
The configuration for the notifier is located in the app/config/notifier.php file of the default App Skeleton.
This file allows you to configure all notifier-related settings, including:
- which notification channels your application should use
- which features should be enabled (Browser, BrowserStream, BrowserGuest, etc.)
- which notification formatters should be applied
- channel-specific options such as SMS DNS or browser channel names
You can adjust this configuration to match the needs of your application.
Channels
Supported Channels
By default, the following channels are supported:
-
Mail Channel
You only need to configure your mailers in the Mail Config file. -
Sms Channel - Vonage
You only need to configure the DNS settings in the Notifier Config file. -
Storage Channel
Stores notifications for later retrieval (e.g., user inbox, dashboard). -
Browser Channel
Provides browser-based notifications.
This includes:- Browser (pull-based) - authenticated users
- BrowserStream (SSE) - authenticated users, real-time
- BrowserGuest - unauthenticated users
These channels are enabled through the corresponding features in the Features section.
Available Channels
Available channels represent the channels that are currently active and usable in your application.
They can be used to:
- restrict which channels a service may use
- display channel names or titles in the UI
- customize channel titles
- sort or map channels
By default, all channels defined in the app/config/notifier.php file are available.
use Tobento\App\Notifier\AvailableChannelsInterface; $channels = $app->get(AvailableChannelsInterface::class); // Check if a channel exists: var_dump($channels->has(channel: 'sms')); // bool(true) // Get the title for a channel: var_dump($channels->titleFor(channel: 'sms')); // string(3) "Sms" // Get all channel names: var_dump($channels->names()); // array(3) {[0]=> string(4) "mail" [1]=> string(3) "sms" [2]=> string(7) "storage"} // Get all channel titles: var_dump($channels->titles()); // array(3) {[0]=> string(4) "Mail" [1]=> string(3) "Sms" [2]=> string(7) "Storage"} // Convert titles to a string: var_dump($channels->titlesToString(separator: ', ')); // string(18) "Mail, Sms, Storage" // Add or override a channel title (returns a new instance): $channels = $channels->withTitle(channel: 'sms', title: 'SMS Channel'); var_dump($channels->titleFor(channel: 'sms')); // string(11) "SMS Channel" // Map titles (returns a new instance): $channels = $channels->map(fn($title, $name) => strtoupper($title)); var_dump($channels->titlesToString(separator: ', ')); // string(26) "MAIL, SMS CHANNEL, STORAGE" // Sort channels by name (returns a new instance): $channels = $channels->sortByName(); // Sort channels by title (returns a new instance): $channels = $channels->sortByTitle(); // Count channels: var_dump($channels->count()); // int(3) // Keep only specific channels (returns a new instance): $channels = $channels->only(['sms', 'mail']); // Exclude specific channels (returns a new instance): $channels = $channels->except(['sms', 'mail']); // Iterate over channels: foreach ($channels->all() as $name => $title) {} // or simply: foreach ($channels as $name => $title) {}
Features
Browser Feature
The Browser Feature enables pull-based browser notifications for authenticated users.
Authentication is handled automatically through the application's user system.
This feature boots the following user-related components:
Tobento\App\User\Boot\Userfrom app-userTobento\App\User\Boot\Aclfrom app-userTobento\App\User\Boot\HttpUserErrorHandlerfrom app-user
Because of this, the Browser Feature works out of the box with the default user system.
If you need web-based authentication, you may install app-user-web.
The authenticated user is resolved from the request via:
$user = $request->getAttribute(UserInterface::class);
The feature then checks:
- the user must implement UserInterface
- the user must be authenticated
- the user must have the required ACL permission (unless ACL is disabled)
If any of these conditions fail, an AuthorizationException is thrown.
Requirements
This feature requires the Browser View Feature which automatically injects its required view snippet into your layout using autoRenderOnView.
Config
You can configure the Browser Feature in the Notifier Config file:
'features' => [ new Feature\Browser( // The browser channels to use for this feature. browserChannels: ['browser'], // If true, routes are localized (default: true). localizeRoute: false, // ACL is enabled by default. // Disable ACL checks only for testing. // When ACL is enabled (recommended for production), users must have // the required permissions to receive browser notifications. withAcl: false, ), ],
ACL Permissions
To receive browser notifications, a user must have the following permission:
notifications.browserUser can receive browser notifications
If using the App Backend, you can assign this permission on the Roles or Users page.
How Notifications Are Delivered
When the browser polls the route:
POST /{?locale}/notifications/browser
The feature:
- Resolves the authenticated user
- Checks ACL (unless disabled)
- Fetches unread notifications from the configured browser channels
- Marks them as read
- Returns them in the JS-Notifier format
This is handled in the browser() method of the feature.
Browser Stream Feature
The Browser Stream Feature enables real-time browser notifications using Server‑Sent Events (SSE).
Unlike the Browser Feature, which relies on polling, this feature pushes notifications to the browser instantly as they occur.
Authentication is handled automatically through the application's user system.
This feature boots the following user-related components:
Tobento\App\User\Boot\Userfrom app-userTobento\App\User\Boot\Aclfrom app-userTobento\App\User\Boot\HttpUserErrorHandlerfrom app-user
Because of this, the Browser Stream Feature works out of the box with the default user system.
If you need web-based authentication, you may install app-user-web.
The authenticated user is resolved from the request via:
$user = $request->getAttribute(UserInterface::class);
The feature then checks:
- the user must implement UserInterface
- the user must be authenticated
- the user must have the required ACL permission (unless ACL is disabled)
If any of these conditions fail, an AuthorizationException is thrown.
Requirements
This feature requires the Browser View Feature which automatically injects its required view snippet into your layout using autoRenderOnView.
Config
You can configure the Browser Stream Feature in the Notifier Config file:
'features' => [ new Feature\BrowserStream( // The browser channels to use for this feature. browserChannels: ['browser'], // If true, routes are localized (default: true). localizeRoute: false, // ACL is enabled by default. // Disable ACL checks only for testing. // When ACL is enabled (recommended for production), users must have // the required permissions to receive browser notifications. withAcl: false, ), ],
ACL Permissions
To receive browser notifications, a user must have the following permission:
notifications.browserUser can receive browser notifications
If using the App Backend, you can assign this permission on the Roles or Users page.
How Notifications Are Delivered
When the browser connects to the SSE endpoint:
GET /{?locale}/notifications/browser-stream
The feature:
- Resolves the authenticated user
- Checks ACL (unless disabled)
- Opens a persistent SSE connection
- Streams new notifications in real time
- Marks streamed notifications as delivered
This is handled in the browser() method of the feature.
Browser Guest Feature
The Browser Guest Feature enables browser notifications for guest users (non-authenticated visitors).
This is useful when your application needs to display notifications to users who are not logged in, such as storefront visitors, checkout flows, or public-facing pages.
Guest notifications are handled separately from authenticated user notifications and do not require a user account.
This feature boots the following user-related components:
Tobento\App\User\Boot\Userfrom app-userTobento\App\User\Boot\Aclfrom app-userTobento\App\User\Boot\HttpUserErrorHandlerfrom app-user
Because of this, the Browser Guest Feature works out of the box with the default user system.
If you need web-based authentication for other features, you may install app-user-web.
Guest notifications do not require an authenticated user.
Instead, the feature identifies the guest using a unique guest token stored in the session or cookie.
This feature does not require the Browser View Feature.
The feature registers its own view handler internally and will only render the snippet if:
- the user (guest) is allowed to receive browser notifications
- the
notifications.browser.guestroute exists - ACL allows access (unless ACL is disabled)
If these conditions are not met, the view is not rendered.
Config
You can configure the Browser Guest Feature in the Notifier Config file:
'features' => [ new Feature\BrowserGuest( // The browser channels to use for this feature. browserChannels: ['browser'], // The view 'notifier.browser.guest' is automatically rendered // when the specified view ('inc/head') is rendered. autoRenderOnView: 'inc/head', // You may disable auto rendering. If disabled, you must manually // include the snippet in your layout: // // <?= $view->render('notifier.browser.guest') ?> // // autoRenderOnView: null, // If true, routes are localized (default: true). localizeRoute: false, // ACL is enabled by default. // Disable ACL checks only for testing. // When ACL is enabled (recommended for production), guests must have // the required permissions to receive guest notifications. withAcl: false, ), ],
ACL Permissions
To receive browser notifications, a user must have the following permission:
notifications.browserUser can receive browser notifications
If using the App Backend, you can assign this permission on the Roles or Users page.
How Guest Notifications Are Delivered
When the browser polls the guest endpoint:
POST /{?locale}/notifications/browser-guest
The feature:
- Resolves the guest identifier (session-based or cookie-based)
- Checks ACL (unless disabled)
- Fetches unread guest notifications from the configured browser channels
- Marks them as read
- Returns them in the JS-Notifier format
This is handled in the browser() method of the feature.
Sending Notifications
See Using Guest Recipient for how to send notifications to guests.
Browser View Feature
The Browser View Feature provides the view snippet required for enabling browser notifications in your application's layout.
It does not register routes or deliver notifications - it only injects the JavaScript and HTML needed for the browser to poll or stream notifications.
This feature boots the following components:
Tobento\App\View\Boot\Viewfrom app-view
Because of this, the Browser View Feature works out of the box with the default view system.
Config
You can configure the Browser View Feature in the Notifier Config file:
'features' => [ new Feature\BrowserView( // The browser channels to use for this feature. browserChannels: ['browser'], // The view 'notifier.browser' is automatically rendered // when the specified view ('inc/head') is rendered. autoRenderOnView: 'inc/head', // You may disable auto rendering. If disabled, you must manually // include the snippet in your layout: // // <?= $view->render('notifier.browser') ?> // // autoRenderOnView: null, ), ],
What This Feature Does
The feature registers a handler for the view key:
notifier.browser
This handler:
- checks ACL (
notifications.browser) - checks whether the user is allowed to receive browser notifications
(based on authentication state and preferred notification channels) - resolves the pull and stream URLs:
notifications.browsernotifications.browser.stream
- injects the view file:
notifier/browser
If any condition fails (ACL, user preference, missing route), the snippet is not rendered.
Creating And Sending Notifications
Using General Recipients
use Tobento\Service\Notifier\Notification; use Tobento\Service\Notifier\NotifierInterface; use Tobento\Service\Notifier\Recipient; class SomeService { public function send(NotifierInterface $notifier): void { // Create a Notification that has to be sent: // using the "email" and "sms" channel $notification = new Notification( subject: 'New Invoice', content: 'You got a new invoice for 15 EUR.', channels: ['mail', 'sms'], ); // The receiver of the notification: $recipient = new Recipient( email: 'mail@example.com', phone: '15556666666', ); // Send the notification to the recipient: $notifier->send($notification, $recipient); } }
For more details, see the Notifier Service - Creating And Sending Notifications.
Example sending to the current user (authenticated or guest)
Make sure you have booted:
$app->boot(\Tobento\App\User\Boot\User::class);
If any browser features are enabled, it is automatically booted.
use Psr\Http\Message\ServerRequestInterface; use Tobento\App\User\UserInterface; use Tobento\Service\Notifier\Notification; use Tobento\Service\Notifier\NotifierInterface; use Tobento\Service\Notifier\UserRecipient; class SomeService { public function send(NotifierInterface $notifier, ServerRequestInterface $request): void { // Create a Notification to be sent using multiple channels $notification = new Notification( subject: 'Thanks for your order', content: 'Your order has been received and is being processed.', channels: ['mail', 'sms', 'browser'], ); // Get the current user (authenticated or guest): $user = $request->getAttribute(UserInterface::class); // Create the recipient for the current user: $recipient = new UserRecipient(user: $user); // Send the notification to the recipient: $notifier->send($notification, $recipient); } }
Using Guest Recipient
use Tobento\App\Notifier\GuestResolverInterface; use Tobento\Service\Notifier\GuestRecipient; use Tobento\Service\Notifier\Notification; use Tobento\Service\Notifier\NotifierInterface; class SomeService { public function send(NotifierInterface $notifier, GuestResolverInterface $guestResolver): void { // Create a Notification that has to be sent: // using the "browser" channel $notification = new Notification( subject: 'Special Offer', content: 'A new discount is available for you!', channels: ['browser'], ); // Send to the current guest session: $recipient = new GuestRecipient( id: $guestResolver->resolveId(), ); // Or send to all guest sessions: $recipient = new GuestRecipient( id: null, ); // Send the notification to the recipient: $notifier->send($notification, $recipient); } }
For more details, see the Notifier Service - Guest Recipient.
For more details, see the Notifier Service - Creating And Sending Notifications.
Browser Message
Use the browser message type to send notifications to the in-browser js-notifier UI.
$notification = new Notification() ->addMessage('browser', new Message\Browser([ 'title' => 'Optional title', // js-notifier: title 'message' => 'You received a new order.', // js-notifier: text 'status' => 'warning', // js-notifier: status // Optional behavior 'autotimeout' => 500, // js-notifier: autotimeout //'autotimeout' => null, // never closes 'showCloseButton' => false, // default: true // Action button 'action_text' => 'View Order', // js-notifier: action.title 'action_route' => 'orders.view', // js-notifier: action.url 'action_route_parameters' => ['id' => 555], 'action_attributes' => [ // js-notifier: action.classes 'class' => 'button', ], ]));
js-notifier parameters
You can find the full list of supported js-notifier parameters here: https://github.com/tobento-ch/js-notifier#parameters
Formatters
If you use action parameters (action_text, action_route, etc.), ensure the General Notification Formatter is enabled.
This formatter is enabled by default.
Using AbstractNotification
use Tobento\Service\Notifier\AbstractNotification; use Tobento\Service\Notifier\Message; use Tobento\Service\Notifier\RecipientInterface; class SampleNotification extends AbstractNotification implements Message\ToBrowser { /** * Returns the browser message. * * @param RecipientInterface $recipient * @param string $channel The channel name. * @return Message\BrowserInterface */ public function toBrowser(RecipientInterface $recipient, string $channel): Message\BrowserInterface { return new Message\Browser(data: [ 'title' => 'Optional title', 'message' => 'You received a new order.', 'status' => 'warning', // Optional behavior 'autotimeout' => 500, 'action_text' => 'View Order', 'action_route' => 'orders.view', 'action_route_parameters' => ['id' => 555], ]); } }
Queuing Notifications
Sending notifications can be a time-consuming task. To avoid delays during request handling, you can queue notification messages for background processing.
To queue a notification, simply add the Queue Parameter to your message:
Example
use Tobento\Service\Notifier\NotifierInterface; use Tobento\Service\Notifier\Notification; use Tobento\Service\Notifier\Recipient; use Tobento\Service\Notifier\Parameter\Queue; class SomeService { public function send(NotifierInterface $notifier): void { // Create a Notification that has to be sent // using the "mail" and "sms" channels: $notification = new Notification( subject: 'New Invoice', content: 'You got a new invoice for 15 EUR.', channels: ['mail', 'sms'], )->parameter(new Queue( // You may specify the queue to be used: name: 'secondary', // You may specify a delay in seconds: delay: 30, // You may specify how many times to retry: retry: 3, // You may specify a priority: priority: 100, // You may specify if you want to encrypt the message: encrypt: true, )); // The receiver of the notification: $recipient = new Recipient( email: 'mail@example.com', phone: '15556666666', ); // Send the notification to the recipient: $notifier->send($notification, $recipient); } }
The Notifier Boot automatically boots the App Queue Boot, enabling queue support out of the box.
You only need to configure your queues in the Queue Config file.
Notification Formatters
Notification formatters may be used to format notifications stored by the Storage Channel or the Browser Channel.
General Notification Formatter
You may use the general formatter, which uses the following message data:
use Tobento\Service\Notifier\Message; use Tobento\Service\Notifier\Notification; // Storage $notification = new Notification() ->addMessage('storage', new Message\Storage([ 'message' => 'You received a new order.', 'action_text' => 'View Order', 'action_route' => 'orders.view', 'action_route_parameters' => ['id' => 555], ])); // Browser $notification = new Notification() ->addMessage('browser', new Message\Browser([ 'message' => 'You received a new order.', 'action_text' => 'View Order', 'action_route' => 'orders.view', 'action_route_parameters' => ['id' => 555], ]));
In app/config/notifier.php:
'formatters' => [ \Tobento\App\Notifier\Formatting\GeneralNotificationFormatter::class, ],
The action_route and action_route_parameters values will be used to generate
the message URL if you have installed the App Http - Routing Boot.
The message and action_text values will be translated by the formatter if you
have installed the App Translation Boot.
General Formatter Example
You may create your own general formatter for notifications:
use Tobento\App\Notifier\Formatting\Notification; use Tobento\App\Notifier\Formatting\NotificationFormatterInterface; class GeneralNotificationFormatter implements NotificationFormatterInterface { public function format(Notification $notification): Notification { // General data available: $id = $notification->id(); $name = $notification->name(); $recipientId = $notification->recipientId(); $recipientType = $notification->recipientType(); $readAt = $notification->get('read_at'); $createdAt = $notification->get('created_at'); $subject = $notification->get('data.subject', ''); $content = $notification->get('data.content', ''); // Format: return $notification ->withMessage($subject.': '.$content); } }
In app/config/notifier.php:
'formatters' => [
GeneralNotificationFormatter::class,
],
Creating Notification Formatters
You may create a specific formatters to format only certain notifications:
use Tobento\App\Notifier\Formatting\NotificationFormatterInterface; use Tobento\App\Notifier\Formatting\Notification; use Tobento\Service\Routing\RouterInterface; use Tobento\Service\Translation\TranslatorInterface; class NewOrderNotificationFormatter implements NotificationFormatterInterface { public function __construct( protected TranslatorInterface $translator, protected RouterInterface $router, ) {} public function format(Notification $notification): Notification { // Only format specific notifications: if (!$notification->name() instanceof NewOrderNotification) { return $notification; } // Stop further formatters from modifying this notification: $notification->stopPropagation(true); // Retrieve specific message data: $orderId = $notification->get('data.order_id'); // Format: return $notification ->withMessage($this->translator->trans('New order received')) ->withAddedAction( text: $this->translator->trans('View Order'), url: $this->router->url('orders.view', ['id' => $orderId]), ); } }
In app/config/notifier.php:
'formatters' => [
NewOrderNotificationFormatter::class,
GeneralNotificationFormatter::class,
],
Custom Notifications
You may easily customize notifications by defining them in the app/config/notifier.php file:
use Tobento\Service\Notifier\NotificationInterface; use Tobento\Service\Notifier\Parameter\Queue; 'notifications' => [ // Using a custom notification: UserRegisterNotification::class => CustomUserRegisterNotification::class, // Using a notification factory: UserRegisterNotification::class => UserRegisterNotificationFactory::class, // Using a closure: UserRegisterNotification::class => function (UserRegisterNotification $notification): NotificationInterface { return $notification->parameter(new Queue()); }, // If named notification: 'register' => CustomUserRegisterNotification::class, ],
Creating Custom Notification
use Tobento\Service\Notifier\AbstractNotification; use Tobento\Service\Notifier\RecipientInterface; use Tobento\Service\Notifier\Message; class CustomUserRegisterNotification extends AbstractNotification implements Message\ToSms { public function __construct( protected UserRegisterNotification $notification, // ... ) {} public function toSms(RecipientInterface $recipient, string $channel, SomeService $service): Message\SmsInterface { return new Message\Sms( subject: 'Thanks for your registration', ); } }
Creating Notification Factory
use Tobento\App\Notifier\NotificationFactoryInterface; use Tobento\Service\Notifier\NotificationInterface; class UserRegisterNotificationFactory implements NotificationFactoryInterface { public function createNotification(NotificationInterface $notification): NotificationInterface { // create custom notification: // or modify original: return $notification; } }
Console
Clear Notifications Command
If you have installed the App Console, you may clear all notifications from channels that support it, such as the Storage Channel and Browser Channel, using the user:notifications:clear command.
Clearing all notifications from all supported channels
php ap user:notifications:clear
Clearing all notifications from all specific channels
php ap user:notifications:clear --channel=foo --channel=bar
Available Options
| Option | Description |
|---|---|
--channel=name |
The name(s) of the channel(s) to clear. |
--read-only |
Clears only notifications that are marked as read. |
--unread-only |
Clears only notifications that are unread. |
--older-than-days=10 |
Clears notifications older than the specified number of days. |
App Notification Integration
You may also want to explore the App Notification package, which provides seamless integration of notifications into your application.