Skip to content

Notification channels

The ibexa/notifications package integrates the Symfony Notifier with Ibexa DXP. You can use it to create notifications and send them through various channels such as email, SMS, communication platforms, and the back office user notifications.

These notifications must not be confused with the notification bars or the user notifications:

Notification category Sent with Description
Notification bars TranslatableNotificationHandlerInterface Rendered as a message bar in the bottom-right corner.
User notifications NotificationService Rendered as back office notification.
Channel-based notifications NotificationServiceInterface Rendering depends on the channel assigned to the notification type.

Unlike notification bars and user notifications, channel-based notifications don't have a predefined channel. You can configure how they are delivered to the user by using YAML configuration. Several channels are provided, and you can create your own.

The Ibexa\Contracts\Notifications\Service\NotificationServiceInterface sends notifications, objects extending the Symfony\Component\Notifier\Notification\Notification class. You can inject this notification service into your code to send the built-in or custom notification types. Channel services implementing Symfony\Component\Notifier\Channel\ChannelInterface subscribe to a selection of notification types and deliver notifications to users through various transports.

Subscribe to notifications

Some events generate notifications that you can deliver to the users through one or more channels.

Available notification types

Available notification channels

You can list the notification channel services with the following command:

1
php bin/console debug:container --tag=notifier.channel
  • actito - Notification forwarded as transactional email
  • browser - Notification forwarded as flash message
  • chat - Notification forwarded to a communication platform like Slack, Microsoft Teams, or Google Chat
  • desktop - Notification forwarded to desktop applications like JoliNotif
  • email - Notification forwarded to email addresses
  • ibexa - Notification forwarded as back office user notifications
  • push - Notification forwarded to specific applications
  • sms - Notification forwarded to phone numbers

Subscriptions configuration

You can find the default configuration in config/packages/ibexa.yaml and config/packages/ibexa_admin_ui.yaml. You can modify it to define your own subscriptions. This page contains several examples of subscriptions configuration.

Scopes may not merge as expected

Subscriptions defined for a scope may not merge with subscriptions from other scopes or from other files. For example, default scope might not be merged within a siteaccess group scope. To ensure you don't unsubscribe against your will, always use the following command to check subscriptions for a siteaccess before and after any changes:

1
php bin/console ibexa:debug:config notifications.subscriptions --siteaccess=<siteaccess>

Subscription example

The following example shows how you can deliver notifications about Commerce-related activities through Slack:

  1. Install the Slack Notifier package:
1
composer require symfony/slack-notifier
  1. In a .env file, set the DSN to target a Slack channel or a Slack user:
1
SLACK_DSN=slack://xoxb-token@default?channel=ibexa-notifications
  1. Subscribe to notification types related to Commerce, such as order, payment, and shipment status changes. For example, define the following configuration in a new config/packages/notifications.yaml file:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
framework:
    notifier:
        chatter_transports:
            slack: '%env(SLACK_DSN)%'
ibexa:
    system:
        default:
            notifier:
                subscriptions:
                    # The configuration below is added to the `default` scope without overriding the one defined in ibexa.yaml
                    # Custom subscriptions:
                    Ibexa\OrderManagement\Notification\OrderStatusChange:
                        channels:
                            - chat
                    Ibexa\Payment\Notification\PaymentStatusChange:
                        channels:
                            - chat
                    Ibexa\Shipping\Notification\ShipmentStatusChange:
                        channels:
                            - chat

Create a notification class

You can define a new notification type and assign a new set of channels to it, customizing how it's delivered. It must extend the Symfony\Component\Notifier\Notification\Notification class and can optionally implement interfaces required by specific channels.

  • Some channels don't accept the notification if it doesn't implement their related notification interface. Those interfaces come with a method to specifically format the notification for the channel.
  • Some channels accept every notification and have a default formatting if the notification doesn't implement their related notification interface.
Channel Specific notification interface Accept any notification
actito Symfony\Component\Notifier\Notification\EmailNotificationInterface No
chat Symfony\Component\Notifier\Notification\ChatNotificationInterface Yes
desktop Symfony\Component\Notifier\Notification\DesktopNotificationInterface Yes
email Symfony\Component\Notifier\Notification\EmailNotificationInterface No
ibexa Ibexa\Contracts\Notifications\SystemNotification\SystemNotificationInterface No
push Symfony\Component\Notifier\Notification\PushNotificationInterface Yes
sms Symfony\Component\Notifier\Notification\SmsNotificationInterface No

The ibexa channel sends notifications to users through their profile menu, exactly as user notifications. The SystemNotificationChannel uses the core NotificationService to do so.

Some channels don't need a recipient:

  • browser: Always sends a flash message to the current user
  • chat: Always sends a message to the same connection resource

Notification sending

Use the objects from the Ibexa\Contracts\Notifications namespace to work with notifications.

The …\Service\NotificationServiceInterface::send() expects two arguments:

For example, to send a notification, you often use a combination like the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
use App\Notifications\MyNotification; // extends Symfony\Component\Notifier\Notification\Notification
use Ibexa\Contracts\Notifications\Value\Notification\SymfonyNotificationAdapter;
use Ibexa\Contracts\Notifications\Value\Recipent\SymfonyRecipientAdapter;
use Ibexa\Contracts\Notifications\Value\Recipent\UserRecipient;

$subject = 'My subject';

/** @var \Ibexa\Contracts\Notifications\Service\NotificationServiceInterface $notificationService */
/** @var \Ibexa\Contracts\Core\Repository\UserService $userService */
/** @var \Ibexa\Contracts\Core\Repository\PermissionResolver $permissionResolver */
$notificationService->send(
    new SymfonyNotificationAdapter(new MyNotification($subject)),
    [new SymfonyRecipientAdapter(new UserRecipient($userService->loadUser($permissionResolver->getCurrentUserReference()->getUserId())))],
);

CommandExecuted example

The following example is a command that sends a notification to users on several channels simultaneously. This example could be a scheduled task or cron job that warns users about its result.

  1. First, create a CommandExecuted notification type. It supports two channels (ibexa, email), but could be extended to support more. As constructor arguments, an instance takes the command itself, the exit code of the run, and any caught exceptions.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<?php declare(strict_types=1);

namespace App\Notifications;

use Ibexa\Contracts\Notifications\SystemNotification\SystemMessage;
use Ibexa\Contracts\Notifications\SystemNotification\SystemNotificationInterface;
use Ibexa\Contracts\Notifications\Value\Recipent\UserRecipientInterface;
use Symfony\Bridge\Twig\Mime\NotificationEmail;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Notifier\Message\EmailMessage;
use Symfony\Component\Notifier\Notification\EmailNotificationInterface;
use Symfony\Component\Notifier\Notification\Notification;
use Symfony\Component\Notifier\Recipient\EmailRecipientInterface;
use Throwable;

class CommandExecuted extends Notification implements SystemNotificationInterface, EmailNotificationInterface
{
    /** @param array<int, Throwable> $exceptions */
    public function __construct(
        private readonly Command $command,
        private readonly int $exitCode,
        private readonly array $exceptions
    ) {
        parent::__construct((Command::SUCCESS === $this->exitCode ? '✔' : '✖') . $this->command->getName());
        $this->importance(Command::SUCCESS === $this->exitCode ? Notification::IMPORTANCE_LOW : Notification::IMPORTANCE_HIGH);
    }

    public function asEmailMessage(EmailRecipientInterface $recipient, ?string $transport = null): ?EmailMessage
    {
        $body = '';
        foreach ($this->exceptions as $exception) {
            $body .= $exception->getMessage() . '<br>';
        }

        $email = NotificationEmail::asPublicEmail()
            ->to($recipient->getEmail())
            ->subject($this->getSubject())
            ->html($body);

        return new EmailMessage($email);
    }

    public function asSystemNotification(UserRecipientInterface $recipient, ?string $transport = null): ?SystemMessage
    {
        $message = new SystemMessage($recipient->getUser());
        $message->setContext([
            'icon' => Command::SUCCESS === $this->exitCode ? 'check-circle' : 'discard-circle',
            'subject' => $this->command->getName(),
            'content' => 'Number of errors: ' . count($this->exceptions),
        ]);

        return $message;
    }
}
  1. Assign channels subscribed to this notification in config/packages/notifications.yaml:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
ibexa:
    system:
        default:
            notifier:
                subscriptions:
                    # The configuration below is added to the `default` scope without overriding the one defined in ibexa.yaml
                    # Custom subscriptions:
                    Ibexa\OrderManagement\Notification\OrderStatusChange:
                        channels:
                            - chat
                    Ibexa\Payment\Notification\PaymentStatusChange:
                        channels:
                            - chat
                    Ibexa\Shipping\Notification\ShipmentStatusChange:
                        channels:
                            - chat
                    App\Notifications\CommandExecuted:
                        channels:
                            - ibexa
                            - email
  1. Create a command sending a CommandExecuted notification at the end of execution: It randomly succeeds or fails to demonstrate how notifications can communicate different execution results. It could be declared as a service to set the list of recipients' logins ($recipientLogins) from a configuration file.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
<?php

declare(strict_types=1);

namespace App\Command;

use App\Notifications\CommandExecuted;
use Ibexa\Contracts\Core\Repository\UserService;
use Ibexa\Contracts\Notifications\Service\NotificationServiceInterface;
use Ibexa\Contracts\Notifications\Value\Notification\SymfonyNotificationAdapter;
use Ibexa\Contracts\Notifications\Value\Recipent\SymfonyRecipientAdapter;
use Ibexa\Contracts\Notifications\Value\Recipent\UserRecipient;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Notifier\Recipient\RecipientInterface;

#[AsCommand(name: 'app:send_notification', description: 'Example of command sending a notification')]
class NotificationSenderCommand extends Command
{
    /** @param array<int, string> $recipientLogins */
    public function __construct(
        private readonly NotificationServiceInterface $notificationService,
        private readonly UserService $userService,
        private readonly array $recipientLogins = ['admin'],
    ) {
        parent::__construct();
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        /** @var array<int, \Throwable> $exceptions */
        $exceptions = [];

        try {
            // Do something
            if (random_int(0, 1) == 1) {
                throw new \RuntimeException('Something went wrong');
            }
            $exitCode = Command::SUCCESS;
        } catch (\Exception $exception) {
            $exceptions[] = $exception;
            $exitCode = Command::FAILURE;
        }

        $recipients = [];
        foreach ($this->recipientLogins as $login) {
            try {
                $user = $this->userService->loadUserByLogin($login);
                $recipients[] = new UserRecipient($user);
            } catch (\Exception $exception) {
                $exceptions[] = $exception;
            }
        }

        $this->notificationService->send(
            new SymfonyNotificationAdapter(new CommandExecuted($this, $exitCode, $exceptions)),
            array_map(
                static fn (RecipientInterface $recipient): SymfonyRecipientAdapter => new SymfonyRecipientAdapter($recipient),
                $recipients
            )
        );

        return $exitCode;
    }
}

When you execute this command, it fails randomly and notifies the Administrator user about the result.

Ibexa notification example

ControllerFeedback example

The following example shows a custom notification sent by a controller and displayed as a flash message on the corresponding page in the browser.

The following ControllerFeedback notification type is a class that only extends the base:

1
2
3
4
5
6
7
8
9
<?php declare(strict_types=1);

namespace App\Notifications;

use Symfony\Component\Notifier\Notification\Notification;

class ControllerFeedback extends Notification
{
}

The ControllerFeedback notification is sent in a controller action:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php declare(strict_types=1);

namespace App\Controller;

use App\Notifications\ControllerFeedback;
use Ibexa\Contracts\Notifications\Service\NotificationServiceInterface;
use Ibexa\Contracts\Notifications\Value\Notification\SymfonyNotificationAdapter;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

class NotificationSenderController extends AbstractController
{
    public function __construct(
        private readonly NotificationServiceInterface $notificationService,
    ) {
    }

    #[Route('/notification-sender')]
    public function index(): Response
    {
        $this->notificationService->send(
            new SymfonyNotificationAdapter((new ControllerFeedback('Message sent from controller'))->emoji('👍')),
        );

        return $this->render('@ibexadesign/notification-sender-controller.html.twig');
    }
}

For the example, the notification is sent in a back office context for all editions and on the front end for Commerce edition. An empty template only extending the pagelayout is used for the demonstration.

templates/themes/admin/notification-sender-controller.html.twig:

1
{% extends '@ibexadesign/ui/layout.html.twig' %}

templates/themes/storefront/notification-sender-controller.html.twig:

1
{% extends '@ibexadesign/storefront/layout.html.twig' %}

In the back office, a notification sent as a flash message has the ibexa-alert--notification CSS class. This doesn't have a default style. For this example, the style will be the same as an existing alert message type.

The assets/scss/notifications.scss declares the CSS class ibexa-alert--notification as being the same as the ibexa-alert--info CSS class

1
2
3
4
5
6
7
@use '@ibexa-admin-ui/src/bundle/Resources/public/scss/_alerts.scss' as *;

.ibexa-alert {
    &--notification {
        @extend .ibexa-alert--info;
    }
}

This assets/scss/notifications.scss is added to the Admin UI layout in webpack.config.js:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const ibexaConfigManager = require('@ibexa/frontend-config/webpack-config/manager');
const getIbexaConfig = require('@ibexa/frontend-config/webpack-config/ibexa');
const ibexaConfig = getIbexaConfig();

ibexaConfigManager.add({
    ibexaConfig,
    entryName: 'ibexa-admin-ui-layout-css',
    newItems: [
        path.resolve(__dirname, './assets/scss/notifications.scss'),
    ],
});

module.exports = [ibexaConfig, ...customConfigs, projectConfig];

On the storefront, a notification sent as a flash message has the ibexa-store-notification--notification CSS class. This class already has a default style applied.

Subscribe to this new notification type in config/packages/notifications.yaml:

  • In the admin_group scope with the browser channel
  • For Commerce edition, in the storefront_group scope with the browser channel
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
ibexa:
    system:
        # …
        admin_group:
            notifier:
                subscriptions:
                    # The configuration below is added to the `admin_group` scope without overriding the one defined in ibexa_admin_ui.yaml
                    # Custom subscriptions:
                    App\Notifications\CommandExecuted:
                        channels:
                            - ibexa
                            - email
                    App\Notifications\ControllerFeedback:
                        channels:
                            - browser
        storefront_group:
            notifier:
                subscriptions:
                    # The configuration defined in ibexa.yaml for `default` scope is repeated as the configuration below overrides it
                    Ibexa\Contracts\User\Notification\UserPasswordReset:
                        channels:
                            - email
                    Ibexa\Contracts\User\Notification\UserInvitation:
                        channels:
                            - email
                    Ibexa\Contracts\FormBuilder\Notifications\FormSubmitted:
                        channels:
                            - email
                    # Custom subscriptions:
                    Ibexa\OrderManagement\Notification\OrderStatusChange:
                        channels:
                            - chat
                    Ibexa\Payment\Notification\PaymentStatusChange:
                        channels:
                            - chat
                    Ibexa\Shipping\Notification\ShipmentStatusChange:
                        channels:
                            - chat
                    App\Notifications\CommandExecuted:
                        channels:
                            - ibexa
                            - email
                    App\Notifications\ControllerFeedback:
                        channels:
                            - browser

Subscriptions for storefront_group

Note that when introducing subscriptions configuration for the storefront_group scope that comes with Commerce edition, several subscriptions had to be copy-pasted into this SiteAccess group to have the same subscriptions as before when it was configured only by the default scope. For example, the subscriptions for the site SiteAccess belonging to this group can be checked with the following command during configuration:

1
php bin/console ibexa:debug:config notifications.subscriptions --siteaccess=site

Reaching this controller in the back office (at /admin/notification-sender) triggers the notification as a flash message in the bottom-right corner:

Notification in back office

Reaching the controller in the default SiteAccess on Commerce edition (at /notification-sender) also triggers the notification as a flash message in the bottom-right corner:

Notification in storefront

Create a custom channel

You may need to create new channels to subscribe to notifications and send them to new destinations. For example, you could create a new channel for Slack that takes more than one DSN for finer dispatching.

A channel is a service implementing Symfony\Component\Notifier\Channel\ChannelInterface, and tagged notifier.channel alongside a channel identifier.

The following example is a custom channel that sends notifications to the logger.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php declare(strict_types=1);

namespace App\Notifier\Channel;

use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Symfony\Component\Notifier\Channel\ChannelInterface;
use Symfony\Component\Notifier\Notification\Notification;
use Symfony\Component\Notifier\Recipient\RecipientInterface;

class LogChannel implements ChannelInterface, LoggerAwareInterface
{
    use LoggerAwareTrait;

    public function notify(Notification $notification, RecipientInterface $recipient, ?string $transportName = null): void
    {
        if (isset($this->logger)) {
            $this->logger->info($notification->getSubject(), [
                'class' => $notification::class,
                'importance' => $notification->getImportance(),
                'content' => $notification->getContent(),
            ]);
        }
    }

    public function supports(Notification $notification, RecipientInterface $recipient): bool
    {
        return true;
    }
}
1
2
3
4
services:
    App\Notifier\Channel\LogChannel:
        tags:
            - { name: 'notifier.channel', channel: 'log' }

Now, the CommandExecuted notification can be subscribed to the log channel:

1
2
3
4
5
                    App\Notifications\CommandExecuted:
                        channels:
                            - ibexa
                            - email
                            - log

The log contains the notifications (in var/log/dev.log when run in the dev Symfony environment):

1
2
3
% tail -Fn0 var/log/dev.log | grep --line-buffered CommandExecuted
[2026-03-26T01:01:54.123431+01:00] app.INFO: ✔app:send_notification {"class":"App\\Notifications\\CommandExecuted","importance":"low","content":""} []
[2026-03-27T01:01:23.888014+01:00] app.INFO: ✖app:send_notification {"class":"App\\Notifications\\CommandExecuted","importance":"high","content":""} []