PHP code example of tobento / app-notification

1. Go to this page and download the library: Download tobento/app-notification library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

tobento / app-notification example snippets


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();

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

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);
    }
}

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;
    }
}

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

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;
    }
}

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

],

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

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

],

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

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

],

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)
    )
);

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

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

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

],

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)
    )
);

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

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

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

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
    }
}

'listeners' => [
    // Specify listeners without event:
    'auto' => [
        \Tobento\App\Notification\Listener\UserNewsletterSubscriber::class,
    ],
],

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

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

],

use Tobento\App\Spam\Factory;

'detectors' => [
    'newsletter.subscribe' => new Factory\Composite(
        new Factory\Honeypot(inputName: 'hp'),
        new Factory\MinTimePassed(inputName: 'mtp', milliseconds: 1000),
    ),
]

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

use Tobento\App\Notification\Event;

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

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

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

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

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

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

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

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

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

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

'formatters' => [
    \Tobento\App\Notification\Notifier\StorageChannelNotificationFormatter::class,
    
    \Tobento\App\Notifier\Storage\GeneralNotificationFormatter::class,
],

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

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

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"),
    ),
);

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

use Tobento\App\Notification\Placeholder\GreetingPlaceholder;

$placeholder = new GreetingPlaceholder();

$placeholder = new GreetingPlaceholder(
    // you may customize the key:
    key: 'greeting', // default
),

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

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

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

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

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

'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/',
        ],
    ],
],

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]);
app/config/notification.php
app/config/spam.php
app/config/notifier.php
app/config/notifier.php
app/config/notifier.php
app/config/notifier.php
app/config/notifier.php

php ap notifications:send

php ap notifications:purge-tracking-tokens