The app notification provides the following features:
- custom notifications for replacing notifications sent by the app - notifier
- manage and sending newsletters to newsletter subscribers
- tracking clicks and events
- reports
- Getting Started
- Documentation
- App
- Notification Boot
- Features
- Channels
- Placeholders
- Console
- Custom Loggers
- Learn More
- Credits
Add the latest version of the app notification project running this command.
composer require tobento/app-notification
- PHP 8.4 or greater
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.
The notification boot does the following:
- installs and loads notification config
- implements notification interfaces
- boots features configured in notification config
use Tobento\App\AppFactory;
use Tobento\App\Notification\Channel\ChannelsInterface;
use Tobento\App\Notification\Newsletter\SubscriberRepositoryInterface;
use Tobento\App\Notification\NotificationRepositoryInterface;
use Tobento\App\Notification\NotificationTypesInterface;
// 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\Notification\Boot\Notification::class);
$app->booting();
// Implemented interfaces:
$channels = $app->get(ChannelsInterface::class);
$subscriberRepository = $app->get(SubscriberRepositoryInterface::class);
$notificationRepository = $app->get(NotificationRepositoryInterface::class);
$notificationTypes = $app->get(NotificationTypesInterface::class);
// Run the app
$app->run();You may install the App Backend and boot the notification in the backend app.
The configuration for the notification is located in the app/config/notification.php file at the default App Skeleton config location where you can configure features and more.
The notifications feature provides a page where users can create, edit and delete any notification types registered such as newsletters notifications, custom notifications or any other custom notification types created.
Config
In the config file you can configure this feature:
'features' => [
new Feature\Notifications(
// A menu name to show the notification link or null if none.
menu: 'main',
menuLabel: 'Notifications',
// A menu parent name (e.g. 'system') or null if none.
menuParent: null,
// you may disable the ACL while testing for instance,
// otherwise only users with the right permissions can access the page.
withAcl: false,
),
],ACL Permissions
notificationsUser can access notificationsnotifications.createUser can create notificationsnotifications.editUser can edit notificationsnotifications.deleteUser can delete notificationsnotifications.sendUser can send notifications
If using the App Backend, you can assign the permissions in the roles or users page.
A preview link is available on the index page and edit page of the notifications if supported by the notification type.
To support previewing, you will use the following two methods on any notification type you want to support it.
Example from the NewsletterNotification::class:
namespace Tobento\App\Notification\Type;
use Psr\Container\ContainerInterface;
use Tobento\App\Notification\NotificationEntityInterface;
use Tobento\App\Notification\NotificationTypeInterface;
use Tobento\App\Notification\ReportAwareInterface;
use Tobento\Service\Notifier\NotificationInterface;
class NewsletterNotification implements NotificationTypeInterface, ReportAwareInterface
{
// ...
/**
* Returns true if a preview notification exists, otherwise false.
*
* @return bool
*/
public function hasPreviewNotification(): bool
{
return true;
}
/**
* Create a preview notification.
*
* @param ContainerInterface $container
* @param NotificationEntityInterface $entity
* @return null|NotificationInterface
*/
public function createPreviewNotification(
ContainerInterface $container,
NotificationEntityInterface $entity,
): null|NotificationInterface {
return $this->createNotification(container: $container, entity: $entity);
}
}You may check out the Tobento\App\Notification\Type\ResetPasswordNotificationType::class file too.
On the notifications index page, each notification may have a link to send the notification for reviewing if supported by the notification type.
Sending notifications is supported:
- when a custom notification has a Preview Notification which will be used
- when any other notification the default notification will be used.
On the notifications index page, each notification may have a link to view reports about sent notifications if supported by the notification type. Reports may differ depending on its notification type. For instance, newsletters notifications, will display the total number of notifications sent, the number of recipients who clicked links if tracking enabled and more.
To support reports, the notification type must implement the ReportAwareInterface and have configured cards using the configureReportCards method.
Example from the NewsletterNotification::class:
namespace Tobento\App\Notification\Type;
use Tobento\App\AppInterface;
use Tobento\App\Card\Cards;
use Tobento\App\Notification\NotificationEntityInterface;
use Tobento\App\Notification\NotificationTypeInterface;
use Tobento\App\Notification\ReportAwareInterface;
class NewsletterNotification implements NotificationTypeInterface, ReportAwareInterface
{
// ...
/**
* Configure report cards.
*
* @param AppInterface $app
* @param NotificationEntityInterface $entity
* @return CardsInterface
*/
public function configureReportCards(AppInterface $app, NotificationEntityInterface $entity): CardsInterface
{
$cards = new Cards(container: $app->container());
// adding cards...
return $cards;
}
}For further details, please refer to the source file.
You may visit the App Card link to find out more about cards.
Available Cards
\Tobento\App\Notification\Card\MostClickedLinksCard::class, displays the most clicked links\Tobento\App\Notification\Card\MostTrackedEventsCard::class, displays the most tracked events\Tobento\App\Notification\Card\RecipientsClickCountCard::class, displays the recipients click count by channels
Adding Custom Cards Using The ReportCards::class
You can add custom cards onto the ReportCards::class class which will be displayed together with the cards defined in the configureReportCards method on the notification types. Use the app on method to add cards on demand only:
use Tobento\App\Notification\Card\ReportCards;
$app->on(ReportCards::class, static function(ReportCards $cards) use ($app): void {
// you may add cards only for specific notfication types:
if ($cards->notificationEntity()->type() === 'newsletter') {
$cards->add(
name: 'orders',
card: $app->make(CustomOrdersCard::class, ['entity' => $cards->notificationEntity(), 'priority' => 100]),
);
}
});Adding Or Modifying Cards For Specific Notification Types
You can modify or add cards by simply extending the notification type:
use Tobento\App\AppInterface;
use Tobento\App\Card\Cards;
use Tobento\App\Notification\NotificationEntityInterface;
use Tobento\App\Notification\Type\NewsletterNotification;
class CustomizedNewsletterNotification extends NewsletterNotification
{
/**
* Configure report cards.
*
* @param AppInterface $app
* @param NotificationEntityInterface $entity
* @return CardsInterface
*/
public function configureReportCards(AppInterface $app, NotificationEntityInterface $entity): CardsInterface
{
$cards = new Cards(container: $app->container());
// adding cards...
return $cards;
}
}Do not forget to register the customized notification type in the notification config file:
'interfaces' => [
NotificationTypesInterface::class =>
static function(Channel\ChannelsInterface $channels): NotificationTypesInterface {
return new NotificationTypes(
new CustomizedNewsletterNotification(
// Filter the channels you want to support:
channels: $channels->only('mail', 'sms'),
// You may disable tracking options:
trackingOptions: false, // true default
// You may change the block editor:
blockEditorName: 'mail', // default
),
);
},
],You may create custom notifications for replacing notifications send by the App - Notifier.
Lets create a custom notification type for the App User Web - Forgot Password Notification by extending the AbstractCustomNotification::class:
use Psr\Container\ContainerInterface;
use Tobento\App\Notification\Placeholder\Placeholder;
use Tobento\App\Notification\Placeholder\Placeholders;
use Tobento\App\Notification\Placeholder\PlaceholdersInterface;
use Tobento\App\Notification\Type\AbstractCustomNotification;
use Tobento\App\User\Web\Notification\ResetPassword;
use Tobento\App\Notification\NotificationEntityInterface;
use Tobento\Service\Notifier\NotificationInterface;
class ResetPasswordNotificationType extends AbstractCustomNotification
{
/**
* Returns the name.
*
* @return string
*/
public function name(): string
{
return 'reset.password';
}
/**
* Returns the title.
*
* @return string
*/
public function title(): string
{
return 'Reset Password';
}
/**
* Returns the notification name which to replace or null if not supported.
*
* @return null|string
*/
public function replacesNotification(): null|string
{
return ResetPassword::class;
}
/**
* Returns true if a preview notification exists, otherwise false.
*
* @return bool
*/
public function hasPreviewNotification(): bool
{
return false;
}
/**
* Create a preview notification.
*
* @param ContainerInterface $container
* @param NotificationEntityInterface $entity
* @return null|NotificationInterface
*/
public function createPreviewNotification(ContainerInterface $container, NotificationEntityInterface $entity): null|NotificationInterface
{
// you may create a notification for preview.
// See file: \Tobento\App\Notification\Type\ResetPasswordNotificationType::class
return null;
}
/**
* Returns the configured placeholders.
*
* @return PlaceholdersInterface
*/
public function configurePlaceholders(): PlaceholdersInterface
{
return new Placeholders(
new Placeholder(
key: 'url',
value: fn (ResetPassword $notification): string => $notification->url(),
description: The url where users can reset theirs password.
),
new Placeholder(
key: 'expires.in.seconds',
value: fn (ResetPassword $notification): int => $notification->token()->expiresAt()->getTimestamp() - time(),
),
);
}
}Check out the Placeholders to learn more about it.
Once created, you will need to register the custom notification type so as to manage and replace custom notifications in the config file:
'interfaces' => [
NotificationTypesInterface::class =>
static function(Channel\ChannelsInterface $channels): NotificationTypesInterface {
return new NotificationTypes(
new ResetPasswordNotificationType(
// Filter the channels you want to support:
channels: $channels->only('mail', 'sms'),
// Define the supported app ids:
supportedAppIds: ['frontend'],
// You may customize the executions the user can select:
allowedExecutions: ['send', 'queue', 'skip'], // default
// You may customize the default execution of the notification:
defaultExecution: 'send',
// You may customize the queues the user can select:
allowedQueueNames: ['file'], // default
// You may disable tracking options:
trackingOptions: false, // true default
// You may change the block editor:
blockEditorName: 'mail', // default
),
);
},
],Custom notifications can be managed by simply enabling the Notifications Feature. Furthermore, the Mail Editor - App Block is used to create the mail messages with, which you may configure to fit your requirements.
Add the ReplacesCustomNotifications::class feature to replace notifications with your custom notifications in the app(s) you support. Only notifications with the status active will be replaced.
Config
In the config file you can configure this feature:
'features' => [
Feature\ReplacesCustomNotifications::class,
],You may consider using the App Backend to manage custom notifications.
In addition, you may check out the Apps bundle if you want to support multiple apps.
Register the users notification type so as to manage and sending user notifications in the config file:
'interfaces' => [
NotificationTypesInterface::class =>
static function(
Channel\ChannelsInterface $channels,
Newsletter\TopicsInterface $topics,
): NotificationTypesInterface {
return new NotificationTypes(
new Type\UsersNotification(
// Filter the channels you want to support:
channels: $channels, //->only('mail'),
// You may disable tracking options:
trackingOptions: false, // true default
// You may change the block editor:
blockEditorName: 'mail', // default
// You may change the name:
name: 'customers', // 'users' is default
// You may change the title:
title: 'Customers',
// You may change the user repository with a custom one:
userRepository: CustomerRepositoryInterface::class,
),
);
},
],User notifications can be easily managed by enabling the Notifications Feature.
You can deliver user notifications created from the web interface by using the Notifications Send Command. There are two main approaches:
Warning Use caution with the
--intervaloption when sending notifications immediately. To avoid timeouts, a maximum of 20 is recommended. When queueing notifications, the interval can be set much higher, with a maximum of 20000 recommended.
Option 1: Using App Schedule
Install the App Schedule bundle and configure a command task:
use Tobento\Service\Schedule\Task;
use Butschster\CronExpression\Generator;
use Butschster\CronExpression\Parts\Hours\BetweenHours;
use Butschster\CronExpression\Parts\Minutes\EveryMinute;
$schedule->task(
new Task\CommandTask(
command: 'notifications:send',
// lets queue 100 active user notifications at a time to the queue file.
input: [
'--interval' => 100,
'--queue' => 'file',
'--type' => ['users', 'customers'], // depending on the type name
'--status' => 'active',
],
)
// schedule task:
->cron(
Generator::create()
->set(new EveryMinute(1))
->set(new BetweenHours(8, 16)
)
);Option 2: Using App Task
Install the App Task bundle and register the task in the task config file. This allows users to schedule the task directly from the web interface:
use Tobento\App\Notification\Task\SendNotificationRegistry;
'registries' => [
'newsletter.send' => new SendNotificationRegistry(
name: 'Send User Notifications',
// You may customize the statuses the user can select to send only:
allowedStatuses: ['active'], // default
// You may customize the notification types the user can select to send:
allowedTypes: ['users', 'customers'],
// You may customize the max. interval the user can to send:
maxAllowedIntervalToSend: 20, // default
// You may customize the max. interval the user can to queue:
maxAllowedIntervalToQueue: 20000, // default
// You may customize the queues the user can select:
allowedQueueNames: ['file'], // default
// You may add task parameters to be always processed:
parameters: [
new \Tobento\Service\Schedule\Parameter\WithoutOverlapping(),
],
// Define the supported apps where the task can be run:
supportedAppIds: ['root', 'backend'],
),
]Alternatively, you can use the Command Task Registry:
'registries' => [
'user.notifications.send' => new Registry\CommandTask(
name: 'Queues 100 user notifications at a time',
command: 'notifications:send',
input: [
'--interval' => 100,
'--queue' => 'file',
'--type' => ['users', 'customers'],
],
// You may add task parameters to be always processed:
parameters: [
new \Tobento\Service\Schedule\Parameter\WithoutOverlapping(),
],
// Define the supported apps where the task can be run:
supportedAppIds: ['root', 'backend'],
),
]Register the newsletter notification type so as to manage and sending newsletter notifications in the config file:
'interfaces' => [
NotificationTypesInterface::class =>
static function(
Channel\ChannelsInterface $channels,
Newsletter\TopicsInterface $topics,
): NotificationTypesInterface {
return new NotificationTypes(
new Type\NewsletterNotification(
// Filter the channels you want to support:
channels: $channels->only('mail', 'sms'),
// You may define the topics the newsletter covers to choose from.
// You may change its titles:
topics: $topics->withTitle('products.new', 'New Products'),
// Or null if no topics at all:
topics: null,
// You may disable tracking options:
trackingOptions: false, // true default
// You may change the block editor:
blockEditorName: 'mail', // default
// You may change the days the unsubscribe link will expire:
unsubscribeLinkExpiresInDays: 30, // default
),
);
},
],Newsletter notifications can be managed by simply enabling the Notifications Feature.
You can deliver newsletters created from the web interface by using the Notifications Send Command. There are two main approaches:
Warning Use caution with the
--intervaloption when sending notifications immediately. To avoid timeouts, a maximum of 20 is recommended. When queueing notifications, the interval can be set much higher, with a maximum of 20000 recommended.
Option 1: Using App Schedule
Install the App Schedule bundle and configure a command task:
use Tobento\Service\Schedule\Task;
use Butschster\CronExpression\Generator;
use Butschster\CronExpression\Parts\Hours\BetweenHours;
use Butschster\CronExpression\Parts\Minutes\EveryMinute;
$schedule->task(
new Task\CommandTask(
command: 'notifications:send',
// lets queue 100 active newsletter notifications at a time to the queue file.
input: [
'--interval' => 100,
'--queue' => 'file',
'--type' => ['newsletter'],
'--status' => 'active',
],
)
// schedule task:
->cron(
Generator::create()
->set(new EveryMinute(1))
->set(new BetweenHours(8, 16)
)
);Option 2: Using App Task
Install the App Task bundle and register the task in the task config file. This allows users to schedule the task directly from the web interface:
use Tobento\App\Notification\Task\SendNotificationRegistry;
'registries' => [
'newsletter.send' => new SendNotificationRegistry(
name: 'Send Newsletters',
// You may customize the statuses the user can select to send only:
allowedStatuses: ['active'], // default
// You may customize the notification types the user can select to send:
allowedTypes: ['newsletter'], // default
// You may customize the max. interval the user can to send:
maxAllowedIntervalToSend: 20, // default
// You may customize the max. interval the user can to queue:
maxAllowedIntervalToQueue: 20000, // default
// You may customize the queues the user can select:
allowedQueueNames: ['file'], // default
// You may add task parameters to be always processed:
parameters: [
new \Tobento\Service\Schedule\Parameter\WithoutOverlapping(),
],
// Define the supported apps where the task can be run:
supportedAppIds: ['root', 'backend'],
),
]Alternatively, you can use the Command Task Registry:
'registries' => [
'newsletter.send' => new Registry\CommandTask(
name: 'Queues 100 Newsletters at a time',
command: 'notifications:send',
input: [
'--interval' => 100,
'--queue' => 'file',
'--type' => ['newsletter'],
],
// You may add task parameters to be always processed:
parameters: [
new \Tobento\Service\Schedule\Parameter\WithoutOverlapping(),
],
// Define the supported apps where the task can be run:
supportedAppIds: ['root', 'backend'],
),
]The newsletter subscribers feature provides a page where users can create, edit and delete newsletter subscribers to send newsletters using the Newsletter Feature.
Config
In the config file you can configure this feature:
'features' => [
new Feature\NewsletterSubscribers(
// A menu name to show the newsletter subscribers link or null if none.
menu: 'main',
menuLabel: 'Newsletter Subscribers',
// A menu parent name (e.g. 'system') or null if none.
menuParent: null,
// you may disable the ACL while testing for instance,
// otherwise only users with the right permissions can access the page.
withAcl: false,
),
],ACL Permissions
newsletter-subscribersUser can access newsletter subscribersnewsletter-subscribers.createUser can create newsletter subscribersnewsletter-subscribers.editUser can edit newsletter subscribersnewsletter-subscribers.deleteUser can delete newsletter subscribers
If using the App Backend, you can assign the permissions in the roles or users page.
Newsletter Subscribers can be managed by simply enabling this feature and using the web interface.
Using The Subscriber Repository
use Tobento\App\Notification\Newsletter\SubscriberRepositoryInterface;
use Tobento\App\User\UserInterface;
use Tobento\Service\User\AddressInterface;
class SomeService
{
public function demoWithUser(SubscriberRepositoryInterface $subscriberRepository): void
{
// Subscribe user returning the subscribed subscriber or null.
// If already subscribed it updates subscription:
$subscriber = $subscriberRepository->subscribeUser($user); // UserInterface
// You may add additional attributes:
$subscriber = $subscriberRepository->subscribeUser(
user: $user,
attributes: ['status' => 'active'], // unconfirmed is default status
);
// Unsubscribe user returning the unsubscribed subscriber or null if none unsubscribed:
$unsubscribedSubscriber = $subscriberRepository->unsubscribeUser($user); // UserInterface
// You may check if the given user has already been subscribed returning a boolean:
$subscribed = $subscriberRepository->hasSubscribedUser($user); // UserInterface
}
public function demoWithAddress(SubscriberRepositoryInterface $subscriberRepository): void
{
// Subscribe address returning the subscribed subscriber or null.
// If already subscribed it updates subscription:
$subscriber = $subscriberRepository->subscribeAddress($address); // AddressInterface
// You may add additional attributes:
$subscriber = $subscriberRepository->subscribeAddress(
address: $address,
attributes: ['status' => 'active'], // unconfirmed is default status
);
// Unsubscribe address returning the unsubscribed subscriber or null if none unsubscribed:
$unsubscribedSubscriber = $subscriberRepository->unsubscribeAddress($address); // AddressInterface
// You may check if the given address has already been subscribed returning a boolean:
$subscribed = $subscriberRepository->hasSubscribedAddress($address); // AddressInterface
}
}You may check out the App User and Service User for more information.
Using User Web
If using the App - User Web bundle, you can register the UserNewsletterSubscriber::class event listener which will subscribe and unsubscribe users from the newsletter after registration and profile updates depending whether the users clicked the newsletter checkbox.
In the config/event.php file add the following listener:
'listeners' => [
// Specify listeners without event:
'auto' => [
\Tobento\App\Notification\Listener\UserNewsletterSubscriber::class,
],
],The subscribe newsletter feature provides a simple subscribe to newsletter functionality.
Config
In the config file you can configure this feature:
'features' => [
new Feature\SubscribeNewsletter(
// The view to render.
view: 'newsletter/subscribe',
// A menu name to show the subscribe link or null if none.
menu: 'footer',
menuLabel: 'Newsletter',
// A menu parent name (e.g. 'information') or null if none.
menuParent: null,
// The message shown after successful subscription:
successMessage = 'Thank you for subscribing to our newsletter - we are excited to have you with us!',
// When the channelVerification parameter is set to true you may consider changing the message to something like:
// successMessage = 'You are almost there! We have sent a confirmation email - just click the link to complete your subscription.',
// The message shown when the email or phone already exists.
alreadySubscribedMessage: 'You have already subscribed to our newsletter.',
// The route to redirect to after successful subscription.
successRedirectRoute: 'home',
// The subscriber status after a successful subscription.
subscriberStatus: 'unconfirmed',
// It is not recommended to set the status to 'active' as it may be a spammed email.
// Change the status manually at the subscriber web interface or
// use channel verification instead.
// If true, routes are being localized.
localizeRoute: false,
// The channels to support.
supportedChannels: ['email', 'smartphone'],
// When true, a notification email and/or sms depending on the supported channels defined
// will be sent to verify the channel(s).
channelVerification: true,
// The expiration in day after the channel verfication url expires.
channelVerificationUrlExpiresInDays: 5,
// When the channelVerification parameter is set to true,
// this status will be used after all channels defined have been verified,
// otherwise the status of the subscriberStatus parameter is used.
subscriberStatusForVerifiedChannels: 'active',
// The message shown after a successful channel confirmation.
confirmSuccessMessage: 'Your newsletter subscription has been successfully confirmed.',
// The message shown after a failed channel confirmation.
confirmFailedMessage: 'Newsletter subscription confirmation failed.',
// When true, only the store route will be available.
onlySubscribeStoreRoute: false,
// you may disable the ACL while testing for instance,
// otherwise only users with the right permissions can access the page.
withAcl: false,
),
],ACL Permissions
newsletter.subscribeUser can subscribe to newsletter
If using the App Backend, you can assign the permissions in the roles or users page.
Topics
You may configure topics the user can select. In the newsletter type web interface you can specify which topics the newsletter covers and should only be sent to those subscribers who have selected the topics.
In the config file you can configure the topics globally:
use Tobento\App\Notification\Newsletter;
'interfaces' => [
Newsletter\TopicsInterface::class =>
static function(): Newsletter\TopicsInterface {
return new Newsletter\Topics([
'products.new' => trans('Inform me about new products.'),
'articles.new' => trans('Inform me about new articles.'),
]);
},
],Spam Protection
The newsletter subscribe form is protected against spam by default using the App Spam bundle. It uses the default spam detector as the defined named newsletter.subscribe detector does not exist. In order to use a custom detector, you will just need to define it on the app/config/spam.php file:
use Tobento\App\Spam\Factory;
'detectors' => [
'newsletter.subscribe' => new Factory\Composite(
new Factory\Honeypot(inputName: 'hp'),
new Factory\MinTimePassed(inputName: 'mtp', milliseconds: 1000),
),
]Rate Limiting
The newsletter subscribe form is rate limitied by default using the App Spam bundle.
You may customize it by overwriting the configureRoutes method:
use Tobento\App\AppInterface;
use Tobento\App\Notification\Feature\SubscribeNewsletter;
use Tobento\App\RateLimiter\Middleware\RateLimitRequests;
use Tobento\App\RateLimiter\Symfony\Registry\SlidingWindow;
use Tobento\App\Spam\Middleware\ProtectAgainstSpam;
use Tobento\Service\Routing\RouterInterface;
class CustomSubscribeNewsletter extends SubscribeNewsletter
{
protected function configureRoutes(RouterInterface $router, AppInterface $app): void
{
$router->getRoute(name: 'newsletter.subscribe.store')->middleware([
RateLimitRequests::class,
'registry' => new SlidingWindow(limit: 6, interval: '5 Minutes', id: 'newsletter.subscribe'),
'redirectRoute' => 'newsletter.subscribe',
'message' => 'Too many attempts. Please retry after :seconds seconds.',
//'messageLevel' => 'error',
], [
ProtectAgainstSpam::class,
'detector' => 'newsletter.subscribe',
]);
}
}Available Events
use Tobento\App\Notification\Event;| Event | Description |
|---|---|
Event\NewsletterSubscribed::class |
The event will dispatch after a user subscribes to the newsletter. |
Event\NewsletterSubscribeFailed::class |
The event will dispatch after a user subscription failed. |
The unsubscribe newsletter feature provides a simple unsubscribe from newsletter functionality.
Config
In the config file you can configure this feature:
'features' => [
new Feature\UnsubscribeNewsletter(
// You may change the success message:
successMessage: 'Your newsletter subscription has been successfully cancelled.',
// You may change the failed message:
successMessage: 'Your newsletter subscription has already been cancelled.',
// You may change the redirect route:
redirectRoute: 'home',
// If true, routes are being localized.
localizeRoute: false,
),
],Use the router to generate unsubscribe links. Make sure you sign the url as the registered route in the feature class is signed too.
use Tobento\Service\Dater\Dater;
use Tobento\Service\Routing\RouterInterface;
final class SomeService
{
public function __construct(
private readonly RouterInterface $router,
) {}
public function generateUnsubscribeLink(int|string $subscriberId): string
{
retrun (string) $this->router
->url('newsletter.unsubscribe', ['id' => $subscriberId])
->sign(new Dater()->addDays(3));
// with locale:
retrun (string) $this->router
->url('newsletter.unsubscribe', ['id' => $subscriberId, 'locale' => 'de'])
->sign(new Dater()->addDays(3));
}
}You may check out the Signed Routing service for more information.
Additionally, a placeholder for an unsubscribe link will be available on the newsletter notification page.
Warning When using multiple apps, ensure that the
signature_keyis consistently configured in eachconfig/httpfile, otherwise unsubscribe links may not function properly.
This feature is in consideration.
The tracking feature will track clicked links which can be view on the reports page. In addition, it will store the click ID in the session which can be used for events tracking.
Config
In the config file you can configure this feature:
'features' => [
new Feature\Tracking(
// you may customize the failed message:
failedMessage: 'The link you used is either expired or invalid, so you\'ve been redirected to this page.',
// you may change the redirect route which will be used if
// tracking token expired or is not found e.g.
failedRedirectRoute: 'home',
// you may change the logging:
logTrackingTokenExpiredException: true, // default
logTrackingTokenExceptions: true, // default
// you may change the route prefix:
routePrefix: 'nft', // default
),
],You may disable tracking using the trackingOptions parameter when registering custom notifications or the newsletter notification.
Warning If you do not enable the feature at all, make sure you disable the tracking on all notification types, otherwise tracking links will not be found and result in 404 responses.
When generating notification content, href links are replaced by a tracking token link, using the implemented TrackerInterface::class and TokenRepositoryInterface::class configurable in the config file. This link will take the user back to your website which will then redirect them to the final destination after logging the click using the implemented ClickRepositoryInterface::class. Make sure the ClickRepositoryInterface::class uses a storage supporting raw statements as they will be used for querying reports data.
use Tobento\App\Notification\Tracking;
use Tobento\Service\Database\DatabasesInterface;
'interfaces' => [
Tracking\TrackerInterface::class => Tracking\Tracker::class,
Tracking\TokenRepositoryInterface::class =>
static function(
DatabasesInterface $databases,
Tracking\TokenFactory $entityFactory
): Tracking\TokenRepositoryInterface {
return new Tracking\TokenStorageRepository(
storage: $databases->default('storage')->storage()->new(),
table: 'notification_tracking_tokens',
entityFactory: $entityFactory,
);
},
Tracking\ClickRepositoryInterface::class =>
static function(
DatabasesInterface $databases,
): Tracking\ClickRepositoryInterface {
return new Tracking\ClickStorageRepository(
storage: $databases->get('mysql-storage')->storage()->new(),
table: 'notification_tracking_clicks',
);
},
],After a link is clicked, the click ID is stored in the session under the key notification_tracking_click_id. You can use this ID to track user activity. You may use the implemented EventRepositoryInterface::class configurable in the config file to store events.
use Tobento\App\Notification\Tracking;
use Tobento\Service\Database\DatabasesInterface;
'interfaces' => [
Tracking\EventRepositoryInterface::class =>
static function(
DatabasesInterface $databases,
Tracking\ClickRepositoryInterface $clickRepository,
): Tracking\EventRepositoryInterface {
return new Tracking\EventStorageRepository(
storage: $databases->get('mysql-storage')->storage()->new(),
table: 'notification_tracking_events',
clickRepository: $clickRepository,
);
},
],Make sure the EventRepositoryInterface::class uses a storage supporting raw statements as they will be used for querying reports data.
Example
For example, you can record how many orders were generated by a newsletter campaign.
use Tobento\App\Notification\Tracking\EventRepositoryInterface;
use Tobento\Service\Session\SessionInterface;
final class NotificationReportOrderCompleted
{
public function __construct(
private readonly SessionInterface $session,
private readonly EventRepositoryInterface $eventRepository,
) {}
public function subscribe(Event\OrderCompleted $event): void
{
if ($clickId = $this->session->get('notification_tracking_click_id')) {
$this->eventRepository->createFromClickId(
clickId: $clickId,
name: 'orders',
meta: ['order_id' => $event->order()->id()],
);
}
}
}Newsletter notifications have already added the most tracked events card, but you may create a custom report card to display reports about generated orders by a newsletter campaign. Use the app on method to register the card on demand only using ReportCards::class to add the card:
use Tobento\App\Notification\Card\ReportCards;
$app->on(ReportCards::class, static function(ReportCards $cards) use ($app): void {
$cards->add(
name: 'orders',
card: $app->make(OrdersCard::class, ['entity' => $cards->notificationEntity(), 'priority' => 100]),
);
});Check out the reports section to find out more about reports.
Channels are responsible for rendering the fields required to create channel‑specific messages within the notifications web interface. Based on the configured notification, they generate the corresponding message for the notifier to deliver. In addition, channels manage the notification preview feature.
Configure Channels
Channels can be configure in the notification config file:
use Tobento\App\Notification\Channel;
'interfaces' => [
Channel\ChannelsInterface::class =>
static function(): Channel\ChannelsInterface {
return new Channel\Channels(
new Channel\Mail(
allowedAttachmentsExtensions: ['jpg'],
),
new Channel\Sms(),
new Channel\Storage(),
);
},
],Be sure to configure the notifier channels as well.
The mail channel allows you to create email messages using the Block Mail Editor, which can be customized to meet your requirements. Additionally, it supports uploading email attachments.
use Tobento\App\Notification\Channel;
'interfaces' => [
Channel\ChannelsInterface::class =>
static function(): Channel\ChannelsInterface {
return new Channel\Channels(
new Channel\Mail(
// Specify the block editor to use:
blockEditorName: 'mail', // default
// Define the allowed attachment extensions:
allowedAttachmentsExtensions: ['jpg'],
// Define the notifier channel name:
name: 'mail', // default
// Specify a title:
name: 'Mail', // default
),
);
},
],Make sure to also configure the corresponding notifier mail channel in the app/config/notifier.php file.
The SMS channel allows you to create SMS messages.
use Tobento\App\Notification\Channel;
'interfaces' => [
Channel\ChannelsInterface::class =>
static function(): Channel\ChannelsInterface {
return new Channel\Channels(
new Channel\Sms(
// Define the notifier channel name:
name: 'sms', // default
// Specify a title:
name: 'SMS', // default
),
);
},
],Make sure to also configure the corresponding notifier sms channel in the app/config/notifier.php file.
The storage channel enables you to create messages that are displayed on the user notifications page, provided the Notifications Feature from the App User Web is installed.
use Tobento\App\Notification\Channel;
'interfaces' => [
Channel\ChannelsInterface::class =>
static function(): Channel\ChannelsInterface {
return new Channel\Channels(
new Channel\Storage(
// Define the notifier channel name:
name: 'storage', // default
// Specify a title:
name: 'Account', // default
),
);
},
],Make sure to also configure the corresponding notifier storage channel in the app/config/notifier.php file.
Additionally, ensure that the StorageChannelNotificationFormatter::class is registered in the same configuration file:
'formatters' => [
\Tobento\App\Notification\Notifier\StorageChannelNotificationFormatter::class,
\Tobento\App\Notifier\Storage\GeneralNotificationFormatter::class,
],The chat channel allows you to create chat messages.
use Tobento\App\Notification\Channel;
'interfaces' => [
Channel\ChannelsInterface::class =>
static function(): Channel\ChannelsInterface {
return new Channel\Channels(
new Channel\Chat(
// Define the notifier channel name:
name: 'chat-slack', // default
// Specify a title:
name: 'Slack', // default
),
);
},
],Make sure to also configure the corresponding notifier chat channel in the app/config/notifier.php file.
The push channel allows you to create push messages.
use Tobento\App\Notification\Channel;
'interfaces' => [
Channel\ChannelsInterface::class =>
static function(): Channel\ChannelsInterface {
return new Channel\Channels(
new Channel\Push(
// Define the notifier channel name:
name: 'push', // default
// Specify a title:
name: 'Push', // default
),
);
},
],Make sure to also configure the corresponding notifier push channel in the app/config/notifier.php file.
Placeholders allow the user to insert placeholders like {{ user.name }} into their content, which can be replaced with dynamic values when the content is rendered.
use Tobento\App\Notification\Placeholder\Placeholder;
use Tobento\App\Notification\Placeholder\Placeholders;
$placeholders = new Placeholders(
new Placeholder(
key: 'date',
value: fn () => date("F j, Y, g:i a"),
),
);Placeholders can be defined in notification type classes. See create custom notification for instance.
This placeholder will replace the placeholder defined by the key parameter with the value defined.
use Tobento\App\Notification\NotificationEntityInterface;
use Tobento\App\Notification\Placeholder\Placeholder;
use Tobento\Service\Notifier\NotificationInterface;
use Tobento\Service\Notifier\RecipientInterface;
use Tobento\Service\User\AddressInterface;
$placeholder = new Placeholder(
// define a key:
key: 'recipient.locale',
// define a value:
value: 'de',
// or using a closure:
value: fn (
NotificationEntityInterface $entity,
RecipientInterface $recipient,
AddressInterface $address,
null|NotificationInterface $notification,
// ... any other parameters are being resolved by autowiring
) => $recipient->getLocale(),
// you set if the placeholder is active or not using a boolean:
active: false,
// or using a closure with autowired parameters:
active: fn (): bool => false,
// you may define a fallback value:
fallbackValue: 'en',
// you may define a description:
description: 'Locale ...',
),This placeholder will replace the placeholder {{ greeting }} with the greeting from the address.
use Tobento\App\Notification\Placeholder\GreetingPlaceholder;
$placeholder = new GreetingPlaceholder();
$placeholder = new GreetingPlaceholder(
// you may customize the key:
key: 'greeting', // default
),Use the following command to send any type of notifications:
php ap notifications:send
Available Options
| Option | Description |
|---|---|
--interval=20 |
The number of notifications to send or queue. |
--queue=file |
Define the queue name if you want to queue the notification, otherwise notifcation will be sent immediately. |
--type=newsletter |
Send only specific types. |
--status=active |
Sends notfication with the status. |
--tries=3 |
The maximum number of times a job should be attempted. |
--priority=-100 |
The priority when queueing, highest are first prioritized. |
--encrypt |
If set notification will be encrypted when queueing. |
Check out the Sending Newsletters section to see possible options how to automate this process.
Use the following command to delete expired tracking tokens.
php ap notifications:purge-tracking-tokens
Available Options
| Option | Description |
|---|---|
--days=30 |
The number of days to retain tracking tokens after expiring. |
You may install the App Task bundle and register the following task in the task config file which enables users to schedule the task from a web interface:
Example using the Command Task Registry:
'registries' => [
'purge.tracking.tokens' => new Registry\CommandTask(
name: 'Deletes tracking tokens 30 days after the expiration date.',
command: 'notifications:purge-tracking-tokens',
input: [
'--days' => 30,
],
// Define the supported apps where the task can be run:
supportedAppIds: ['root', 'backend'],
),
]You can configure custom loggers in the Logging Config file for the following classes, which make use of the Logger Trait.
/*
|--------------------------------------------------------------------------
| Aliases
|--------------------------------------------------------------------------
*/
'aliases' => [
\Tobento\App\Notification\Placeholder\Replacer::class => 'daily',
\Tobento\App\Notification\Feature\Tracking::class => 'daily',
],When using multiple Apps, you may use the App Backend for managing notifications and subscribers. Make sure that you configure the database using the same connection as in this example.
'features' => [
new Feature\Notifications(),
new Feature\NewsletterSubscribers(),
// only for frontend so we uncomment:
//new Feature\SubscribeNewsletter(),
// used for generating unsubscribe links
new Feature\UnsubscribeNewsletter(),
// you may uncomment if not supporting for backend:
//Feature\ReplacesCustomNotifications::class,
new Feature\Tracking(),
],
'interfaces' => [
Newsletter\SubscriberRepositoryInterface::class =>
static function(
DatabasesInterface $databases,
Newsletter\SubscriberEntityFactory $entityFactory
): Newsletter\SubscriberRepositoryInterface {
return new Newsletter\SubscriberStorageRepository(
storage: $databases->default('shared:storage')->storage()->new(),
table: 'newsletter_subscribers',
entityFactory: $entityFactory,
);
},
Tracking\TokenRepositoryInterface::class =>
static function(
DatabasesInterface $databases,
Tracking\TokenFactory $entityFactory
): Tracking\TokenRepositoryInterface {
return new Tracking\TokenStorageRepository(
storage: $databases->default('shared:storage')->storage()->new(),
table: 'notification_tracking_tokens',
entityFactory: $entityFactory,
);
},
Tracking\ClickRepositoryInterface::class =>
static function(
DatabasesInterface $databases,
): Tracking\ClickRepositoryInterface {
return new Tracking\ClickStorageRepository(
storage: $databases->default('shared:storage')->storage()->new(),
table: 'notification_tracking_clicks',
);
},
Tracking\EventRepositoryInterface::class =>
static function(
DatabasesInterface $databases,
Tracking\ClickRepositoryInterface $clickRepository,
): Tracking\EventRepositoryInterface {
return new Tracking\EventStorageRepository(
storage: $databases->default('shared:storage')->storage()->new(),
table: 'notification_tracking_events',
clickRepository: $clickRepository,
);
},
],Next, configure the config for another app (frontend). You may visit the Apps documentation to find out more about creating apps.
'features' => [
// only for backend so we uncomment:
//new Feature\Notifications(),
//new Feature\NewsletterSubscribers(),
new Feature\SubscribeNewsletter(),
new Feature\UnsubscribeNewsletter(),
Feature\ReplacesCustomNotifications::class,
new Feature\Tracking(),
],
'interfaces' => [
Newsletter\SubscriberRepositoryInterface::class =>
static function(
DatabasesInterface $databases,
Newsletter\SubscriberEntityFactory $entityFactory
): Newsletter\SubscriberRepositoryInterface {
return new Newsletter\SubscriberStorageRepository(
storage: $databases->default('shared:storage')->storage()->new(),
table: 'newsletter_subscribers',
entityFactory: $entityFactory,
);
},
Tracking\TokenRepositoryInterface::class =>
static function(
DatabasesInterface $databases,
Tracking\TokenFactory $entityFactory
): Tracking\TokenRepositoryInterface {
return new Tracking\TokenStorageRepository(
storage: $databases->default('shared:storage')->storage()->new(),
table: 'notification_tracking_tokens',
entityFactory: $entityFactory,
);
},
Tracking\ClickRepositoryInterface::class =>
static function(
DatabasesInterface $databases,
): Tracking\ClickRepositoryInterface {
return new Tracking\ClickStorageRepository(
storage: $databases->default('shared:storage')->storage()->new(),
table: 'notification_tracking_clicks',
);
},
Tracking\EventRepositoryInterface::class =>
static function(
DatabasesInterface $databases,
Tracking\ClickRepositoryInterface $clickRepository,
): Tracking\EventRepositoryInterface {
return new Tracking\EventStorageRepository(
storage: $databases->default('shared:storage')->storage()->new(),
table: 'notification_tracking_events',
clickRepository: $clickRepository,
);
},
],Finally, in the database config file, in both apps, configure the shared:storage database:
'defaults' => [
'pdo' => 'mysql',
'storage' => 'file',
'shared:storage' => 'shared:file',
],
'databases' => [
'shared:file' => [
'factory' => \Tobento\Service\Database\Storage\StorageDatabaseFactory::class,
'config' => [
'storage' => \Tobento\Service\Storage\JsonFileStorage::class,
'dir' => directory('app:parent').'storage/database/file/',
],
],
],When using multiple apps you may configure languages for the following services in the backend app.
use Tobento\App\Crud\ActionProcessor;
use Tobento\App\Crud\ActionProcessorInterface;
use Tobento\App\Notification\Action\PreviewNotificationAction;
use Tobento\App\Notification\Action\PreviewNotificationChannelAction;
use Tobento\App\Notification\Controller\SendNotificationController;
use Tobento\Service\Language\AreaLanguagesInterface;
// Get the languages you support for resources:
$areaLanguages = $app->get(AreaLanguagesInterface::class);
$languages = $areaLanguages->get('resources'); // or whatever name you have configured
// Configure:
$app->set(ActionProcessorInterface::class, ActionProcessor::class)->with(['languages' => $languages]);
$app->set(PreviewNotificationAction::class)->with(['languages' => $languages]);
$app->set(PreviewNotificationChannelAction::class)->with(['languages' => $languages]);
$app->set(SendNotificationController::class)->with(['languages' => $languages]);