PHPackages                             tobento/app-notification - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Mail &amp; Notifications](/categories/mail)
4. /
5. tobento/app-notification

ActiveLibrary[Mail &amp; Notifications](/categories/mail)

tobento/app-notification
========================

The app notification enables custom notifications, newsletter management and delivery, click and event tracking, and detailed reporting.

2.0(5mo ago)00MITPHPPHP &gt;=8.0

Since Dec 3Pushed 5mo agoCompare

[ Source](https://github.com/tobento-ch/app-notification)[ Packagist](https://packagist.org/packages/tobento/app-notification)[ Docs](https://www.tobento.ch)[ RSS](/packages/tobento-app-notification/feed)WikiDiscussions 2.x Synced 1mo ago

READMEChangelog (1)Dependencies (27)Versions (2)Used By (0)

App Notification
================

[](#app-notification)

The app notification provides the following features:

- [custom notifications](#custom-notifications-feature) for replacing notifications sent by the [app - notifier](https://github.com/tobento-ch/app-notifier#creating-and-sending-notifications)
- [manage and sending newsletters](#newsletter-feature) to [newsletter subscribers](#newsletter-subscribers-feature)
- [tracking clicks and events](#tracking-feature)
- [reports](#reports)

Table of Contents
-----------------

[](#table-of-contents)

- [Getting Started](#getting-started)
    - [Requirements](#requirements)
- [Documentation](#documentation)
    - [App](#app)
    - [Notification Boot](#notification-boot)
        - [Notification Config](#notification-config)
    - [Features](#features)
        - [Notifications Feature](#notifications-feature)
            - [Preview Notification](#preview-notification)
            - [Send Notification For Review](#send-notification-for-review)
            - [Reports](#reports)
        - [Custom Notifications Feature](#custom-notifications-feature)
            - [Create Custom Notification](#create-custom-notification)
            - [Register Custom Notification](#register-custom-notification)
            - [Manage Custom Notification](#manage-custom-notification)
            - [Replace Custom Notification](#replace-custom-notification)
        - [Users Notification Feature](#users-notification-feature)
            - [Register User Notification](#register-user-notification)
            - [Manage User Notification](#manage-user-notification)
            - [Sending User Notifications](#sending-user-notifications)
        - [Newsletter Feature](#newsletter-feature)
            - [Register Newsletter Notification](#register-newsletter-notification)
            - [Manage Newsletter Notification](#manage-newsletter-notification)
            - [Sending Newsletters](#sending-newsletters)
        - [Newsletter Subscribers Feature](#newsletter-subscribers-feature)
            - [Manage Newsletter Subscribers](#manage-newsletter-subscribers)
            - [Subscribe And Unsubscribe From Newsletter](#subscribe-and-unsubscribe-from-newsletter)
        - [Subscribe Newsletter Feature](#subscribe-newsletter-feature)
        - [Unsubscribe Newsletter Feature](#unsubscribe-newsletter-feature)
        - [Sent Notifications Feature](#sent-notifications-feature)
        - [Tracking Feature](#tracking-feature)
            - [Links Tracking](#links-tracking)
            - [Events Tracking](#events-tracking)
    - [Channels](#channels)
        - [Mail Channel](#mail-channel)
        - [SMS Channel](#sms-channel)
        - [Storage Channel](#storage-channel)
        - [Chat Channel](#chat-channel)
        - [Push Channel](#push-channel)
    - [Placeholders](#placeholders)
        - [Placeholder](#placeholder)
        - [Greeting Placeholder](#greeting-placeholder)
    - [Console](#console)
        - [Notifications Send Command](#notifications-send-command)
        - [Purge Tracking Tokens Command](#purge-tracking-tokens-command)
    - [Custom Loggers](#custom-loggers)
    - [Learn More](#learn-more)
        - [Using Multiple Apps](#using-multiple-apps)
        - [Configure Language For Notifications](#configure-language-for-notifications)
- [Credits](#credits)

---

Getting Started
===============

[](#getting-started)

Add the latest version of the app notification project running this command.

```
composer require tobento/app-notification

```

Requirements
------------

[](#requirements)

- PHP 8.4 or greater

Documentation
=============

[](#documentation)

App
---

[](#app)

Check out the [**App Skeleton**](https://github.com/tobento-ch/app-skeleton) if you are using the skeleton.

You may also check out the [**App**](https://github.com/tobento-ch/app) to learn more about the app in general.

Notification Boot
-----------------

[](#notification-boot)

The notification boot does the following:

- installs and loads notification config
- implements notification interfaces
- boots [features](#features) configured in [notification config](#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](https://github.com/tobento-ch/app-backend) and [boot the notification](https://github.com/tobento-ch/app-backend#adding-boots) in the backend app.

### Notification Config

[](#notification-config)

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](#features) and more.

Features
--------

[](#features)

### Notifications Feature

[](#notifications-feature)

The notifications feature provides a page where users can create, edit and delete any notification types registered such as [newsletters notifications](#newsletter-feature), [custom notifications](#custom-notifications-feature) or any other custom notification types created.

**Config**

In the [config file](#notification-config) 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**

- `notifications` User can access notifications
- `notifications.create` User can create notifications
- `notifications.edit` User can edit notifications
- `notifications.delete` User can delete notifications
- `notifications.send` User can send notifications

If using the [App Backend](https://github.com/tobento-ch/app-backend), you can assign the permissions in the roles or users page.

#### Preview Notification

[](#preview-notification)

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.

#### Send Notification For Review

[](#send-notification-for-review)

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](#custom-notifications-feature) has a [Preview Notification](#preview-notification) which will be used
- when any other notification the default notification will be used.

#### Reports

[](#reports)

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](#newsletter-feature), 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](https://github.com/tobento-ch/app-card) 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](https://github.com/tobento-ch/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](#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
            ),
        );
    },

],
```

### Custom Notifications Feature

[](#custom-notifications-feature)

You may create custom notifications for replacing notifications send by the [App - Notifier](https://github.com/tobento-ch/app-notifier#creating-and-sending-notifications).

#### Create Custom Notification

[](#create-custom-notification)

Lets create a custom notification type for the [App User Web - Forgot Password Notification](https://github.com/tobento-ch/app-user-web#customize-reset-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](#placeholders) to learn more about it.

#### Register Custom Notification

[](#register-custom-notification)

[Once created](#create-custom-notification), you will need to register the custom notification type so as to [manage](#manage-custom-notification) and [replace](#replace-custom-notification) custom notifications in the [config file](#notification-config):

```
'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
            ),
        );
    },

],
```

#### Manage Custom Notification

[](#manage-custom-notification)

Custom notifications can be managed by simply enabling the [Notifications Feature](#notifications-feature). Furthermore, the [Mail Editor - App Block](https://github.com/tobento-ch/app-block#mail-editor) is used to create the mail messages with, which you may configure to fit your requirements.

#### Replace Custom Notification

[](#replace-custom-notification)

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](#notification-config) you can configure this feature:

```
'features' => [
    Feature\ReplacesCustomNotifications::class,
],
```

You may consider using the [App Backend](https://github.com/tobento-ch/apps) to [manage custom notifications](#manage-custom-notification).

In addition, you may check out the [Apps](https://github.com/tobento-ch/apps) bundle if you want to support multiple apps.

### Users Notification Feature

[](#users-notification-feature)

#### Register User Notification

[](#register-user-notification)

Register the users notification type so as to [manage](#manage-user-notification) and [sending](#sending-user-notifications) user notifications in the [config file](#notification-config):

```
'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,
            ),
        );
    },

],
```

#### Manage User Notification

[](#manage-user-notification)

User notifications can be easily managed by enabling the [Notifications Feature](#notifications-feature).

#### Sending User Notifications

[](#sending-user-notifications)

You can deliver user notifications created from the web interface by using the [Notifications Send Command](#notifications-send-command). There are two main approaches:

> **Warning**Use caution with the `--interval` option 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](https://github.com/tobento-ch/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](https://github.com/tobento-ch/app-task) bundle and register the task in the [task config file](https://github.com/tobento-ch/app-task#task-config). 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](https://github.com/tobento-ch/app-task#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'],
    ),
]
```

### Newsletter Feature

[](#newsletter-feature)

#### Register Newsletter Notification

[](#register-newsletter-notification)

Register the newsletter notification type so as to [manage](#manage-newsletter-notification) and [sending](#sending-newsletters) newsletter notifications in the [config file](#notification-config):

```
'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
            ),
        );
    },

],
```

#### Manage Newsletter Notification

[](#manage-newsletter-notification)

Newsletter notifications can be managed by simply enabling the [Notifications Feature](#notifications-feature).

#### Sending Newsletters

[](#sending-newsletters)

You can deliver newsletters created from the web interface by using the [Notifications Send Command](#notifications-send-command). There are two main approaches:

> **Warning**Use caution with the `--interval` option 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](https://github.com/tobento-ch/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](https://github.com/tobento-ch/app-task) bundle and register the task in the [task config file](https://github.com/tobento-ch/app-task#task-config). 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](https://github.com/tobento-ch/app-task#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'],
    ),
]
```

### Newsletter Subscribers Feature

[](#newsletter-subscribers-feature)

The newsletter subscribers feature provides a page where users can create, edit and delete newsletter subscribers to send newsletters using the [Newsletter Feature](#newsletter-feature).

**Config**

In the [config file](#notification-config) 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-subscribers` User can access newsletter subscribers
- `newsletter-subscribers.create` User can create newsletter subscribers
- `newsletter-subscribers.edit` User can edit newsletter subscribers
- `newsletter-subscribers.delete` User can delete newsletter subscribers

If using the [App Backend](https://github.com/tobento-ch/app-backend), you can assign the permissions in the roles or users page.

#### Manage Newsletter Subscribers

[](#manage-newsletter-subscribers)

Newsletter Subscribers can be managed by simply enabling this feature and using the web interface.

#### Subscribe And Unsubscribe From Newsletter

[](#subscribe-and-unsubscribe-from-newsletter)

**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](https://github.com/tobento-ch/app-user) and [Service User](https://github.com/tobento-ch/service-user) for more information.

**Using User Web**

If using the [App - User Web](https://github.com/tobento-ch/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,
    ],
],
```

### Subscribe Newsletter Feature

[](#subscribe-newsletter-feature)

The subscribe newsletter feature provides a simple subscribe to newsletter functionality.

**Config**

In the [config file](#notification-config) 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.subscribe` User can subscribe to newsletter

If using the [App Backend](https://github.com/tobento-ch/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](#manage-newsletter-notification) 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](https://github.com/tobento-ch/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](https://github.com/tobento-ch/app-rate-limiter) 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;
```

EventDescription`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.### Unsubscribe Newsletter Feature

[](#unsubscribe-newsletter-feature)

The unsubscribe newsletter feature provides a simple unsubscribe from newsletter functionality.

**Config**

In the [config file](#notification-config) 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](https://github.com/tobento-ch/service-routing#signed-routing) service for more information.

Additionally, a [placeholder](#placeholders) for an unsubscribe link will be available on the [newsletter notification page](#manage-newsletter-notification).

> **Warning**When using [multiple apps](#using-multiple-apps), ensure that the `signature_key` is consistently configured in each `config/http` file, otherwise unsubscribe links may not function properly.

### Sent Notifications Feature

[](#sent-notifications-feature)

This feature is in consideration.

### Tracking Feature

[](#tracking-feature)

The tracking feature will [track clicked links](#links-tracking) which can be view on the [reports page](#reports). In addition, it will store the click ID in the session which can be used for [events tracking](#events-tracking).

**Config**

In the [config file](#notification-config) 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](#register-custom-notification) or the [newsletter notification](#register-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.

#### Links Tracking

[](#links-tracking)

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](#notification-config). 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](https://github.com/tobento-ch/service-storage#raw-statements) as they will be used for [querying reports data](#reports).

```
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',
        );
    },
],
```

#### Events Tracking

[](#events-tracking)

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](#notification-config) 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](https://github.com/tobento-ch/service-storage#raw-statements) as they will be used for [querying reports data](#reports).

**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](#newsletter-feature) 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](#reports) section to find out more about reports.

Channels
--------

[](#channels)

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](https://github.com/tobento-ch/app-notifier) to deliver. In addition, channels manage the notification preview feature.

**Configure Channels**

Channels can be configure in the [notification config](#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](https://github.com/tobento-ch/app-notifier#notifier-config) as well.

### Mail Channel

[](#mail-channel)

The mail channel allows you to create email messages using the [Block Mail Editor](https://github.com/tobento-ch/app-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](https://github.com/tobento-ch/service-notifier#mail-channel) in the `app/config/notifier.php` file.

### SMS Channel

[](#sms-channel)

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](https://github.com/tobento-ch/service-notifier#sms-channel) in the `app/config/notifier.php` file.

### Storage Channel

[](#storage-channel)

The storage channel enables you to create messages that are displayed on the user notifications page, provided the [Notifications Feature](https://github.com/tobento-ch/app-user-web#notifications-feature) from the [App User Web](https://github.com/tobento-ch/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](https://github.com/tobento-ch/service-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,
],
```

### Chat Channel

[](#chat-channel)

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](https://github.com/tobento-ch/service-notifier#chat-channel) in the `app/config/notifier.php` file.

### Push Channel

[](#push-channel)

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](https://github.com/tobento-ch/service-notifier#push-channel) in the `app/config/notifier.php` file.

Placeholders
------------

[](#placeholders)

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](#create-custom-notification) for instance.

### Placeholder

[](#placeholder)

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 ...',
),
```

### Greeting Placeholder

[](#greeting-placeholder)

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
),
```

Console
-------

[](#console)

### Notifications Send Command

[](#notifications-send-command)

Use the following command to send any type of notifications:

```
php ap notifications:send

```

**Available Options**

OptionDescription`--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](#sending-newsletters) section to see possible options how to automate this process.

### Purge Tracking Tokens Command

[](#purge-tracking-tokens-command)

Use the following command to delete expired tracking tokens.

```
php ap notifications:purge-tracking-tokens

```

**Available Options**

OptionDescription`--days=30`The number of days to retain tracking tokens after expiring.You may install the [App Task](https://github.com/tobento-ch/app-task) bundle and register the following task in the [task config file](https://github.com/tobento-ch/app-task#task-config) which enables users to schedule the task from a web interface:

Example using the [Command Task Registry](https://github.com/tobento-ch/app-task#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'],
    ),
]
```

Custom Loggers
--------------

[](#custom-loggers)

You can configure custom loggers in the [Logging Config](https://github.com/tobento-ch/app-logging#logging-config) file for the following classes, which make use of the [Logger Trait](https://github.com/tobento-ch/app-logging#logger-trait).

```
/*
|--------------------------------------------------------------------------
| Aliases
|--------------------------------------------------------------------------
*/

'aliases' => [
    \Tobento\App\Notification\Placeholder\Replacer::class => 'daily',
    \Tobento\App\Notification\Feature\Tracking::class => 'daily',
],
```

Learn More
----------

[](#learn-more)

### Using Multiple Apps

[](#using-multiple-apps)

When using multiple [Apps](https://github.com/tobento-ch/apps), you may use the [App Backend](https://github.com/tobento-ch/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](https://github.com/tobento-ch/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](https://github.com/tobento-ch/app-database#database-config), 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/',
        ],
    ],
],
```

### Configure Language For Notifications

[](#configure-language-for-notifications)

When using [multiple apps](#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]);
```

Credits
=======

[](#credits)

- [Tobias Strub](https://www.tobento.ch)
- [All Contributors](../../contributors)

###  Health Score

31

—

LowBetter than 68% of packages

Maintenance71

Regular maintenance activity

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity40

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

2

Last Release

166d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/055d6a1b5c2384bb179c75ab0b55914231d898fdc4dffeb30770f81200e52206?d=identicon)[TOBENTOch](/maintainers/TOBENTOch)

---

Top Contributors

[![tobento-ch](https://avatars.githubusercontent.com/u/16684832?v=4)](https://github.com/tobento-ch "tobento-ch (1 commits)")

---

Tags

packagenotificationappnewslettertobento

###  Code Quality

TestsPHPUnit

Static AnalysisPsalm

Type Coverage Yes

### Embed Badge

![Health badge](/badges/tobento-app-notification/health.svg)

```
[![Health](https://phpackages.com/badges/tobento-app-notification/health.svg)](https://phpackages.com/packages/tobento-app-notification)
```

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
