Skip to content

Notification channels

the ibexa/notifications package offers an extension to the Symfony notifier allowing to subscribe to notifications and sent them to information channels like email addresses, SMS, communication platforms, etc., including the 🔔 Back Office user profile notification.

Those notifications must not be confused with the notification bars (sent with TranslatableNotificationHandlerInterface) or the 🔔 user notifications (sent with Ibexa\Contracts\Core\Repository\NotificationService).

TODO: Introduce the Ibexa\Contracts\Notifications\Service\NotificationServiceInterface

Subscribe to notifications

Some events send notifications you can subscribe to with one or more channels.

Available notifications:

TODO: What about notifications outside the Ibexa\Contracts namespace??

  • Ibexa\Share\Notification\ContentEditInvitationNotification
  • Ibexa\Share\Notification\ContentViewInvitationNotification
  • Ibexa\Share\Notification\ExternalParticipantContentViewInvitationNotification

Available notification channels:

1
php bin/console debug:container --tag=notifier.channel

For example, let's subscribe to Commerce activity with a Slack channel:

1
composer require symfony/slack-notifier
  • browser - Notification as flash message TODO: Test from a controller to see if it works
  • chat - Notification sent to a communication platform like Slack, Microsoft Teams, Google Chat, etc.
  • desktop - Notification sent to JoliNotif TODO: Do we support this?
  • email - Notification sent to email addresses
  • ibexa - Notification sent to back office user profiles
  • push - TODO
  • sms - Notification sent to phone numbers

In a .env file, set the DSN for the targetted Slack channel or user:

1
SLACK_DSN=slack://xoxb-token@default?channel=ibexa-notifications
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
framework:
    notifier:
        chatter_transports:
            slack: '%env(SLACK_DSN)%'
ibexa:
    system:
        default:
            notifier:
                subscriptions:
                    Ibexa\OrderManagement\Notification\OrderStatusChange:
                        channels:
                            - chat
                    Ibexa\Payment\Notification\PaymentStatusChange:
                        channels:
                            - chat
                    Ibexa\Shipping\Notification\ShipmentStatusChange:
                        channels:
                            - chat

Create a notification class

A new notification class can be created to send a new type of message to a new set of channels. It must extend Symfony\Component\Notifier\Notification\Notification and optionally implements some interfaces depending on the channels it could be sent to.

  • Some channels don't accept the notification if it doesn't implement its related notification interface.
  • Some channels accept every notification and have a default behavior if the notification doesn't implement their related notification interface.

TODO: List what type of channel notification interfaces can be implemented TODO: Namespaces, Ibexa custom vs Symfony native

Channel Notification interface ! Description
chat ChatNotificationInterface TODO
email EmailNotificationInterface TODO
ibexa SystemNotificationInterface TODO
sms SmsNotificationInterface TODO

TODO: About ibexa channel being the 🔔 user notification https://github.com/ibexa/notifications/blob/v5.0.6/src/lib/SystemNotification/SystemNotificationChannel.php#L51

TODO: How to deal with channels not needing a user like chat + Slack channel?

TODO: About SymfonyNotificationAdapter and SymfonyRecipientAdapter

Example

The following example is a command that sends a notification to users on several channels simultaneously. it could be a scheduled task, a cronjob, warning users about its final result.

First, a CommandExecuted notification type is created. It is supported by two channels for the example but could be extended to more.

 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' => Command::SUCCESS === $this->exitCode ? 'No error' : count($this->exceptions) . ' error' . (count($this->exceptions) > 1 ? 's' : ''),
        ]);

        return $message;
    }
}

The channels subscribing to this notification are set in config/packages/ibexa.yaml:

1
2
3
4
5
6
7
8
9
    system:
        default:
            notifier:
                subscriptions:
                # …
                    App\Notifications\CommandExecuted:
                        channels:
                            - ibexa
                            - email

TODO: Explain the command

 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\src\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 (rand(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;
    }
}

TODO: Screenshots

Create a channel

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

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' => get_class($notification),
                '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' }