From 89296c0aad8e9a56b5e2da3c73eb8d3a20a86a69 Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Tue, 17 Mar 2026 09:35:58 +0100 Subject: [PATCH 01/57] Draft ibexa/notifications doc --- .../notifications/config/packages/ibexa.yaml | 18 +++++ .../src/Command/NotificationSenderCommand.php | 67 +++++++++++++++++++ .../src/Notifications/CommandExecuted.php | 55 +++++++++++++++ docs/users/notifications.md | 60 +++++++++++++++++ mkdocs.yml | 1 + 5 files changed, 201 insertions(+) create mode 100644 code_samples/user_management/notifications/config/packages/ibexa.yaml create mode 100644 code_samples/user_management/notifications/src/Command/NotificationSenderCommand.php create mode 100644 code_samples/user_management/notifications/src/Notifications/CommandExecuted.php create mode 100644 docs/users/notifications.md diff --git a/code_samples/user_management/notifications/config/packages/ibexa.yaml b/code_samples/user_management/notifications/config/packages/ibexa.yaml new file mode 100644 index 0000000000..5a385e7e5f --- /dev/null +++ b/code_samples/user_management/notifications/config/packages/ibexa.yaml @@ -0,0 +1,18 @@ +ibexa: + system: + default: + notifier: + subscriptions: + Ibexa\Contracts\User\Notification\UserPasswordReset: + channels: + - email + Ibexa\Contracts\User\Notification\UserInvitation: + channels: + - email + Ibexa\Contracts\FormBuilder\Notifications\FormSubmitted: + channels: + - email + App\Notifications\CommandExecuted: + channels: + - ibexa + - email diff --git a/code_samples/user_management/notifications/src/Command/NotificationSenderCommand.php b/code_samples/user_management/notifications/src/Command/NotificationSenderCommand.php new file mode 100644 index 0000000000..4d30f95423 --- /dev/null +++ b/code_samples/user_management/notifications/src/Command/NotificationSenderCommand.php @@ -0,0 +1,67 @@ + $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 $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; + } +} diff --git a/code_samples/user_management/notifications/src/Notifications/CommandExecuted.php b/code_samples/user_management/notifications/src/Notifications/CommandExecuted.php new file mode 100644 index 0000000000..47f2a5b3f8 --- /dev/null +++ b/code_samples/user_management/notifications/src/Notifications/CommandExecuted.php @@ -0,0 +1,55 @@ + $exceptions */ + public function __construct( + private readonly Command $command, + private readonly int $exitCode, + private readonly array $exceptions + ) { + parent::__construct($this->command->getName()); + } + + public function asEmailMessage(EmailRecipientInterface $recipient, ?string $transport = null): ?EmailMessage + { + $subject = (0 === $this->exitCode ? '✔' : '✖') . $this->command->getName(); + + $body = ''; + foreach ($this->exceptions as $exception) { + $body .= $exception->getMessage() . '
'; + } + + $email = NotificationEmail::asPublicEmail() + ->to($recipient->getEmail()) + ->subject($subject) + ->html($body); + + return new EmailMessage($email); + } + + public function asSystemNotification(UserRecipientInterface $recipient, ?string $transport = null): ?SystemMessage + { + $message = new SystemMessage($recipient->getUser()); + $message->setContext([ + 'icon' => 0 === $this->exitCode ? 'check-circle' : 'discard-circle', + 'subject' => $this->command->getName(), + 'content' => 0 === $this->exitCode ? 'No error' : count($this->exceptions) . ' error' . (count($this->exceptions) > 1 ? 's' : ''), + ]); + + return $message; + } +} diff --git a/docs/users/notifications.md b/docs/users/notifications.md new file mode 100644 index 0000000000..df648661aa --- /dev/null +++ b/docs/users/notifications.md @@ -0,0 +1,60 @@ +--- +description: Notify users TODO. +month_change: true +--- + +# Notifications + +TODO: [notification bar](/administration/back_office/notifications.md) VS [🔔 user notification](/administration/back_office/notifications.md#create-custom-notifications) VS this + +TODO: +[Ibexa\Contracts\Core\Repository\NotificationService](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-NotificationService.html) +versus +[Ibexa\Contracts\Notifications\Service\NotificationServiceInterface](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-Service-NotificationServiceInterface.html) + +Based on [Symfony notifier]([[= symfony_doc =]]/notifier.html) + +Use an existing notification class + +TODO: List available classes + +Create a notification class + +TODO: List what type of channel notification interfaces can be implemented +TODO: Namespaces, Ibexa custom vs Symfony native + +| Channel | Notification interface | Description | +|:--------|:----------------------------------------------------------------------------------------------------------------------------------------------------------|:------------| +| `ibexa` | [`SystemNotificationInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-SystemNotification-SystemNotificationInterface.html) | TODO | +| `email` | `EmailNotificationInterface` | TODO | +| `chat` | `ChatNotificationInterface` | TODO | +| `sms` | `SmsNotificationInterface` | TODO | + +TODO: About `ibexa` channel being the [🔔 user notification](/administration/back_office/notifications.md#create-custom-notifications) +https://github.com/ibexa/notifications/blob/v5.0.6/src/lib/SystemNotification/SystemNotificationChannel.php#L51 + +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. + +``` php +[[= include_file('code_samples/user_management/notifications/src/Notifications/CommandExecuted.php') =]] +``` + +The channels subscribing to this notification are set in `config/packages/ibexa.yaml` below the default ones: + +``` yaml +[[= include_file('code_samples/user_management/notifications/config/packages/ibexa.yaml') =]] +``` + +TODO: Explain the command + +``` php +[[= include_file('code_samples/user_management/notifications/src/Command/NotificationSenderCommand.php') =]] +``` diff --git a/mkdocs.yml b/mkdocs.yml index c7cb98d738..6262ffb473 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -479,6 +479,7 @@ nav: - User grouping: - Customer groups: users/customer_groups.md - Segment API: users/segment_api.md + - User notifications: users/notifications.md - Personalization: - Personalization: personalization/personalization.md - Personalization guide : personalization/personalization_guide.md From 0e7ca027af40d479677ba6c7d1a6c89f191a5b48 Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Tue, 17 Mar 2026 09:37:14 +0100 Subject: [PATCH 02/57] install_with_ddev.md: Mailer --- docs/getting_started/install_with_ddev.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/getting_started/install_with_ddev.md b/docs/getting_started/install_with_ddev.md index bc28a942c8..22b5122471 100644 --- a/docs/getting_started/install_with_ddev.md +++ b/docs/getting_started/install_with_ddev.md @@ -88,6 +88,14 @@ Depending on your database of choice (MySQL or PostgreSQL), use the appropriate ddev config --web-environment-add DATABASE_URL=postgresql://db:db@db:5432/db ``` +#### Configure mailer (optional) + +You can configure [Symfony Mailer]([[= symfony_doc =]]/mailer.html) to use the [integrated mail catcher Mailpit](https://docs.ddev.com/en/stable/users/usage/developer-tools/#email-capture-and-review-mailpit): + +```bash +ddev config --web-environment-add MAILER_DSN=smtp://localhost:1025 +``` + #### Enable Mutagen (optional) If you're using macOS or Windows, you might want to enable [Mutagen](https://ddev.readthedocs.io/en/latest/users/install/performance/#mutagen) to improve performance. From a137ac02d904e50162e291b856471d91545622cd Mon Sep 17 00:00:00 2001 From: adriendupuis Date: Tue, 17 Mar 2026 08:55:08 +0000 Subject: [PATCH 03/57] PHP & JS CS Fixes --- .../notifications/src/Notifications/CommandExecuted.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code_samples/user_management/notifications/src/Notifications/CommandExecuted.php b/code_samples/user_management/notifications/src/Notifications/CommandExecuted.php index 47f2a5b3f8..819879c97e 100644 --- a/code_samples/user_management/notifications/src/Notifications/CommandExecuted.php +++ b/code_samples/user_management/notifications/src/Notifications/CommandExecuted.php @@ -1,4 +1,4 @@ - Date: Tue, 17 Mar 2026 12:37:08 +0100 Subject: [PATCH 04/57] BO notifications.md: Minor fixes --- docs/administration/back_office/notifications.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/administration/back_office/notifications.md b/docs/administration/back_office/notifications.md index 29c2d69f54..60e57f09bd 100644 --- a/docs/administration/back_office/notifications.md +++ b/docs/administration/back_office/notifications.md @@ -5,17 +5,17 @@ month_change: false # Notifications -You can send two types on notifications to the users. +You can send two types of notifications to the users. [Notification bar](#notification-bars) is displayed in specific situations as a message bar appearing at the bottom of the page. It appears to whoever is doing a specific operation in the back office. -![Example of an info notification](notification2.png "Example of the notification bar") +![Example of an info notification](notification2.png "Example of notification bar") [Custom notifications](#create-custom-notifications) are sent to a specific user. They appear in their profile in the back office. -![Notification in profile](notification3.png) +![Notification in profile](notification3.png "Profile notification bell menu") ## Notification bars @@ -56,7 +56,7 @@ Dispatch the event with `document.body.dispatchEvent(eventInfo);`. You can send your own custom notifications to the user which are displayed in the user menu. -To create a new notification you must use the `createNotification(Ibexa\Contracts\Core\Repository\Values\Notification\CreateStruct $createStruct)` method from `Ibexa\Contracts\Core\Repository\NotificationService`. +To create a new notification you can use the [core `NotificationService::createNotification(CreateStruct $createStruct)` method](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-NotificationService.html#method_createNotification) like in the example below. Example: @@ -117,3 +117,9 @@ ibexa: The values shown above are the defaults. `0` means the notification doesn't hide automatically. + +### `ibexa` notification channel + +You can also subscribe to a notification with the channel `ibexa` + +See [Notifications to users](users/notifications.md) for more details about notifications and channel subscription. From 43b3df8536aca850b083f1cc17be3473f51c2257 Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Tue, 17 Mar 2026 12:39:13 +0100 Subject: [PATCH 05/57] Users notifications.md: Continue --- .../config/packages/custom_notifications.yaml | 22 ++++++++++ .../notifications/config/packages/ibexa.yaml | 18 -------- docs/users/notifications.md | 44 ++++++++++++++----- 3 files changed, 56 insertions(+), 28 deletions(-) create mode 100644 code_samples/user_management/notifications/config/packages/custom_notifications.yaml delete mode 100644 code_samples/user_management/notifications/config/packages/ibexa.yaml diff --git a/code_samples/user_management/notifications/config/packages/custom_notifications.yaml b/code_samples/user_management/notifications/config/packages/custom_notifications.yaml new file mode 100644 index 0000000000..5674488f6d --- /dev/null +++ b/code_samples/user_management/notifications/config/packages/custom_notifications.yaml @@ -0,0 +1,22 @@ +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 + App\Notifications\CommandExecuted: + channels: + - ibexa + - email diff --git a/code_samples/user_management/notifications/config/packages/ibexa.yaml b/code_samples/user_management/notifications/config/packages/ibexa.yaml deleted file mode 100644 index 5a385e7e5f..0000000000 --- a/code_samples/user_management/notifications/config/packages/ibexa.yaml +++ /dev/null @@ -1,18 +0,0 @@ -ibexa: - system: - default: - notifier: - subscriptions: - Ibexa\Contracts\User\Notification\UserPasswordReset: - channels: - - email - Ibexa\Contracts\User\Notification\UserInvitation: - channels: - - email - Ibexa\Contracts\FormBuilder\Notifications\FormSubmitted: - channels: - - email - App\Notifications\CommandExecuted: - channels: - - ibexa - - email diff --git a/docs/users/notifications.md b/docs/users/notifications.md index df648661aa..41d7bb3e0f 100644 --- a/docs/users/notifications.md +++ b/docs/users/notifications.md @@ -5,18 +5,39 @@ month_change: true # Notifications -TODO: [notification bar](/administration/back_office/notifications.md) VS [🔔 user notification](/administration/back_office/notifications.md#create-custom-notifications) VS this +the `ibexa/notifications` package offers an extension to the [Symfony notifier]([[= symfony_doc =]]/notifier.html) 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](/administration/back_office/notifications.md#create-custom-notifications). -TODO: -[Ibexa\Contracts\Core\Repository\NotificationService](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-NotificationService.html) -versus -[Ibexa\Contracts\Notifications\Service\NotificationServiceInterface](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-Service-NotificationServiceInterface.html) +Those notifications must not be confused with the [notification bars](/administration/back_office/notifications.md) (sent with [`TranslatableNotificationHandlerInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-AdminUi-Notification-TranslatableNotificationHandlerInterface.html)) +or the [🔔 user notifications](/administration/back_office/notifications.md#create-custom-notifications) (sent with [`Ibexa\Contracts\Core\Repository\NotificationService`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-NotificationService.html)). -Based on [Symfony notifier]([[= symfony_doc =]]/notifier.html) +TODO: Introduce the [`Ibexa\Contracts\Notifications\Service\NotificationServiceInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-Service-NotificationServiceInterface.html) -Use an existing notification class +## Subscribe to notifications -TODO: List available classes +Some events send notifications you can subscribe to with one or more channels. + +Available notifications: + +* [`Ibexa\Contracts\FormBuilder\Notifications\FormSubmitted`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-FormBuilder-Notifications-FormSubmitted.html) +* [`Ibexa\Contracts\Notifications\SystemNotification\SystemNotification`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-SystemNotification-SystemNotification.html) +* [`Ibexa\Contracts\OrderManagement\Notification\OrderStatusChange`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-OrderManagement-Notification-OrderStatusChange.html) +* [`Ibexa\Contracts\Payment\Notification\PaymentStatusChange`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Payment-Notification-PaymentStatusChange.html) +* [`Ibexa\Contracts\Shipping\Notification\ShipmentStatusChange`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Shipping-Notification-ShipmentStatusChange.html) +* [`Ibexa\Contracts\User\Notification\UserInvitation`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-User-Notification-UserInvitation.html) +* [`Ibexa\Contracts\User\Notification\UserPasswordReset`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-User-Notification-UserPasswordReset.html) +* [`Ibexa\Contracts\User\Notification\UserRegister`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-User-Notification-UserRegister.html) + +TODO: What about notifications outside the `Ibexa\Contracts` namespace?? + +* `Ibexa\Share\Notification\ContentEditInvitationNotification` +* `Ibexa\Share\Notification\ContentViewInvitationNotification` +* `Ibexa\Share\Notification\ExternalParticipantContentViewInvitationNotification` + +For example, let's subscribe to Commerce activity with a Slack channel: + +``` yaml +[[= include_file('code_samples/user_management/notifications/config/packages/custom_notifications.yaml', 0, 18) =]] +``` Create a notification class @@ -33,6 +54,8 @@ TODO: Namespaces, Ibexa custom vs Symfony native TODO: About `ibexa` channel being the [🔔 user notification](/administration/back_office/notifications.md#create-custom-notifications) 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 @@ -47,10 +70,11 @@ It is supported by two channels for the example but could be extended to more. [[= include_file('code_samples/user_management/notifications/src/Notifications/CommandExecuted.php') =]] ``` -The channels subscribing to this notification are set in `config/packages/ibexa.yaml` below the default ones: +The channels subscribing to this notification are set in `config/packages/ibexa.yaml`: ``` yaml -[[= include_file('code_samples/user_management/notifications/config/packages/ibexa.yaml') =]] +[[= include_file('code_samples/user_management/notifications/config/packages/custom_notifications.yaml', 5, 9) =]] # … +[[= include_file('code_samples/user_management/notifications/config/packages/custom_notifications.yaml', 18) =]] ``` TODO: Explain the command From 96f91460842aaddd8291035126154cd51dcb06dd Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Tue, 17 Mar 2026 15:35:42 +0100 Subject: [PATCH 06/57] CommandExecuted enhancement --- .../src/Notifications/CommandExecuted.php | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/code_samples/user_management/notifications/src/Notifications/CommandExecuted.php b/code_samples/user_management/notifications/src/Notifications/CommandExecuted.php index 819879c97e..489cce61bb 100644 --- a/code_samples/user_management/notifications/src/Notifications/CommandExecuted.php +++ b/code_samples/user_management/notifications/src/Notifications/CommandExecuted.php @@ -21,13 +21,12 @@ public function __construct( private readonly int $exitCode, private readonly array $exceptions ) { - parent::__construct($this->command->getName()); + 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 { - $subject = (0 === $this->exitCode ? '✔' : '✖') . $this->command->getName(); - $body = ''; foreach ($this->exceptions as $exception) { $body .= $exception->getMessage() . '
'; @@ -35,7 +34,7 @@ public function asEmailMessage(EmailRecipientInterface $recipient, ?string $tran $email = NotificationEmail::asPublicEmail() ->to($recipient->getEmail()) - ->subject($subject) + ->subject($this->getSubject()) ->html($body); return new EmailMessage($email); @@ -45,9 +44,9 @@ public function asSystemNotification(UserRecipientInterface $recipient, ?string { $message = new SystemMessage($recipient->getUser()); $message->setContext([ - 'icon' => 0 === $this->exitCode ? 'check-circle' : 'discard-circle', + 'icon' => Command::SUCCESS === $this->exitCode ? 'check-circle' : 'discard-circle', 'subject' => $this->command->getName(), - 'content' => 0 === $this->exitCode ? 'No error' : count($this->exceptions) . ' error' . (count($this->exceptions) > 1 ? 's' : ''), + 'content' => Command::SUCCESS === $this->exitCode ? 'No error' : count($this->exceptions) . ' error' . (count($this->exceptions) > 1 ? 's' : ''), ]); return $message; From ffe904df8902bf2c69657316d13fab5caf93f8ff Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Tue, 17 Mar 2026 15:42:55 +0100 Subject: [PATCH 07/57] Continue users/notifications.md; Add channel examples --- .../notifications/config/services.yaml | 4 ++ .../src/Notifier/Channel/LogChannel.php | 28 +++++++++ docs/users/notifications.md | 61 ++++++++++++++++--- 3 files changed, 86 insertions(+), 7 deletions(-) create mode 100644 code_samples/user_management/notifications/config/services.yaml create mode 100644 code_samples/user_management/notifications/src/Notifier/Channel/LogChannel.php diff --git a/code_samples/user_management/notifications/config/services.yaml b/code_samples/user_management/notifications/config/services.yaml new file mode 100644 index 0000000000..51d97cdfbe --- /dev/null +++ b/code_samples/user_management/notifications/config/services.yaml @@ -0,0 +1,4 @@ +services: + App\Notifier\Channel\LogChannel: + tags: + - { name: 'notifier.channel', channel: 'log' } diff --git a/code_samples/user_management/notifications/src/Notifier/Channel/LogChannel.php b/code_samples/user_management/notifications/src/Notifier/Channel/LogChannel.php new file mode 100644 index 0000000000..8611a69a3d --- /dev/null +++ b/code_samples/user_management/notifications/src/Notifier/Channel/LogChannel.php @@ -0,0 +1,28 @@ +logger->info($notification->getSubject(), [ + 'class' => get_class($notification), + 'importance' => $notification->getImportance(), + 'content' => $notification->getContent(), + ]); + } + + public function supports(Notification $notification, RecipientInterface $recipient): bool + { + return true; + } +} diff --git a/docs/users/notifications.md b/docs/users/notifications.md index 41d7bb3e0f..fb2e10864a 100644 --- a/docs/users/notifications.md +++ b/docs/users/notifications.md @@ -33,23 +33,54 @@ TODO: What about notifications outside the `Ibexa\Contracts` namespace?? * `Ibexa\Share\Notification\ContentViewInvitationNotification` * `Ibexa\Share\Notification\ExternalParticipantContentViewInvitationNotification` +Available notification channels: + +```bash +php bin/console debug:container --tag=notifier.channel +``` + For example, let's subscribe to Commerce activity with a Slack channel: +```bash +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](https://github.com/symfony/slack-notifier?tab=readme-ov-file#dsn-example): + +```dotenv +SLACK_DSN=slack://xoxb-token@default?channel=ibexa-notifications +``` + ``` yaml [[= include_file('code_samples/user_management/notifications/config/packages/custom_notifications.yaml', 0, 18) =]] ``` -Create a notification class +## 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 | -|:--------|:----------------------------------------------------------------------------------------------------------------------------------------------------------|:------------| -| `ibexa` | [`SystemNotificationInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-SystemNotification-SystemNotificationInterface.html) | TODO | -| `email` | `EmailNotificationInterface` | TODO | -| `chat` | `ChatNotificationInterface` | TODO | -| `sms` | `SmsNotificationInterface` | TODO | +| Channel | Notification interface | ! | Description | +|:--------|:----------------------------------------------------------------------------------------------------------------------------------------------------------|---|:------------| +| `chat` | `ChatNotificationInterface` | | TODO | +| `email` | `EmailNotificationInterface` | ✔ | TODO | +| `ibexa` | [`SystemNotificationInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-SystemNotification-SystemNotificationInterface.html) | ✔ | TODO | +| `sms` | `SmsNotificationInterface` | ✔ | TODO | TODO: About `ibexa` channel being the [🔔 user notification](/administration/back_office/notifications.md#create-custom-notifications) https://github.com/ibexa/notifications/blob/v5.0.6/src/lib/SystemNotification/SystemNotificationChannel.php#L51 @@ -82,3 +113,19 @@ TODO: Explain the command ``` php [[= include_file('code_samples/user_management/notifications/src/Command/NotificationSenderCommand.php') =]] ``` + +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. + +``` php +[[= include_file('code_samples/user_management/notifications/src/Notifier/Channel/LogChannel.php') =]] +``` + +``` yaml +[[= include_file('code_samples/user_management/notifications/config/services.yaml') =]] +``` From 0d9abdcc33723ab68c5dd5a701d9f87c945105a4 Mon Sep 17 00:00:00 2001 From: adriendupuis Date: Tue, 17 Mar 2026 14:53:04 +0000 Subject: [PATCH 08/57] PHP & JS CS Fixes --- .../notifications/src/Notifier/Channel/LogChannel.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code_samples/user_management/notifications/src/Notifier/Channel/LogChannel.php b/code_samples/user_management/notifications/src/Notifier/Channel/LogChannel.php index 8611a69a3d..3bf3bda913 100644 --- a/code_samples/user_management/notifications/src/Notifier/Channel/LogChannel.php +++ b/code_samples/user_management/notifications/src/Notifier/Channel/LogChannel.php @@ -1,4 +1,4 @@ - Date: Wed, 18 Mar 2026 10:05:31 +0100 Subject: [PATCH 09/57] =?UTF-8?q?users/notifications.md=20=E2=86=92=20user?= =?UTF-8?q?s/notification=5Fchannels.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mainly to avoid confusion with administration/back_office/notifications.md and fix duplicate: WARNING - Doc file 'administration/back_office/notifications.md' contains a link 'users/notifications.md', but the target 'administration/back_office/users/notifications.md' is not found among documentation files. WARNING - AutoLinksPlugin: Duplicate filename referred to in 'docs/administration/project_organization/bundles.md': 'notifications.md' exists at ['docs/administration/back_office/notifications.md', 'docs/users/notifications.md'] WARNING - AutoLinksPlugin: Duplicate filename referred to in 'docs/api/event_reference/other_events.md': 'notifications.md' exists at ['docs/administration/back_office/notifications.md', 'docs/users/notifications.md'] WARNING - AutoLinksPlugin: Duplicate filename referred to in 'docs/resources/new_in_doc.md': 'notifications.md' exists at ['docs/administration/back_office/notifications.md', 'docs/users/notifications.md'] --- docs/administration/back_office/notifications.md | 2 +- docs/administration/project_organization/bundles.md | 2 +- .../{notifications.md => notification_channels.md} | 10 +++++----- mkdocs.yml | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) rename docs/users/{notifications.md => notification_channels.md} (89%) diff --git a/docs/administration/back_office/notifications.md b/docs/administration/back_office/notifications.md index 60e57f09bd..5e41f3dd3e 100644 --- a/docs/administration/back_office/notifications.md +++ b/docs/administration/back_office/notifications.md @@ -122,4 +122,4 @@ The values shown above are the defaults. You can also subscribe to a notification with the channel `ibexa` -See [Notifications to users](users/notifications.md) for more details about notifications and channel subscription. +See [Notifications to channels](notification_channels.md) for more details about notifications and channel subscription. diff --git a/docs/administration/project_organization/bundles.md b/docs/administration/project_organization/bundles.md index be3c393e9a..36bcf221d5 100644 --- a/docs/administration/project_organization/bundles.md +++ b/docs/administration/project_organization/bundles.md @@ -54,7 +54,7 @@ To remove a bundle (either one you created yourself, or an out-of-the-box one th |[ibexa/http-cache](https://github.com/ibexa/http-cache)|[HTTP cache handling](http_cache.md), using multi tagging| |[ibexa/i18n](https://github.com/ibexa/i18n)|Centralized translations to ease synchronization with Crowdin| |[ibexa/messenger](https://github.com/ibexa/messenger)|[Background and asynchronous task processing](background_tasks.md) using Symfony Messenger| -|[ibexa/notifications](https://github.com/ibexa/notifications)| Sending [notifications](notifications.md)| +|[ibexa/notifications](https://github.com/ibexa/notifications)| Sending [notifications to channels](notification_channels.md)| |[ibexa/post-install](https://github.com/ibexa/post-install)|Apache and nginx templates| |[ibexa/rest](https://github.com/ibexa/rest)|REST API| |[ibexa/search](https://github.com/ibexa/search)|Common search functionalities| diff --git a/docs/users/notifications.md b/docs/users/notification_channels.md similarity index 89% rename from docs/users/notifications.md rename to docs/users/notification_channels.md index fb2e10864a..839766daf8 100644 --- a/docs/users/notifications.md +++ b/docs/users/notification_channels.md @@ -3,12 +3,12 @@ description: Notify users TODO. month_change: true --- -# Notifications +# Notification channels -the `ibexa/notifications` package offers an extension to the [Symfony notifier]([[= symfony_doc =]]/notifier.html) 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](/administration/back_office/notifications.md#create-custom-notifications). +the `ibexa/notifications` package offers an extension to the [Symfony notifier]([[= symfony_doc =]]/notifier.html) 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](notifications.md#create-custom-notifications). -Those notifications must not be confused with the [notification bars](/administration/back_office/notifications.md) (sent with [`TranslatableNotificationHandlerInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-AdminUi-Notification-TranslatableNotificationHandlerInterface.html)) -or the [🔔 user notifications](/administration/back_office/notifications.md#create-custom-notifications) (sent with [`Ibexa\Contracts\Core\Repository\NotificationService`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-NotificationService.html)). +Those notifications must not be confused with the [notification bars](notifications.md#notification-bars) (sent with [`TranslatableNotificationHandlerInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-AdminUi-Notification-TranslatableNotificationHandlerInterface.html)) +or the [🔔 user notifications](notifications.md#create-custom-notifications) (sent with [`Ibexa\Contracts\Core\Repository\NotificationService`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-NotificationService.html)). TODO: Introduce the [`Ibexa\Contracts\Notifications\Service\NotificationServiceInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-Service-NotificationServiceInterface.html) @@ -82,7 +82,7 @@ TODO: Namespaces, Ibexa custom vs Symfony native | `ibexa` | [`SystemNotificationInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-SystemNotification-SystemNotificationInterface.html) | ✔ | TODO | | `sms` | `SmsNotificationInterface` | ✔ | TODO | -TODO: About `ibexa` channel being the [🔔 user notification](/administration/back_office/notifications.md#create-custom-notifications) +TODO: About `ibexa` channel being the [🔔 user notification](notifications.md#create-custom-notifications) 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? diff --git a/mkdocs.yml b/mkdocs.yml index 6262ffb473..912fc50326 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -479,7 +479,7 @@ nav: - User grouping: - Customer groups: users/customer_groups.md - Segment API: users/segment_api.md - - User notifications: users/notifications.md + - User notifications: users/notification_channels.md - Personalization: - Personalization: personalization/personalization.md - Personalization guide : personalization/personalization_guide.md From 7aa8f3a96bcd765c13edd29e6c54c7828525be36 Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Wed, 18 Mar 2026 10:14:48 +0100 Subject: [PATCH 10/57] LogChannel: $this->logger isn't guaranteed Trying to fix "Cannot call method info() on Psr\Log\LoggerInterface|null." --- .../src/Notifier/Channel/LogChannel.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/code_samples/user_management/notifications/src/Notifier/Channel/LogChannel.php b/code_samples/user_management/notifications/src/Notifier/Channel/LogChannel.php index 3bf3bda913..80bc374566 100644 --- a/code_samples/user_management/notifications/src/Notifier/Channel/LogChannel.php +++ b/code_samples/user_management/notifications/src/Notifier/Channel/LogChannel.php @@ -14,11 +14,13 @@ class LogChannel implements ChannelInterface, LoggerAwareInterface public function notify(Notification $notification, RecipientInterface $recipient, ?string $transportName = null): void { - $this->logger->info($notification->getSubject(), [ - 'class' => get_class($notification), - 'importance' => $notification->getImportance(), - 'content' => $notification->getContent(), - ]); + 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 From 08408406fa15c975a77e4ec2cd1cdcae2d2b6f3d Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Thu, 26 Mar 2026 18:24:15 +0100 Subject: [PATCH 11/57] NotificationSenderCommand: Fix namespace --- .../notifications/src/Command/NotificationSenderCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code_samples/user_management/notifications/src/Command/NotificationSenderCommand.php b/code_samples/user_management/notifications/src/Command/NotificationSenderCommand.php index 4d30f95423..2788cbd8a6 100644 --- a/code_samples/user_management/notifications/src/Command/NotificationSenderCommand.php +++ b/code_samples/user_management/notifications/src/Command/NotificationSenderCommand.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\src\Command; +namespace App\Command; use App\Notifications\CommandExecuted; use Ibexa\Contracts\Core\Repository\UserService; From 54254abdf9bc5aa5fae1e56fef80d585ab4aa385 Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Fri, 27 Mar 2026 16:43:23 +0100 Subject: [PATCH 12/57] Continue notification_channels.md --- .../assets/scss/notifications.scss | 7 + .../config/packages/custom_notifications.yaml | 8 + .../NotificationSenderController.php | 28 ++++ .../src/Notifications/ControllerFeedback.php | 9 + .../notification-sender-controller.html.twig | 1 + .../notifications/webpack.config.js | 62 +++++++ docs/users/img/notification-browser.png | Bin 0 -> 10448 bytes docs/users/img/notification-ibexa.png | Bin 0 -> 37065 bytes docs/users/notification_channels.md | 155 ++++++++++++++---- 9 files changed, 234 insertions(+), 36 deletions(-) create mode 100644 code_samples/user_management/notifications/assets/scss/notifications.scss create mode 100644 code_samples/user_management/notifications/src/Controller/NotificationSenderController.php create mode 100644 code_samples/user_management/notifications/src/Notifications/ControllerFeedback.php create mode 100644 code_samples/user_management/notifications/templates/themes/admin/notification-sender-controller.html.twig create mode 100644 code_samples/user_management/notifications/webpack.config.js create mode 100644 docs/users/img/notification-browser.png create mode 100644 docs/users/img/notification-ibexa.png diff --git a/code_samples/user_management/notifications/assets/scss/notifications.scss b/code_samples/user_management/notifications/assets/scss/notifications.scss new file mode 100644 index 0000000000..6c880f226f --- /dev/null +++ b/code_samples/user_management/notifications/assets/scss/notifications.scss @@ -0,0 +1,7 @@ +@use '../../vendor/ibexa/admin-ui/src/bundle/Resources/public/scss/_alerts.scss' as *; + +.ibexa-alert { + &--notification { + @extend .ibexa-alert--info; + } +} diff --git a/code_samples/user_management/notifications/config/packages/custom_notifications.yaml b/code_samples/user_management/notifications/config/packages/custom_notifications.yaml index 5674488f6d..f8e665bde6 100644 --- a/code_samples/user_management/notifications/config/packages/custom_notifications.yaml +++ b/code_samples/user_management/notifications/config/packages/custom_notifications.yaml @@ -20,3 +20,11 @@ ibexa: channels: - ibexa - email + - log + admin_group: + notifier: + subscriptions: + App\Notifications\ControllerFeedback: + channels: + - browser + - log diff --git a/code_samples/user_management/notifications/src/Controller/NotificationSenderController.php b/code_samples/user_management/notifications/src/Controller/NotificationSenderController.php new file mode 100644 index 0000000000..2f8ee32037 --- /dev/null +++ b/code_samples/user_management/notifications/src/Controller/NotificationSenderController.php @@ -0,0 +1,28 @@ +notificationService->send( + new SymfonyNotificationAdapter((new ControllerFeedback('Message sent from controller'))->emoji('👍')), + ); + + return $this->render('@ibexadesign/notification-sender-controller.html.twig'); + } +} diff --git a/code_samples/user_management/notifications/src/Notifications/ControllerFeedback.php b/code_samples/user_management/notifications/src/Notifications/ControllerFeedback.php new file mode 100644 index 0000000000..0c82a0be11 --- /dev/null +++ b/code_samples/user_management/notifications/src/Notifications/ControllerFeedback.php @@ -0,0 +1,9 @@ + { + options.runtime = 'classic'; + }) + .enableSingleRuntimeChunk() + .copyFiles({ + from: './assets/images', + to: 'images/[path][name].[ext]', + pattern: /\.(png|svg)$/, + }) + .configureBabelPresetEnv((config) => { + config.useBuiltIns = 'usage'; + config.corejs = 3; + }); + +// Welcome page stylesheets +Encore.addEntry('welcome-page-css', [ + path.resolve(__dirname, './assets/scss/welcome-page.scss'), +]); + +// Welcome page javascripts +Encore.addEntry('welcome-page-js', [ + path.resolve(__dirname, './assets/js/welcome.page.js'), +]); + +if (isReactBlockPathCreated) { + // React Blocks javascript + Encore.addEntry('react-blocks-js', './assets/js/react.blocks.js'); +} + +Encore.addEntry('app', './assets/app.js'); + +const projectConfig = Encore.getWebpackConfig(); + +projectConfig.name = 'app'; + +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]; diff --git a/docs/users/img/notification-browser.png b/docs/users/img/notification-browser.png new file mode 100644 index 0000000000000000000000000000000000000000..912237897c125be4768612787108324976427dbc GIT binary patch literal 10448 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 E2jC*r8~^|S literal 0 HcmV?d00001 diff --git a/docs/users/img/notification-ibexa.png b/docs/users/img/notification-ibexa.png new file mode 100644 index 0000000000000000000000000000000000000000..1528635c9c6430012c1223ebd97abc4ba022f5fe GIT binary patch literal 37065 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 Y@PkMS17Ig3BuA2C|bYGo~_ z^PxaFXbV~mz=dz$zP)aHyRn+Ql7931v;D&EwD)wkpktp99^R(gwx5!5oF64I%dsd| zuxj5Kt?zqp_U-mz^@Chd@sV}^7h*=_#ICNL9U~Z*S-4K|yyk{>Gbgijw>Zf`ZfK4z@Xl zAC=@Wa0?aXIQTWLQhKx>YLRf?&*mTOLA-Z*aWUY?kbpZQp1!cpeBZ@`gHzZYjI|-D zMTao7iR5;|FT0FJVpdlvG$T$V$IC6T?r^rhFD&%VopI5;YhUlM&BX3<*41hPK3gTPKdc1o0^e zYO*i%{HhE0so;B@&ll*phflHma%1b#te~#|JY=Tx(*WiMR#T)z!+h>--2R zigS=Ju-Vzf?FRINL%rxqi;JxafU4AmCtX>N7uy9%4}&svaix%@AG?1LPEKknwq-_KKn0K`W>N4es8YdP^aoG zH4q&02uX8dh2BwYmutl%C@3kH%Zkk}f=W5<)_|LSFVRW9w+G|(&W8%amyN3+Z<5yq zH3+=s?NEqhy4BU15-o14VNstNGHAW+afZOYU4fRC7Ld8pXe*1te-(!sUs6&Mr}gzK z=XmB*G9qDSmYyc1Z`$+y>11KrxYz+LZg4L!$^ZQ+JZ|@TcY3Z|%enRW#M)vgj*Q%P zB1b4zt#zQauY-B!!zu~p?W=VXNe--IJfUWf1*au{!~bFL9sl!czPQntjoHRF8#Oi>vvJa} zvD4VLlg374+qTu%wtaT)_IIAo^9P)FCohw0=i0MpX3bh(tnKj(y11vk|Di*`Yw?0A zE+vKt50lsnf=7c5jmyl^wyDrY%bmOENLrsVgzS$BH-p1l316GlMznIM-WYUFs4Z^8$%eWK#KkjZL`bIaRj_tytwsiBXy?zb5p49J1)$ttcB9}bhekWc3O^q~&MEdypbY_Fs9@hyn z@bPfrlZ=VNXJZe#+do*^PQlP0&crn}H5(?fjoP*%4r*L(Og~CB zG`q^_>PkND=OFF8-rsXPuKV7pDTE`ZGOc%fPGUAhb-(ESW=FUo1Pz!$_r&P+DaZJO0rMWtL!&#@$x$~FUz`9&5Fg)KwsMDV$I?>Zxf_4a-&CVeIfXi?l91PX zbM@5=4mEZlnhaD-C9(^Y>Y*Tm%3)Fg>_7z*PVEIqfTiLED0gL+tK-|FNF$hjvWDi3 zrS{l=db<&dbEf`f>x@XlDX^dguUfi?zfNMR)3XP^dG0rx_!TW4zV5}_K2XN#VpB)0 zYe|Mir6dNCAZDk{-BqON;`}_WCttc5@H%soPARIZC)=-US`gt2V>9VNNajY{pS9jG zRqj?pJo$?&15cVBZI!0v6W-USyE807i?udz$=tPx2t3#Q_;1>KGObDJuBTs!W)ihs z{OhdN1QK;TAWJnHeaLzoVh~fECo0=-|KPZ-S=xa4(Agc$(FrmViu&EHx@P2^rV%3y z1yjFzxuF2rgdy_9wCt6z3q%n)DJkQDx!#n!4(TzUD$T{y*|1s@Vw5*nS?m4g@J#y~ zh{P6fJ{AZCMkojiLl9`-GX|Nhcf1N}2KhBS-Qs0xShXrc5pV>yWO)Siz&o68Me=p# zx{Id|&J-ourNQq{iV+mBn$6j1)qhSX-(;g7w(@}=z(%za7pK%&>-6=8jLA<)>5cf{ z6A>PsW1Qix9%R2OwcaL~&Xv1H;BhaLo|TxGC_Xn`@TQfHR)c_T@O;tFYzbZa_(|g7 zCK5=4SY#kf>)F?3IQAw`hQc{5qs0-(r7Q{Cu4j+@u}2RsHHJ+kR7CVthGfUd>y5Ng zG1wH4RJPbce~3UYd@pkDM^0z%#^Zx;6DP=xxL>GW?$~SWj)f#1GB)aF5{HB^wK;6` zBbUKth@r9c|AYnF<7NG@lUR*qp^>0YK4hab!*ps-w_3&lD#60d9X6fhCnUd^UupV0 zCCQKzNbp)<|LhBeKwjv8M8Ml~L2u;aCZd!?c#tE8H0&NF9*Br1D0e|@zEmfj`^Ek| zN|&M4p1NQ%el}ZZhcnk$+-h)8LXdW#-1Uw=ZzoRN_~EnlI={SCe#~m4iFFC0rijJptixt@Nj%6+WZhBT%enQeJ{gQ#gdGNSR}sT(g_xK zkUH6zUIVXghnIpfON2DzeX{<_dj^Q--cT)-z??)=VOak&jNtJ@VGCn&jq>#DX8Or_I-2 z;Bjbkmq5AG`gl4LBj^^*<)KqPaGb4YY0<^}t{u=X`&KDpjOehj-nl1HB1!+NM_~gw z8qsF62EstXU@)e4GpWXW@eCaklN`SS60!gqvqC5Wn<=&J>6A<2E`saSfOr&BV>QL9 zjefP)_3qS=^-@V?x-J{Y*t7@0OvgGpFt4Al7%tJH=*UqGS2C}LBABUkw$pKWJTUh2B5>IU^|?ZWQaoP>Xu`K)>ZlUmPU!sA zi7@|Zy_J>mKn2JEsRck@Wb>6XR3&QE`P}m(>M*Nd+TQ4Gjflmyo^depjiY4CPtl!@ zZloGcoZF~#{l0?TefzFBmZN_)ud$m`6@vPE8Ta;C>+PBL(Ducj<#~3A(!`2|i#9LH zSk4Y^e=b2!YriUPXzJ*EOl+WKA9d9 zKhKC_C~(8Gb8?*1$z8u()gVe0fp%V>uB7C3LT-B5ov#ZJ{Y>>uWNZo1aldi+wPcc? zrRgxs()jj#6=?J8(xfl!<4nGEVPV-){aFX<0)OSO9^40UP`j_C;A2@~46WD9iN&GF zaGwsQW%+2vl;|{|g25+&86*Nn8jkw}-3YBHEywFpJ> zyO$ua3S8D5aTwz9*f1S+uw2KP0=}eUv)^=vFF1#0@IJuEVd;WLt}RyYRdi1fWXoi5 zI#8qCX;U#kft}C2?B_%}GhUUJZ2Nh`DmDCE;}H@^i%?uNw`{l^DyF2R4QtIyytIiB z{}W3gOOGea^Y#pXY0fku_)9GO6PaP(XMSha_j2%s5M=s~UV7FA6&E$H_1T9{%uZ3) zIXCixfegkT8lns#ldYb1o=v*<*0#0*aHXGm*T=AA#6-B80-?)Eiq|XbGigC=61FRn zURAW|O!y=VWbg~VJ$iF~yi#ga6IrIsX60gVR>b#wq_f$RzkqS<5*z7+AOoE;FNa>B zGJfn~rlI?FRA)IkD3oS0fF=w&2#d!wCg9@#2XnPtl`Mz>=yV_f6(KXP<4O1Pk*|vV ztiDPe`8cg(L0m5>A6z5n3tK3MR|hg7S#pa!rcUbuSnS zq-Oeb@kh18QSHfNT|bvh?zgA4d2U1F#vO%0okFX##ywtgJ}IJ3Jq*m0ih_c`!rJPE zx^S0F+>ZV@EN3B?&xU;t)lN`{!H_hxH`cqHShmaDHY%6v#t6q@H6bioS^^ab98NVH z(Ea3!p4-06nPqZRB#2O)eP)Zf{`%jvgL;;sN7j`;RP&{iq*HYrT+>MpesZ0`IWRWd z5$6;~y5HHul1Q_bem7w(K!YWrHh6%K%iNE99%LXbi>)*ZFjm}4%oxH>&d&zt4z)%s zDB(>k#?N$M3W_L+C#{?%3l~K*+1t*RO^vq-=sDQh#UaHR_mK|Oe6(L6G`j$)6NEF} zaEu+#8Do!#X7w$lPrIV$)SUj3k`jK1B&=7KFIc{DThFP$cnRzIIfFt2Yp#O+<`OTMD%(@*WR+AKtLTHaU&``-Zr1-o@ zr@0%^U(tbsVz9N`F)k%AEVN*R6QD0oQxpU(l4HyLv(Q<|pXL#Yl4lx)c;-QZXJ*G^ zp?Ez>ES!$vLo}NEt3nhc#{Cp~51r8!NhX$R{e&InZ%U*(|J^9FmrWZYphVzKoMX zxidj(e`Cfuu)gP+gk!wrjo&Ef3*|Y5=|{eD@}nU$L36VVKSm4l)o6&prFE z%w7^*O*K2b0|WpRzRnN?!jg1x0n06vX;`#npw+EAA7g5=qsD+ZOAm$_cn%96)A&0Z zI^ZzT!c+AQ3sL6&e?ygn$ON+_SWT!}V1f22Ete|}<(qNH5=c&>R_vH2tk5K?^FL0? zHBg@)Fnj#(wps8Y7>hSll~@3h&DGu~(WTeM-CtDX_YGvo?mwbq!U?6r&2dEW<+%7) zjc=lu|BSj$)~oIB@=qqb|Ay{q0>K#Qw!AQj-lv0jv|4)f@d$c`i~}H0vf-_m3z63o zv@#x@aY73w|8Wdokvk4eczIV_xHtHR%J+CRX4~0Vgi1($MMq2Euv^B>dRN56{xdm& zK8b*I$>K|v+|WT#6{Y7%>+&oB7|j#_0smNTV0w0+PNKfKBGeSGi83!8{Fb1!ZMNIg z7~ll6i?4`F@-gLT*X_Z6NQeExz8D~xfIF6Z11Jv_?=Pe?K)WcQZb0)Ry>LKNyZjyF z${&V};q3&d;CAqZTvXpMf9o%MKHu9sRDDoU?V{+_73Om|jC0$8k|5D>6GYG{mXMK2 z&==}b<(w#SaNX>&X|ya;R-pZ#O-N1_rck;TE>pm3^dXez-dsafl|wqk(J=2fM3k|q zDYYm6v*oMY9reS9B>+g<_s7zXL2}sQDl{=EvcEK-^@Mnmb=irZ-v-|*Z@vBPCSy5Q z^6S~Tp7(ky(r&w$ZJh6|#H|25HrRNiA&^h`7Ae69?*M=xGJz^+AW>&`JVOkqRny+< z^aq!(7|N59|EB8(Ag)X?2!&#RYLCv7i`8eYMD1H%9GnRn!RBhUF_Pz%{TBdio(9g$ z!8I-mX<6O}2+1p=>F&j+hQ0DMFfYmaU5rzRScUXxJx>7XD(u|EQ(#&FoE>1u50 zU?Y&=J8DMX8uv>)zVr5X)A=$yEt_6L;th{0K)+k~My_6=ivkHjMw4VaapNvO z33MEsZt9K1gMsMoYXBvgsxoY^cE2?ZR*~o2_4uyNyO<@$2N-a}>bc+F+-2VSWKfOd1mljP=!@t%<@dT-X(*A+}KB+YJs+z>CBH*YV) zGotPFQBnoGc`B5lO*oc9r%Tbc;%g8CBt%U`*3ADn+srl;>wDYXdP4(-C|^EVl18sI z{nS)E>bQ}^=BV2!0_(xyVKicH#c(q!qM7>p@0KR&>YsqlkW8?TFy6a+SAnx1O`>vP z?V{tlb8|HQl9k>8VwpB>vb7sFF{oa&Jlx!V zgfuj$;;4L4MbT0=VIch^5F|xxjbm9dLW9Bz}YD@_owic6*fX`YJ8G!!UlYSZ}#vkmZJqB2LG~mY|@ZP^efSW53=38WIwc za1#gs0qRY4)+#EP0IcZ?^uR2VOq9_KkQ?M9A@RLESz<8RI^q=XPIj}QBPS2w=HV&w zeDhEc6^a_bF$7Qm_QdKam+O52p!I0(2%#|TZ9DMi!#}masYmqL0Jl8pY(<1ZCN--3 z-2QA;ItbfoGq6c_sWJyLkQ(3nZ?wylCjyoWH9nTO(ZeO*!9I=+9?_+`6J(RKyS2n=fYb69_jo#C{{sZ;Gqe|gc0eZ} zU?3n_t1?W%y;68UMRG3JYK}lfi=||~Bm}sd+sRDKEB);+8-}45e+LB&rJZ7Ngiz!z0G)EIF8|@M-;UWky4bFiwo4t^A*?_ zm%#1bcm~6Ego_9OsW#f}O@?3;`wgHFfck|WVQcDwRNC%Pf6;FD00(8)93CFtL#il` zvejeU*jK$Uz9toT`7g8JClTf+o2}-qi6#-NVA-Wd)#B#|iJgWT8XL>c5(*qhBZNo( zm@6*sEBXR2LGMGR;6}nqeTXfc%Ha*YM8=YmjsG~tt~nQ>;cp@@!ramlwxs5j=;6K(k0;=9BgdDA~K4C zi6X@dLB@r^ChQC!q2={;Lh>D2A|tm|Iy$QxE@NL0UT=mYFEdC2Ikr{5fr zPoF=3CbeQ0gGxsQ3tbn z`y<^{tvBd_fkCd~Z~~owoIIwR52Aw@Hkadv;?Sq7RTIJ92Q&&j{}QI4jyvt88WR$5 zxS7P#3I&}wtHT*dNu~J&+06~@zsdj>3?S5uUy=$i6Ot4v)LX65qhie@a3C#_%+yCI z0MJnH9_^YqB+=u;Llqu#6xq1Bd@wOR3?c;lW+##ftwvo~0_}tHaY2Q%+MdO2@kv&v+_mp7|$j5zr$!+2-sQF=~UvR%L)(6I1_2&?w@w``F}CB?@j6JO~aC%nO*>>g-^ z@sTG4H+?lx2ulT%S^7B;FxY420jY*9zIIE+a59Ijej{I0w!F*-V*Y(O8bDSc?w&<0 z%2V9bu4(J?w~=|ydvy4Uj}wGuI^hqG6_SgIgCk}Q0mjq;3jJGaL1BXjzMl}8RNU$A zv=tqFM&USsP&~?%gNsr??*lAsUUwkUG$d9SG7bs1JrRbFAHPplR+eDI`7PdOBbLOG zYP_8eC8l#|8wgULO?7SS7$xfHt2%WfE!tH`e5U z)58QSBMWE

_u6_$~{7SelbX3d;UH1b-k~C%Y_SjH@PM;*lvieP6Cj{*p8||3>{p zs9dNhkW9!Wu8xnNZi|b(5R=D+RcWKQN}U?zh2udO2Aq1R<57sYa#&cQEHFiE#KeN6 z(g1wz$;s6mWB^|Ub-^!)u=P+v=_>(F3J1oPJ^&OuXwB49P~W>$xUAPf$TdQJkv9VZ z>M#dU96FF7K3w+l$Q=k0`(KCK1)QI|5j6^Gec1F9Lf_=?wb;zmw2tukj0Ob((-6Qx z`E5ACdlMV73yRnM@j9VNT=*57mcOI0peUm;|8G_WcvSY0@Y+%zN=1-V72xe7UPoUJ zd^WlV2S28{=$C4b4kKK8(V?K`IC#ZDGJ|+6_=rn)e9`>@1G5*6I8zNXcY!j`nPiWQ z^%P)j{efzMlhD1Vh|)r`aX3HRRk|@cL89DEO&Z@g)VC^;fn><+&_b*JQh{np^rN)9 zboyV#TP72U*GD|#+m`S~(OON%D8Jsy!cD*7SV%o(d&C1J`?hR5j6BfGqA)M&uS*K2 z&mr)m#xx`fM>u1c1#XMOiRI5XmnIqnM{WP5c+!yGRhzPkMe%cgMUg-@_5%b6Za5(J zqYD3$Ig|HfjwPdb?w=kn6BN)4T7XQjO8(2;@`Hh7ZrZI(o#x*!pn&!ewV?d-wEs)y z$lsH>t47n(f63e@5FnX@0@14eUow~cp3IHz{jU9&%>Dm2@&E0U7*kdr?#%jK>>~Yl zlHVJzd;nV?`6|K6&K`N+{y&rgn9vGP?`-10A&FVw%?m)aQrNH%5XTbow?ObBKF1R4 zSXrsHRdCS#g85H4_a7c%?FiBF>#)$?F_k6Ye;?LGdzYVf=0^Wp2I$_^B$>G`j$!{F zd?dcC&|rGy1`pQG7|;2Xwl1~4L{eU91YQX}9(U%aH_LlBi@(4W3s|_15*{wz;N)y@ z$R!*lJ5$=_XFEVl7zJo;26~r#m!I7SCI_QKF5wH4Bvjsk*Z)R1fef?2Q^eWWC?_NM zDNVI5T<>*3z$8%bt-bZit2}$?_1^nh8zxs?P;Va=28MDf`ZsZ%@+95i@5242r*9}C zy|q^GqQIA=Rf9LOToad{>%yJRd;7SEMCBCVVC-PcKuVS>!cu~gWkPstL6oV< z`=`|7Yh;LFTIMyt!c%UoGdW&qXFr;!eZsPqKZP8U;->G9Ni?Te3)-DIZRDb=F!oce z<+Qo5de*6T)Or6$*72~KvEE;ir5kMYc~fLHw3CHD!uBt`n8{YC6V+fV#``&Asu@(a zxB2qqok?VekGo4ix2eg~wtV|lpi%%4=Y=W<*8UxAKE03_F}Pn&pEpCc5^YzbL=jUe zR<~4}x=9=hK6HEO&nN%$t{WU44|2Sus*qzRT$Nbh!ms?Wl-|W6>&a-!T-vtuCS&A( z?%MOr0Wb4-LT&9DS51rSJ)z~ZEQ2uZG1Wq0LFXYecaMwwpdiFB>3 zWPvx181#pgI|`-&YGWmxviLm5=RV$=K|na}T2XED^8$4QeH`N0B3u6D($#9>h`YdA1NuLpJ6e$QAyrjyADgDSG|InWn zgeBNkD^CZSmq*CtC|!y7Zqk@w$Q5Z^KjqBLao;OJ^;%PD5nO349N26~P#W6p0rRk6 zyA`0xM9%*HkHeWFG*;6YaX&g9o-{UdRoQ;vMH#W=(nUZSg^qpWt5fQO=(t!?`dhazt(B_SwYft$O?KWF>Ha1yNMx*zl_Rhys zoI-T3b^3P}U3c9`zJZZNC)MfSlkj;_ASmYU0`>6;op zu5VBn!5e7nMV~4hP|RvroygO20_55K1!--8ND?52fcU0jp?%7Ze-0)~IN1 z%Vxqw6O;x8{!^sqX1;20tv3$FDxY-tA3Az1Ujq8Ib5|?F%9?^vD-SVM(7Lt;e>!Qt z#Hg<+IvIlOt&boNYcoDFr{e3Gd8mm-Lm^t|%*RG1fj9TXRBvC`8mPFqxCW(p`1o{s zKvmE@NIJEG>-5(|oW+x$+?{I803fAa0417xH?(19`+c|{ zwDdLXplZwQEuv;*Ub!v*8xT;=Qz+zUvO_8-meX7*^_|f%42_~S0%0z3XCjZ6mxXnQ za}bA_DK1zH33z(5O)G=et8RY$2?A@X%a*p1!Z=dYE)W29^A+-|Xf6JXQB}zovyWPC~jFyXRel^ zfG71vZu6@;7lA~STO5G4a_6l7e#Lp4u(!7WgyT3L zUx<&7kLr2K@J(fPbz&2N>oF{fElESX;H>Za4(0a#gf;7Otj_&3?e^96jD&gMpcRQ31SsdiZ}oIH;wx$os?TVZT#{A@QZXWPJ3ePdOQ@lZ+ZYU)8;fAdOV zZlg+v&H9el-^NN0!P8UBk|uj~KNffSHn zPme3Xfz7cj*HQjFUH-Ctcu3s^B_P(Ez@S4mw+YX{Eel|Q(XaeTx+vu2Ajmb=djiR7V2Qn26jbxuPG{UD5pA=eWLMJ4iYeMOy za~(k~4F{3 zO`OyWK2t$A4##fbhT~{{;(aH;h%UFLt|Bfl`{px|3Q35q1HX~Ycj2FKOsd@ZVb|*8>l_PupSDia!EZjo?{6&vyb}LQf zM_Z5QgdSoN!w`-nLAt=-mssqYLETAAIYqJ=+AP+(o^^E*14Y=9)ufV*)uE99i zPtyw)nmwmR(+M6gkFn+D^$lV+&j96$n(52`F$f0mc55^#0wTNxTOL87fDgIRBpKOg z6f!}OftOLM?ss?qs#>Zlvk=fBK+ALsU0n3@jF*U@o#k5_CNd+;S#^V}6*R5!4et+I9LV7U)a@Oe&|8U)ll~Fx>A8BsmnCbbf z-xBBFLC*a{?J#a?r@ly%`GQ~uvYtuvo``6)cXl~f0{Q05GsVE$4By?$3P~fTn z0?s4TTFN#@pO%f(e0%#4w7FZr>kfW@ZDv zA(noW9)lro$}Yh)cB?E^Pvj6lZ~xVKx?nsChjYwz!yk(@3fc$MJ1ESYLEozoT0f`| zO)r9pFE$G4#T(>T2TqWHGTQRskAggg1ZYeM%}#_6;MAw~!zB1LD<`hE+J4BO1BXdSFy+Z*(sQL8=rO zwA(rYBkWC9sZ*iA5g#7m_7%Opw-dSo^aD~zrDQi209 zS91EO1DH_VG+V(5R0xc_mI8J*_EvKPsJRB3U3tZ&_V6gVMMaqK zD42#uM&yx;c+?INjg5LOQ{HqZ60ziiHj)!^5-JVCDQp&9-k;y%(_oTpY-|z;Gu)px zA6#2hJ+S+mPwGI4Rk4M7i^;4dyO_I@NNIsKN-jw&>CW7*4c=PS;T+=!F(LGC%$pEI za34M(*VFc~PmZPYxDKnHj?4V+i}3MrL$Pnk_i=pQLH`&Ni5`!0dBB+#33}ZZq!N=(J!m#^U%!P9{*EVxjIcD>_rf;+{i$b`!LEPD zzeBd;Yx#6i^te(ASL#i6i2g~i?G7PF^0_I{?ff^42G$r|_n`6piG6VWu;e~VpAfjE z$!Pgn+;R_Nz18ye&SivYp#0BEY8G6^?ycMkb@;1GL&rSJEqs(8MP~W~Fu!Ly8Npp^ z(CRCeH-?KFeB2w55k9dXdkZB55_kmlPXGfMh3G&> zk5dH*y+U=!v z=i{JnGMbZgc!RY&!3m$YT4n+N_`#UXj7`a z>$5eTTOv584lfT}8`p|^f`-Zj1j(?EDFzW6I7l=(V(nd)^8w75JOJT?zl2NIDFqy8 zw+y+(xhEI>5C_kpxj;=qs>x56;AQ?X2DXkRMFEBclXhXI&`m z?gcI4W!`P55}KpwL4}6K#=*#XnpIlOFo;TUTObJz+_#Id{ggP>6Al;xY0NuZHUxs$ z)+C7V@Ub|IJ=iEU1fI`Dsu?in0#S0p!hSGUL-90UhSLR;M3D6{Zqy0SlvY=VhQ)VjHCrsH1h4x0hr(zCfr42fH{zgn z6+c+(5DHiPVdPa)!LGo=s*kL*Tp>d zd8ZTREi;ipM>G(L;-cG|AW~wLUJoV|x4aanz0-~5If9yGSyU?{M1G=ua8v{mQ^Lb z#Xi$m#iTq9J95%|SKWwyb-+Db@->lwk&!%P-h|OE=9``8LtE6ZVd7L`{feaUA`TlZ zYxtio?hyWw`$8h-i?34k&3rErP{hJVJof^Zgt;LkBDfT4A#_QC0NeC#X-FCfv^@`Z zbE4~$LiWCrXB*;g%qPIcN#n2q6T7A7PuPUGyTP#v+p=SXQX@U-5D25nwv?*T{o~)X zZY1C{8_bsCSC^$t`W&F^9SZ)I!q14pktv;|3vSVALppqKEP;Pqow_Hd$#5v36NS|u zRe{tlt+fzXmldAhS_p0-7=^0sFYr-3-|bmcqhl^rv2JZ36h<$o$idU5j2y_h4ii9L zJx{}f^UB*_BGR2N$IZ7{5?!?|>f!Fb(~$k{JiO0GoG+JPPKjA;=(-cZOjU_Qh?BsH zxWq2PgU1eNXY>eh_PA&}q|)9*@Rr85>plKz2wA%?NaWAA9N2Yg9Ia3E$_mS_X;x1< zRMntHM{FFi(U+X@c7;=7^LdkFxzlK=X7Di}45xu=9Jl^eR3mI(^^D<+Thg_}E`DUh z;5M58nI%l*lRQhmk1x(JpI}hA19iFkoO{Js=-LmBMK&SvQ6b37Oq~F#y6Br!C!U(ytR<0-I2s}q}*dh z#Yea0YOEQR)|WrA3Ja0lZiLs*W|Cg3DFHR+kEx==uz%(T-r{PjYdj~0inVi>B6Y0g zf3RzrJXk|Lk?O3>_2waKm3C=^r&;C;vP_Hbr?*)>P7a6okq}b5$0|c*Ze^l*aFl>q zIJ1Rc1;pkz_k^Yw);b>}{X6CyQ3*KVc_3i0D$SY82s&? z^-d!iT{+Z8l2A}E7|`pF{?Hgqp=H57%hGW&fWo= zBUr+G;z)gpvCHMn259{!(>${|g<2>=OzM)(&uVcF_YmVKsZ;`zH(!GK{P&=4*U2_g zTb!#vlb&enlK%sMzpmFygP?5~814*g)9*N=nE8&eS`?}O#IB|w`r^_UqtlPGP#ao+ z_)16!cdCP_K#Ni!@prUwATYXMNdX8r=M$QPOEBMjrNU)i<(3c<=1rZB(b3NIRmfVX zeFra57<2#66$@7yL>Es;6Eyn$@?BXy8LcaFDRE^q@}<(q>UDo%i!DuGv%RS!n9D0{s88CXXTBUOSPq8Gb zM#R)FxxOz>TDzvA@mg6$E~pKoO)Cz{q!;`lZ@= zbtSgf!x@Hp7p%{%*(p!dzZNyfMg09zIcNyNTqpJ$Q#k6TqHOiS-T<-?k)cY{!BB?R zj35dK^d~0oXTU%}7F#1^07>qtkFSo){zOfUzoWWaUt8i}dbz`o{<>EukX zThrrA{+;izo7c>yo*ug2I{iH=05ejk1xEc_c^&?aK_&SYt66>nx-}fc2(eQF7@>NF zdL;xdChPdf2qhWtD%*8#+5q6Rx6}?cEFbCfyOJkYsg#iA>sI`_^?&DK z<^w3JPFV*ET64YWta^fJ_osB#%Tkl+zPvFX@%&uUX~6O*3ivhAh`Kq`0qX@{mnp+s zY`@TTRTc9=FvaVLWd6zeg}RvODtBDly&f@xB<5ED+CnH*s}}8X-ReVVIX$Fo0=%D2 z-%rKzN*=EE>|S?|W6~)E`HV(rh?|mFw*lQtCKBH>7K8Mz%sUKtRHLU>&1$AH{M-$c ze+C=%CDrP`!JD%Fav7|G{B*O_x_&yZfRl_ zbZivp!!JrA#;Z2O6ZTs-y6x@NwFTc8H*`h)wFE61kNp&Z3VJw;POHgJP*BjRbvhh? z)+n{xSrX?ztVkd)5_lv@d^Q>(RjtSo<>}UY!q)iB_2Y88=o*;+DL`~Mw4I0?TGv_6h~EKFvv{9Ztbp?XLK}I z3Ye8VH+oCEi`1%(SX{1Y;*y+#P*G98HU=|Yyk71M8+zN(q5|z0;nYZ7pKdMUM&YY< zHkBQc*(|>5_qC8?mIMY;)St&R#OS!5AZXXL&_F`4peB|#H*;yU$VNv-A|c=|WXzkn zt3rG<^WMZw4P})F6j~BM3yW{=9c^$wSVQ0Ezat-}uu$c~R`A1FlNrZ~``rp^4d761 zg27vEym#N|ayFgF1Z{Q+?O8e6*`ZUR%Jz$f9&GSnvP_PT7lFs5&2+xn9a`82uvvpt z0BREu=zIsq&;(Mx&}`@-!kYBmoQElJGi4%YgMc8`N(c)mnE^KW{=ts_?~6D4C-rMtyzVaVro9op zebTK04-b#+%}rwZ6+n$*6YLn5nmS^+gj+}b5}N2I^s%hWk~kX>6J%X%@vZA4YtYVw zkLPW+d^5!?S=8@WAgiEaVeu=K%U)Q(7Sve36w&GvanGx|Zuo!q1;#9mwO`&3uyEu` z985>>$l|tM%lZCZfA|aFl@CByGPj`quWN?vHkl3TIxYtyj= zPBvHdlpGCA0PXZECMKpQ`FT3ym2&R)pjC3f)dZ9qNvG-Qd^z9zmJmbBIk)HQQ2G7w z_WrgS?)kOIMbdARdMdH@_;>d1Yo;SCFiTxnJ|P!F0K$*iY<=-(1RRN599T#3^P0#` ze(TwjIsF3*RA9ddJplC>+C{G!!yGYe7S5MT6DmMLMFj}0-%x3=^mDLf{VD4KhNeN8 za(HpkpO|E6n1jAN#^+T6O%TaxBoEK z6E`<^&tZ>~@uJ@q6A?HBrhYN9FiG3aY~YoAThVY_ATwc=69vYpvDC2tH?xZJY}=8> z$I^crUUI!jO}5skS*vx5Q0`up!dycmg9ED5H&XyN`n);Y?`qTr0#z+BaP2-$+7J?> zKim~dzJmkW@_Q^bbwBRB)xVJI40iZh`aORKR?kp5jv^s`hv7_BI!<1@u}SvtvGH4s zU#F-DpJDXh7k|wZ1fn!X_9Xb4-X|tV6zRWB{~p)zK`~|z zFQ%6NcLg&QS407k!vDwhe-r)AwjQF$$(R1$ zH^G^s;QxabnsJYwH%St!n(RBh&Sx{!+wX|Fubb{lriX%dAErMhuO%?k+wu;(b;QSq z|B-G@Vj671H&0QED;e(K{XAcB^BS^baiF+Jg(bb>=;*ZD)zYt8UTbiwuUN+lpQ^8L z@S0#0z@0?0STmTKv-aQpYY+lAKd9`-p*wj7n29-mp5g-ZXwaYf$hg5Hn@_uE;ee}s zW=3i8=TYg3Go2y@6%#vqqT@*j&?3bu?ga1*NPGk&LIpuVj#Wr|cpTPI0LJ9>z@gK0 z(TB*4!qS$;uxGV$OXV+Lm+Ip4d<6rD zo=kvU6koCdI~4NC+yvFz=n`4(d#oztJmhdVWV!2&_=uYc$Y?7WMI|*#p2F-ZTb-{| z4oLpcjLelL+Fo5=z7rtm7`4e*HnP!|c?vH%@2jx_1CLltZ5DqH2_6*E z@Otq12L?`if!1Tx+kbVV4Ew5H!dMgyweW+!vR)Q*=;x&y*l^!sR0@KjBZ$r{9n2mzW6xfrQT znRXoNs7}BaikK1^V_RC9r>|@(4I(XaiXz!=p-^^K!%2G1J^l=m$~7 zeV_Vo>?^Fd87xqy^Af125lV_%Z8|pORZ`ny3ypD92jm|saR_n6&1FQe?ELE;?c4h}~bjPsmb&P(1|AfhK+ii-^Y)}#8ym(bQJoVph^*)N{A6&V1 zIT4_%$6@yj+61U45e}f)6)~$|3y}Zys{E#4wOme8DGNiTLEMBExW2hTksldvC#08$ z!5RYSPpmB8h3~F_7UHsVp|H4EM2<+#dc13j?#sAL$#h{MB?>D60fEh1*ClIqkX0%? zR=LSfbMum#0?9=BDh7)FDa%x(O3gZR|FF{R74bxRt^cRIbN`1bjpKO6XsFHIL5eZS ztwAH&lIvvTwrF@iu4qf3rAjHmP5yZ_YDL`7SAIw<53S-KH>Uhr} zW()hBM6-*r{lP+dLX>J+i87fUF|NSvU?jJ4m~)VZhI?|Rti%v+HaYT*hvyt4(}+`zz@-aRmo)4cv+%=%If zFE5ozs80`xf=aJllNGQU%c9BI@@v;A9>xfzo&izrMS3Psk^r2$mqD(^C&{H4eJT%Q;8( z-;Ttb$Km2Yo2OtEh$j!cjldlp{EiC4Ovg>kxsbuTJg&$N$OQ~CrbX~bSIHgy1N?vy&-8zdt zotJlbQ=_10a^bI8@)RL?i4zm2JGOU-**=m4Ot|5Mh$re*DwU&|Lt|s=SsNeiq!0AS zM7{Xp5(w{*`S_AoV>h0AT+*u_{`@c0=vGlg?g14qlrpsuC#2>^NaQHpw$&Jq2rxnX z0*X8&(%~)Paw9AqdE=Wl78c&0H{5dC4SkY^sT>7771zf0M4GgS)2mx1obSp@XwtPP zRMvXU$TH66H*sADA&lq!RNPaK@cy-Oa#^)S7cUybv{4>pBwHG0Q})(-3c8c-s7$k3 zZ-ZVF<>U?#%85^zWZT7l#t-^Bh|}AH^HD=b_ONMTER2$PO_cg*r^p3~!-g|ovK901 z%d(aDTF?MNilp07a3Y?ra^>qq-5E0W7UP6i=Pp%v%R4rc>%zpjs88uz&AS|F}#=sklb=LGfNtk$v!#;Q1MvhX$*suc)l z3o2_GEWZm9MnxnpdM2L=FevLw@5lfWgEgq+a`#C40f~sxkm5wYLXuz#uQe$Y_v)8n|MkRp3$sP?wN)Xd7w8 z-6j(R2<=!-0BLb0CHNo`@PP5Ws-rkC!hzw5#F!cGv0{)OmjTWT?3nr#D3wGQ8l%sp zW^>JgN$*zeay<%w0Bu@$EK?Zp%v61RYuhTgr~P0vosB+VDkD*?AAC39g<;C&Q%SYn z@Nhn$Vv#tB6!>#t_~pAg5_x1#<0VTW(y zL(>U~#IdRASI+y10(g>_wHp(VH2kyoDXj)xwcG~G1h?kd6*#gbNYkw#^d`D?<#1@- vOD2|CyvzN^|IWAoRu+f)p=9- Date: Fri, 27 Mar 2026 15:50:53 +0000 Subject: [PATCH 13/57] PHP & JS CS Fixes --- .../notifications/assets/scss/notifications.scss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/code_samples/user_management/notifications/assets/scss/notifications.scss b/code_samples/user_management/notifications/assets/scss/notifications.scss index 6c880f226f..db5e780aac 100644 --- a/code_samples/user_management/notifications/assets/scss/notifications.scss +++ b/code_samples/user_management/notifications/assets/scss/notifications.scss @@ -1,7 +1,7 @@ @use '../../vendor/ibexa/admin-ui/src/bundle/Resources/public/scss/_alerts.scss' as *; .ibexa-alert { - &--notification { - @extend .ibexa-alert--info; - } + &--notification { + @extend .ibexa-alert--info; + } } From a4818cd17abeec6f211e8f51f739dab1f6a8e8ba Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Mon, 30 Mar 2026 10:40:36 +0200 Subject: [PATCH 14/57] About `SymfonyNotificationAdapter` and `SymfonyRecipientAdapter` --- docs/users/notification_channels.md | 40 ++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/docs/users/notification_channels.md b/docs/users/notification_channels.md index 3b68ea388f..d47f724d15 100644 --- a/docs/users/notification_channels.md +++ b/docs/users/notification_channels.md @@ -93,7 +93,7 @@ and optionally implements some interfaces depending on the channels it could be | `push` | `PushNotificationInterface` | | | `sms` | `SmsNotificationInterface` | ✔ | -The [`SystemNotificationInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-SystemNotification-SystemNotificationInterface.html) is not part of Symfony and has its own namespace. +Notice tha the [`SystemNotificationInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-SystemNotification-SystemNotificationInterface.html) is not part of Symfony and has its own namespace. The `ibexa` channel send notifications to users through their profile menu, exactly as [user notification](notifications.md#create-custom-notifications). The [`SystemNotificationChannel` uses the core `NotificationService`](https://github.com/ibexa/notifications/blob/v5.0.6/src/lib/SystemNotification/SystemNotificationChannel.php#L51) to do so. @@ -101,9 +101,41 @@ The [`SystemNotificationChannel` uses the core `NotificationService`](https://gi Some channels don't need a recipient: - `browser`: Always send a flash message to the current user -- `chat`: Always send message to same ressource - -TODO: About `SymfonyNotificationAdapter` and `SymfonyRecipientAdapter` +- `chat`: Always send message to the same connection resource + +### Notification sending + +In the [`Ibexa\Contracts\Notifications`](/api/php_api/php_api_reference/namespaces/ibexa-contracts-notifications.html) namespace can be found everything needed to work with notifications. + +The [`…\Service\NotificationServiceInterface::send()`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-Service-NotificationServiceInterface.html#method_send) expects two arguments: + +- The first argument is an [`…\Value\NotificationInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-Value-NotificationInterface.html). + This interface is implemented by the [`…\Value\Notification\SymfonyNotificationAdapter`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-Value-Notification-SymfonyNotificationAdapter.html) + which allows to wrap any class extending `Symfony\Component\Notifier\Notification\Notification`. +- The optional second argument is an array of [`…\Value\RecipientInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-Value-RecipientInterface.html). + This interface is implemented by the [`…\Value\Recipent\SymfonyRecipientAdapter`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-Value-Recipent-SymfonyRecipientAdapter.html) + made to wrap `Symfony\Component\Notifier\Recipient\RecipientInterface`. + - This Symfony interface is implemented by [`…\Value\Recipent\UserRecipient`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-Value-Recipent-UserRecipient.html) + which can wrap classes implementing the [`Ibexa\Contracts\Core\Repository\Values\User\UserReference` interaface](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-User-UserReference.html) + - The [`UserService` methods to load a user](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-UserService.html#method_loadUser) are returning objects implementing this `UserReference` interface. + - The [`PermissionResolver::getCurrentUserReference()` method](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-PermissionResolver.html#method_getCurrentUserReference) is returning objects implementing this `UserReference` interface. + +So, for example, to send a notification, you often as this kind of combo: + +```php hl_lines="8-11" +use App\Notifications\MyNotification; // Extends Symfony\Component\Notifier\Notification\Notification +use Ibexa\Contracts\Core\Repository\PermissionResolver; +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; +//… +$this->notificationService->send( + new SymfonyNotificationAdapter(new MyNotification($subject)), + [new SymfonyRecipientAdapter(new UserRecipient($this->permissionResolver->getCurrentUserReference()))], +); +//… +``` ### `CommandExecuted` example From 69659961e1d3cf2bca75ce58f4c0d5babb20322c Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Mon, 30 Mar 2026 11:14:06 +0200 Subject: [PATCH 15/57] Extends ControllerFeedback example to storefront --- .../config/packages/custom_notifications.yaml | 7 +++- .../notification-sender-controller.html.twig | 1 + ...ser.png => notification-browser-admin.png} | Bin .../img/notification-browser-storefront.png | Bin 0 -> 14584 bytes docs/users/notification_channels.md | 30 ++++++++++++++---- 5 files changed, 31 insertions(+), 7 deletions(-) create mode 100644 code_samples/user_management/notifications/templates/themes/storefront/notification-sender-controller.html.twig rename docs/users/img/{notification-browser.png => notification-browser-admin.png} (100%) create mode 100644 docs/users/img/notification-browser-storefront.png diff --git a/code_samples/user_management/notifications/config/packages/custom_notifications.yaml b/code_samples/user_management/notifications/config/packages/custom_notifications.yaml index f8e665bde6..e74e0c56b0 100644 --- a/code_samples/user_management/notifications/config/packages/custom_notifications.yaml +++ b/code_samples/user_management/notifications/config/packages/custom_notifications.yaml @@ -27,4 +27,9 @@ ibexa: App\Notifications\ControllerFeedback: channels: - browser - - log + storefront_group: + notifier: + subscriptions: + App\Notifications\ControllerFeedback: + channels: + - browser diff --git a/code_samples/user_management/notifications/templates/themes/storefront/notification-sender-controller.html.twig b/code_samples/user_management/notifications/templates/themes/storefront/notification-sender-controller.html.twig new file mode 100644 index 0000000000..89078dd236 --- /dev/null +++ b/code_samples/user_management/notifications/templates/themes/storefront/notification-sender-controller.html.twig @@ -0,0 +1 @@ +{% extends '@ibexadesign/storefront/layout.html.twig' %} diff --git a/docs/users/img/notification-browser.png b/docs/users/img/notification-browser-admin.png similarity index 100% rename from docs/users/img/notification-browser.png rename to docs/users/img/notification-browser-admin.png diff --git a/docs/users/img/notification-browser-storefront.png b/docs/users/img/notification-browser-storefront.png new file mode 100644 index 0000000000000000000000000000000000000000..83ac41a37ccd6d8014fa719e270b21713190dfb2 GIT binary patch literal 14584 zcmbumbx>SS&@PN@a9JQ|a3^T+0Kpvs1a}D#g1fsDJXnIe1=rvn++`Pc0xa$hcOk#` z{r>u@Zq=m0@6DGobe~NC?osW(i5J zp%0AB#l(~>#iYb+t!y1s?2L>}#myb#Ki31 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 bKfxz1G9##eoBi Date: Mon, 30 Mar 2026 11:19:10 +0200 Subject: [PATCH 16/57] Apply Rector suggestions --- .../notifications/src/Command/NotificationSenderCommand.php | 2 +- .../notifications/src/Notifier/Channel/LogChannel.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/code_samples/user_management/notifications/src/Command/NotificationSenderCommand.php b/code_samples/user_management/notifications/src/Command/NotificationSenderCommand.php index 2788cbd8a6..ddd75f2e24 100644 --- a/code_samples/user_management/notifications/src/Command/NotificationSenderCommand.php +++ b/code_samples/user_management/notifications/src/Command/NotificationSenderCommand.php @@ -35,7 +35,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int try { // Do something - if (rand(0, 1) == 1) { + if (random_int(0, 1) == 1) { throw new \RuntimeException('Something went wrong'); } $exitCode = Command::SUCCESS; diff --git a/code_samples/user_management/notifications/src/Notifier/Channel/LogChannel.php b/code_samples/user_management/notifications/src/Notifier/Channel/LogChannel.php index 80bc374566..39abce608e 100644 --- a/code_samples/user_management/notifications/src/Notifier/Channel/LogChannel.php +++ b/code_samples/user_management/notifications/src/Notifier/Channel/LogChannel.php @@ -16,7 +16,7 @@ public function notify(Notification $notification, RecipientInterface $recipient { if (isset($this->logger)) { $this->logger->info($notification->getSubject(), [ - 'class' => get_class($notification), + 'class' => $notification::class, 'importance' => $notification->getImportance(), 'content' => $notification->getContent(), ]); From 9c186a786f029e96ad0164f9282573090b2173a3 Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Mon, 30 Mar 2026 16:12:59 +0200 Subject: [PATCH 17/57] About subscriptions scopes overrides --- .../config/packages/custom_notifications.yaml | 35 ---------- .../config/packages/notifications.yaml | 64 +++++++++++++++++++ docs/users/notification_channels.md | 31 ++++++--- 3 files changed, 85 insertions(+), 45 deletions(-) delete mode 100644 code_samples/user_management/notifications/config/packages/custom_notifications.yaml create mode 100644 code_samples/user_management/notifications/config/packages/notifications.yaml diff --git a/code_samples/user_management/notifications/config/packages/custom_notifications.yaml b/code_samples/user_management/notifications/config/packages/custom_notifications.yaml deleted file mode 100644 index e74e0c56b0..0000000000 --- a/code_samples/user_management/notifications/config/packages/custom_notifications.yaml +++ /dev/null @@ -1,35 +0,0 @@ -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 - App\Notifications\CommandExecuted: - channels: - - ibexa - - email - - log - admin_group: - notifier: - subscriptions: - App\Notifications\ControllerFeedback: - channels: - - browser - storefront_group: - notifier: - subscriptions: - App\Notifications\ControllerFeedback: - channels: - - browser diff --git a/code_samples/user_management/notifications/config/packages/notifications.yaml b/code_samples/user_management/notifications/config/packages/notifications.yaml new file mode 100644 index 0000000000..0a05fab7b3 --- /dev/null +++ b/code_samples/user_management/notifications/config/packages/notifications.yaml @@ -0,0 +1,64 @@ +framework: + notifier: + chatter_transports: + slack: '%env(SLACK_DSN)%' +ibexa: + system: + default: + notifier: + subscriptions: + # Default subscriptions from ibexa.yaml are properly added. + # 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 + - log + admin_group: + notifier: + subscriptions: + # Default subscriptions from ibexa_admin_ui.yaml are properly added + # Custom subscriptions: + App\Notifications\ControllerFeedback: + channels: + - browser + storefront_group: + notifier: + subscriptions: + # Default subscriptions from ibexa.yaml: + 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 + - log + App\Notifications\ControllerFeedback: + channels: + - browser diff --git a/docs/users/notification_channels.md b/docs/users/notification_channels.md index 19856474cf..f756224424 100644 --- a/docs/users/notification_channels.md +++ b/docs/users/notification_channels.md @@ -50,7 +50,18 @@ php bin/console debug:container --tag=notifier.channel * [`push`]([[= symfony_doc =]]/notifier.html#push-channel) - Notification forwarded to specific applications * [`sms`]([[= symfony_doc =]]/notifier.html#sms-channel) - Notification forwarded to phone numbers -Some default subscriptions can be found in `config/packages/ibexa.yaml`. +Some default subscriptions can be found in `config/packages/ibexa.yaml` and `config/packages/ibexa_admin_ui.yaml`. + +!!! caution "Scopes may not merge as expected" + + Subscriptions defined for a scope may not merge with subscriptions from others 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 additions: + + ```bash + php bin/console ibexa:debug:config notifications.subscriptions --siteaccess= + ``` ### Subscription example @@ -69,10 +80,10 @@ SLACK_DSN=slack://xoxb-token@default?channel=ibexa-notifications ``` Subscribe to notification types related to Commerce like order, payment, and shipment status changes. -For example in a new `config/packages/custom_notifications.yaml` file: +For example, in a new `config/packages/notifications.yaml` file: ``` yaml -[[= include_file('code_samples/user_management/notifications/config/packages/custom_notifications.yaml', 0, 18) =]] +[[= include_file('code_samples/user_management/notifications/config/packages/notifications.yaml', 0, 20) =]] ``` ## Create a notification class @@ -150,11 +161,11 @@ As constructor arguments, an instance takes the command itself, the exit code of [[= include_file('code_samples/user_management/notifications/src/Notifications/CommandExecuted.php') =]] ``` -The channels subscribing to this notification are set in `config/packages/custom_notifications.yaml`: +The channels subscribing to this notification are set in `config/packages/notifications.yaml`: ``` yaml -[[= include_file('code_samples/user_management/notifications/config/packages/custom_notifications.yaml', 4, 9) =]] # … -[[= include_file('code_samples/user_management/notifications/config/packages/custom_notifications.yaml', 18, 22) =]] +[[= include_file('code_samples/user_management/notifications/config/packages/notifications.yaml', 4, 9) =]] # … +[[= include_file('code_samples/user_management/notifications/config/packages/notifications.yaml', 20, 24) =]] ``` The example command sends a `CommandExecuted` notification at the end of what could be a regular execution. @@ -216,14 +227,14 @@ This `assets/scss/notifications.scss` is added to the Admin UI layout in `webpac 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. -Subscribe to this new notification type in `config/packages/custom_notifications.yaml`: +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 ``` yaml -[[= include_file('code_samples/user_management/notifications/config/packages/custom_notifications.yaml', 4, 6) =]] # … -[[= include_file('code_samples/user_management/notifications/config/packages/custom_notifications.yaml', 23) =]] +[[= include_file('code_samples/user_management/notifications/config/packages/notifications.yaml', 4, 6) =]] # … +[[= include_file('code_samples/user_management/notifications/config/packages/notifications.yaml', 25, 60) =]][[= include_file('code_samples/user_management/notifications/config/packages/notifications.yaml', 61) =]] ``` Reaching this controller in the back office (at `/admin/notification-sender`) triggers the notification as a flash message in the bottom-right corner: @@ -252,7 +263,7 @@ The following example is a custom channel that sends notifications to the logger Now, [`CommandExecuted` notification](#commandexecuted-example) can be subscribed to with the `log` channel: ``` yaml -[[= include_file('code_samples/user_management/notifications/config/packages/custom_notifications.yaml', 18, 23) =]] +[[= include_file('code_samples/user_management/notifications/config/packages/notifications.yaml', 20, 25) =]] ``` The log file contains the notifications: From fad8c6a3af84e9a92d92ee24a4cd5c80587a8ba3 Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Tue, 31 Mar 2026 01:18:16 +0200 Subject: [PATCH 18/57] About subscriptions scopes overrides --- .../config/packages/notifications.yaml | 9 +++++++-- docs/users/notification_channels.md | 13 ++++++------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/code_samples/user_management/notifications/config/packages/notifications.yaml b/code_samples/user_management/notifications/config/packages/notifications.yaml index 0a05fab7b3..a67980b726 100644 --- a/code_samples/user_management/notifications/config/packages/notifications.yaml +++ b/code_samples/user_management/notifications/config/packages/notifications.yaml @@ -7,7 +7,7 @@ ibexa: default: notifier: subscriptions: - # Default subscriptions from ibexa.yaml are properly added. + # Default subscriptions from ibexa.yaml are properly merged. # Custom subscriptions: Ibexa\OrderManagement\Notification\OrderStatusChange: channels: @@ -26,8 +26,13 @@ ibexa: admin_group: notifier: subscriptions: - # Default subscriptions from ibexa_admin_ui.yaml are properly added + # Default subscriptions from ibexa_admin_ui.yaml are properly merged # Custom subscriptions: + App\Notifications\CommandExecuted: + channels: + - ibexa + - email + - log App\Notifications\ControllerFeedback: channels: - browser diff --git a/docs/users/notification_channels.md b/docs/users/notification_channels.md index f756224424..f07d108ad1 100644 --- a/docs/users/notification_channels.md +++ b/docs/users/notification_channels.md @@ -82,7 +82,7 @@ SLACK_DSN=slack://xoxb-token@default?channel=ibexa-notifications Subscribe to notification types related to Commerce like order, payment, and shipment status changes. For example, in a new `config/packages/notifications.yaml` file: -``` yaml +``` yaml hl_lines="12-20" [[= include_file('code_samples/user_management/notifications/config/packages/notifications.yaml', 0, 20) =]] ``` @@ -163,9 +163,8 @@ As constructor arguments, an instance takes the command itself, the exit code of The channels subscribing to this notification are set in `config/packages/notifications.yaml`: -``` yaml -[[= include_file('code_samples/user_management/notifications/config/packages/notifications.yaml', 4, 9) =]] # … -[[= include_file('code_samples/user_management/notifications/config/packages/notifications.yaml', 20, 24) =]] +``` yaml hl_lines="17-20" +[[= include_file('code_samples/user_management/notifications/config/packages/notifications.yaml', 4, 24) =]] ``` The example command sends a `CommandExecuted` notification at the end of what could be a regular execution. @@ -232,9 +231,9 @@ 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 -``` yaml +``` yaml hl_lines="13-15 43-45" [[= include_file('code_samples/user_management/notifications/config/packages/notifications.yaml', 4, 6) =]] # … -[[= include_file('code_samples/user_management/notifications/config/packages/notifications.yaml', 25, 60) =]][[= include_file('code_samples/user_management/notifications/config/packages/notifications.yaml', 61) =]] +[[= include_file('code_samples/user_management/notifications/config/packages/notifications.yaml', 25, 34) =]][[= include_file('code_samples/user_management/notifications/config/packages/notifications.yaml', 35, 65) =]][[= include_file('code_samples/user_management/notifications/config/packages/notifications.yaml', 66) =]] ``` Reaching this controller in the back office (at `/admin/notification-sender`) triggers the notification as a flash message in the bottom-right corner: @@ -262,7 +261,7 @@ The following example is a custom channel that sends notifications to the logger Now, [`CommandExecuted` notification](#commandexecuted-example) can be subscribed to with the `log` channel: -``` yaml +``` yaml hl_lines="5" [[= include_file('code_samples/user_management/notifications/config/packages/notifications.yaml', 20, 25) =]] ``` From 91d8af7adea55b6afb73b6b4892dc743f4586697 Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Tue, 31 Mar 2026 09:36:41 +0200 Subject: [PATCH 19/57] About subscriptions scopes overrides --- docs/users/notification_channels.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/users/notification_channels.md b/docs/users/notification_channels.md index f07d108ad1..d39ae61c11 100644 --- a/docs/users/notification_channels.md +++ b/docs/users/notification_channels.md @@ -236,6 +236,17 @@ Subscribe to this new notification type in `config/packages/notifications.yaml`: [[= include_file('code_samples/user_management/notifications/config/packages/notifications.yaml', 25, 34) =]][[= include_file('code_samples/user_management/notifications/config/packages/notifications.yaml', 35, 65) =]][[= include_file('code_samples/user_management/notifications/config/packages/notifications.yaml', 66) =]] ``` +Notice that when introducing subscriptions configuration for `storefront_group` scope, +several subscriptions had to be copy-pasted into this SiteAccess group to have the same subscriptions as before, +when it was configured by the `default` scope. + +For example, the subscriptions for default `site` SiteAccess coming with Commerce edition +can be checked with the following command during the configuration: + +```bash +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: ![Browser back office notification example](notification-browser-admin.png "Controller message displayed as a flash message in the browser") From 251d463f2fc9d6b5e8b8ad6d29eda3f1a90b09e7 Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Tue, 31 Mar 2026 10:01:15 +0200 Subject: [PATCH 20/57] About subscriptions scopes overrides --- docs/users/notification_channels.md | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/docs/users/notification_channels.md b/docs/users/notification_channels.md index d39ae61c11..d324158017 100644 --- a/docs/users/notification_channels.md +++ b/docs/users/notification_channels.md @@ -104,7 +104,7 @@ and optionally implements some interfaces depending on the channels it could be | `push` | `PushNotificationInterface` | | | `sms` | `SmsNotificationInterface` | ✔ | -Notice tha the [`SystemNotificationInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-SystemNotification-SystemNotificationInterface.html) is not part of Symfony and has its own namespace. +Notice that the [`SystemNotificationInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-SystemNotification-SystemNotificationInterface.html) is not part of Symfony and has its own namespace. The `ibexa` channel send notifications to users through their profile menu, exactly as [user notification](notifications.md#create-custom-notifications). The [`SystemNotificationChannel` uses the core `NotificationService`](https://github.com/ibexa/notifications/blob/v5.0.6/src/lib/SystemNotification/SystemNotificationChannel.php#L51) to do so. @@ -236,16 +236,18 @@ Subscribe to this new notification type in `config/packages/notifications.yaml`: [[= include_file('code_samples/user_management/notifications/config/packages/notifications.yaml', 25, 34) =]][[= include_file('code_samples/user_management/notifications/config/packages/notifications.yaml', 35, 65) =]][[= include_file('code_samples/user_management/notifications/config/packages/notifications.yaml', 66) =]] ``` -Notice that when introducing subscriptions configuration for `storefront_group` scope, -several subscriptions had to be copy-pasted into this SiteAccess group to have the same subscriptions as before, -when it was configured by the `default` scope. +!!! note "Subscriptions for `storefront_group`" -For example, the subscriptions for default `site` SiteAccess coming with Commerce edition -can be checked with the following command during the configuration: - -```bash -php bin/console ibexa:debug:config notifications.subscriptions --siteaccess=site -``` + Notice that when introducing subscriptions configuration for `storefront_group` scope coming 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 `site` SiteAccess belonging this group + can be checked with the following command during the configuration: + + ```bash + 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: From 2a748a52bb7789610ed2eff3a1dc00276485d22f Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Tue, 31 Mar 2026 10:01:42 +0200 Subject: [PATCH 21/57] About subscriptions scopes overrides --- docs/users/notification_channels.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/users/notification_channels.md b/docs/users/notification_channels.md index d324158017..7f67697e5c 100644 --- a/docs/users/notification_channels.md +++ b/docs/users/notification_channels.md @@ -242,7 +242,7 @@ Subscribe to this new notification type in `config/packages/notifications.yaml`: 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 `site` SiteAccess belonging this group + For example, the subscriptions for the `site` SiteAccess belonging this group can be checked with the following command during the configuration: ```bash From 1161d9f02224863cddb9f602c89f1ec9379f7bac Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Tue, 31 Mar 2026 10:22:27 +0200 Subject: [PATCH 22/57] CommandExecuted: Apply SonarCloud suggestion sonarqubecloud / SonarCloud Code Analysis Extract this nested ternary operation into an independent statement. --- .../notifications/src/Notifications/CommandExecuted.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/code_samples/user_management/notifications/src/Notifications/CommandExecuted.php b/code_samples/user_management/notifications/src/Notifications/CommandExecuted.php index 489cce61bb..ac20a127e6 100644 --- a/code_samples/user_management/notifications/src/Notifications/CommandExecuted.php +++ b/code_samples/user_management/notifications/src/Notifications/CommandExecuted.php @@ -42,11 +42,16 @@ public function asEmailMessage(EmailRecipientInterface $recipient, ?string $tran public function asSystemNotification(UserRecipientInterface $recipient, ?string $transport = null): ?SystemMessage { + $errorCount = 'No error'; + if (count($this->exceptions) > 0) { + $plural = count($this->exceptions) > 1 ? 's' : ''; + $errorCount = count($this->exceptions) . ' error' . $plural; + } $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' : ''), + 'content' => $errorCount, ]); return $message; From 64edb655dde98548b6dfaf136184bfa0424ad58a Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Tue, 31 Mar 2026 10:29:29 +0200 Subject: [PATCH 23/57] notification_channels.md: Minor addition --- docs/users/notification_channels.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/users/notification_channels.md b/docs/users/notification_channels.md index 7f67697e5c..5b8c417c82 100644 --- a/docs/users/notification_channels.md +++ b/docs/users/notification_channels.md @@ -169,12 +169,13 @@ The channels subscribing to this notification are set in `config/packages/notifi The example command sends a `CommandExecuted` notification at the end of what could be a regular execution. It randomly succeeds or fails to show how the notification can be used to communicate different execution results. +It could be declared as a service to set the list of recipients' logins `$recipientLogins` from a configuration file. ``` php [[= include_file('code_samples/user_management/notifications/src/Command/NotificationSenderCommand.php') =]] ``` -If you execute this command, it will some time succeed, some time fails. +If you execute this command, it will sometimes succeed, sometimes fails. ![Ibexa notification example](notification-ibexa.png "Command notifications shown in the `ibexa` channel, the back office user notification menu") From f463636c5d04c630765fcffd202650c1cd6b0586 Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Tue, 31 Mar 2026 11:16:25 +0200 Subject: [PATCH 24/57] Apply suggestions from code review Co-authored-by: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> --- docs/users/notification_channels.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/docs/users/notification_channels.md b/docs/users/notification_channels.md index 5b8c417c82..5b11447547 100644 --- a/docs/users/notification_channels.md +++ b/docs/users/notification_channels.md @@ -151,7 +151,7 @@ $this->notificationService->send( ### `CommandExecuted` 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. +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. @@ -175,7 +175,7 @@ It could be declared as a service to set the list of recipients' logins `$recipi [[= include_file('code_samples/user_management/notifications/src/Command/NotificationSenderCommand.php') =]] ``` -If you execute this command, it will sometimes succeed, sometimes fails. +If you execute this command, it sometimes succeeds, sometimes fails. ![Ibexa notification example](notification-ibexa.png "Command notifications shown in the `ibexa` channel, the back office user notification menu") @@ -183,7 +183,7 @@ If you execute this command, it will sometimes succeed, sometimes fails. 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 just a class extending the base: +The following `ControllerFeedback` notification type is a class only extending the base: ``` php [[= include_file('code_samples/user_management/notifications/src/Notifications/ControllerFeedback.php') =]] @@ -242,10 +242,8 @@ Subscribe to this new notification type in `config/packages/notifications.yaml`: Notice that when introducing subscriptions configuration for `storefront_group` scope coming 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 this group can be checked with the following command during the configuration: - ```bash php bin/console ibexa:debug:config notifications.subscriptions --siteaccess=site ``` @@ -254,14 +252,14 @@ Reaching this controller in the back office (at `/admin/notification-sender`) tr ![Browser back office notification example](notification-browser-admin.png "Controller message displayed as a flash message in the browser") -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: +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: ![Browser storefront notification example](notification-browser-storefront.png "Controller message displayed as a flash message in the browser") ## Create a custom channel -A channel is a service implementing `Symfony\Component\Notifier\Channel\ChannelInterface`, and tagged `notifier.channel` alongside a `channel` shortname. +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. From b7ef37e25dfb8b04c4d3a6913a8bf9ca91038000 Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Tue, 31 Mar 2026 18:02:29 +0200 Subject: [PATCH 25/57] notification_channels.md: Apply some Copilot suggestions --- docs/users/notification_channels.md | 53 +++++++++++++++++------------ 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/docs/users/notification_channels.md b/docs/users/notification_channels.md index 5b11447547..54e2898bdf 100644 --- a/docs/users/notification_channels.md +++ b/docs/users/notification_channels.md @@ -5,17 +5,22 @@ month_change: true # Notification channels -the `ibexa/notifications` package offers an extension to the [Symfony notifier]([[= symfony_doc =]]/notifier.html) -allowing to subscribe to notifications and forward them to information channels like email addresses, SMS, communication platforms, etc., -including the [Back Office user profile notification](notifications.md#create-custom-notifications). +The `ibexa/notifications` package offers an extension to the [Symfony notifier]([[= symfony_doc =]]/notifier.html). +It allows to subscribe to notifications and forward them to various channels such as email, SMS, communication platforms, +and the [Back Office user notifications](notifications.md#create-custom-notifications). -Those notifications must not be confused with the [notification bars](notifications.md#notification-bars) (sent with [`TranslatableNotificationHandlerInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-AdminUi-Notification-TranslatableNotificationHandlerInterface.html)) -or the [user notifications](notifications.md#create-custom-notifications) (sent with [core `NotificationService`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-NotificationService.html)). +Those notifications must not be confused with the [notification bars](notifications.md#notification-bars) or the [user notifications](notifications.md#create-custom-notifications). + +| Notification family | Package | Service | Description | +|--------------------------------------------------------------------|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------| +| [notification bars](notifications.md#notification-bars) | `ibexa/admin-ui` | [`TranslatableNotificationHandlerInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-AdminUi-Notification-TranslatableNotificationHandlerInterface.html) | Rendered as a message bar in the bottom-right corner. | +| [user notifications](notifications.md#create-custom-notifications) | `ibexa/core` | [`NotificationService`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-NotificationService.html) | Rendered as a message in the back office bell menu. | +| [Channel subscribable notification](#subscribe-to-notifications) | `ìbexa/notifications` | [`NotificationServiceInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-Service-NotificationServiceInterface.html) | Render depends on the channels subscribing to the notification type. | The service implementing the [`Ibexa\Contracts\Notifications\Service\NotificationServiceInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-Service-NotificationServiceInterface.html) sends notifications extending the `Symfony\Component\Notifier\Notification\Notification`. Other services can have this notification service injected and send their own typed notifications. -Channel services implementing `Symfony\Component\Notifier\Channel\ChannelInterface` subscribe to a selections of notification types +Channel services implementing `Symfony\Component\Notifier\Channel\ChannelInterface` subscribe to a selection of notification types and take some actions like, for example, to convey notifications to users through various transports. ## Subscribe to notifications @@ -90,23 +95,23 @@ For example, in a new `config/packages/notifications.yaml` file: 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. +and optionally implements some interfaces depending on the channels it can be sent to. -- Some channels don't accept the notification if it doesn't implement its related notification interface. See the ⚠ column in the table below. +- Some channels don't accept the notification if it doesn't implement its related notification interface. They have checkmark in the ⚠ column below. - Some channels accept every notification and have a default behavior if the notification doesn't implement their related notification interface. -| Channel | Notification interface | ⚠ | -|:----------|:-------------------------------|----------| -| `chat` | `ChatNotificationInterface` | | -| `desktop` | `DesktopNotificationInterface` | | -| `email` | `EmailNotificationInterface` | ✔ | -| `ibexa` | `SystemNotificationInterface` | ✔ | -| `push` | `PushNotificationInterface` | | -| `sms` | `SmsNotificationInterface` | ✔ | +| Channel | Notification interface | ⚠ | +|:----------|:-------------------------------|---------------------------------------------------------------------------------------| +| `chat` | `ChatNotificationInterface` | | +| `desktop` | `DesktopNotificationInterface` | | +| `email` | `EmailNotificationInterface` | ✔ | +| `ibexa` | `SystemNotificationInterface` | ✔ | +| `push` | `PushNotificationInterface` | | +| `sms` | `SmsNotificationInterface` | ✔ | Notice that the [`SystemNotificationInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-SystemNotification-SystemNotificationInterface.html) is not part of Symfony and has its own namespace. -The `ibexa` channel send notifications to users through their profile menu, exactly as [user notification](notifications.md#create-custom-notifications). +The `ibexa` channel sends notifications to users through their profile menu, exactly as [user notification](notifications.md#create-custom-notifications). The [`SystemNotificationChannel` uses the core `NotificationService`](https://github.com/ibexa/notifications/blob/v5.0.6/src/lib/SystemNotification/SystemNotificationChannel.php#L51) to do so. Some channels don't need a recipient: @@ -127,7 +132,7 @@ The [`…\Service\NotificationServiceInterface::send()`](/api/php_api/php_api_re This interface is implemented by the [`…\Value\Recipent\SymfonyRecipientAdapter`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-Value-Recipent-SymfonyRecipientAdapter.html) made to wrap `Symfony\Component\Notifier\Recipient\RecipientInterface`. - This Symfony interface is implemented by [`…\Value\Recipent\UserRecipient`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-Value-Recipent-UserRecipient.html) - which can wrap classes implementing the [`Ibexa\Contracts\Core\Repository\Values\User\UserReference` interaface](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-User-UserReference.html) + which can wrap classes implementing the [`Ibexa\Contracts\Core\Repository\Values\User\UserReference` interface](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-User-UserReference.html) - The [`UserService` methods to load a user](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-UserService.html#method_loadUser) are returning objects implementing this `UserReference` interface. - The [`PermissionResolver::getCurrentUserReference()` method](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-PermissionResolver.html#method_getCurrentUserReference) is returning objects implementing this `UserReference` interface. @@ -151,7 +156,7 @@ $this->notificationService->send( ### `CommandExecuted` 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. +This example could be a scheduled task or cron job that warns users about its result. First, a `CommandExecuted` notification type is created. It is supported by two channels for the example but could be extended to more. @@ -209,7 +214,7 @@ An empty template only extending the pagelayout is used for the demonstration. ``` In the back office, a notification sent as a flash message has the `ibexa-alert--notification` CSS class. -This hasn't a default style. +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` declare the CSS class `ibexa-alert--notification` as being the same as the `ibexa-alert--info` CSS class @@ -225,7 +230,7 @@ This `assets/scss/notifications.scss` is added to the Admin UI layout in `webpac ``` 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. +This class already has a default style applied. Subscribe to this new notification type in `config/packages/notifications.yaml`: @@ -259,6 +264,9 @@ Reaching the controller in the default SiteAccess on Commerce edition (at `/noti ## 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 a finer dispatch. + 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. @@ -277,7 +285,8 @@ Now, [`CommandExecuted` notification](#commandexecuted-example) can be subscribe [[= include_file('code_samples/user_management/notifications/config/packages/notifications.yaml', 20, 25) =]] ``` -The log file contains the notifications: +The log contains the notifications +(in `var/log/dev.log` in this example run in the `dev` Symfony environment): ```console % tail -Fn0 var/log/dev.log | grep --line-buffered CommandExecuted From 818ae24c5590021457d2c1ab80031ab4855c13a9 Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Tue, 31 Mar 2026 18:34:21 +0200 Subject: [PATCH 26/57] notification_channels.md: Apply some Copilot suggestions --- docs/users/notification_channels.md | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/docs/users/notification_channels.md b/docs/users/notification_channels.md index 54e2898bdf..04e8df4c15 100644 --- a/docs/users/notification_channels.md +++ b/docs/users/notification_channels.md @@ -6,7 +6,7 @@ month_change: true # Notification channels The `ibexa/notifications` package offers an extension to the [Symfony notifier]([[= symfony_doc =]]/notifier.html). -It allows to subscribe to notifications and forward them to various channels such as email, SMS, communication platforms, +It allows you to subscribe to notifications and forward them to various channels such as email, SMS, communication platforms, and the [Back Office user notifications](notifications.md#create-custom-notifications). Those notifications must not be confused with the [notification bars](notifications.md#notification-bars) or the [user notifications](notifications.md#create-custom-notifications). @@ -15,7 +15,11 @@ Those notifications must not be confused with the [notification bars](notificati |--------------------------------------------------------------------|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------| | [notification bars](notifications.md#notification-bars) | `ibexa/admin-ui` | [`TranslatableNotificationHandlerInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-AdminUi-Notification-TranslatableNotificationHandlerInterface.html) | Rendered as a message bar in the bottom-right corner. | | [user notifications](notifications.md#create-custom-notifications) | `ibexa/core` | [`NotificationService`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-NotificationService.html) | Rendered as a message in the back office bell menu. | -| [Channel subscribable notification](#subscribe-to-notifications) | `ìbexa/notifications` | [`NotificationServiceInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-Service-NotificationServiceInterface.html) | Render depends on the channels subscribing to the notification type. | +| [Channel subscribable notification](#subscribe-to-notifications) | `ibexa/notifications` | [`NotificationServiceInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-Service-NotificationServiceInterface.html) | Render depends on the channels subscribing to the notification type. | + +Those notifications don't have a predefined channel like the ones from the first two bundles. +The channels you subscribe to notifications with determine the way the user recive the information. +A several channels are proposed, and you can create your owns. The service implementing the [`Ibexa\Contracts\Notifications\Service\NotificationServiceInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-Service-NotificationServiceInterface.html) sends notifications extending the `Symfony\Component\Notifier\Notification\Notification`. @@ -56,10 +60,12 @@ php bin/console debug:container --tag=notifier.channel * [`sms`]([[= symfony_doc =]]/notifier.html#sms-channel) - Notification forwarded to phone numbers Some default subscriptions can be found in `config/packages/ibexa.yaml` and `config/packages/ibexa_admin_ui.yaml`. +You can modify them or add your own subscriptions. +This page contains several examples of subscriptions configuration. !!! caution "Scopes may not merge as expected" - Subscriptions defined for a scope may not merge with subscriptions from others scopes or from other files. + 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 additions: @@ -95,7 +101,7 @@ For example, in a new `config/packages/notifications.yaml` file: 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 can be sent to. +and can optionally implement interfaces required by specific channels. - Some channels don't accept the notification if it doesn't implement its related notification interface. They have checkmark in the ⚠ column below. - Some channels accept every notification and have a default behavior if the notification doesn't implement their related notification interface. @@ -136,7 +142,7 @@ The [`…\Service\NotificationServiceInterface::send()`](/api/php_api/php_api_re - The [`UserService` methods to load a user](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-UserService.html#method_loadUser) are returning objects implementing this `UserReference` interface. - The [`PermissionResolver::getCurrentUserReference()` method](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-PermissionResolver.html#method_getCurrentUserReference) is returning objects implementing this `UserReference` interface. -So, for example, to send a notification, you often as this kind of combo: +For example, to send a notification, you often use a combination like this: ```php hl_lines="8-11" use App\Notifications\MyNotification; // Extends Symfony\Component\Notifier\Notification\Notification @@ -173,7 +179,7 @@ The channels subscribing to this notification are set in `config/packages/notifi ``` The example command sends a `CommandExecuted` notification at the end of what could be a regular execution. -It randomly succeeds or fails to show how the notification can be used to communicate different execution results. +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. ``` php @@ -217,7 +223,7 @@ In the back office, a notification sent as a flash message has the `ibexa-alert- 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` declare the CSS class `ibexa-alert--notification` as being the same as the `ibexa-alert--info` CSS class +The `assets/scss/notifications.scss` declares the CSS class `ibexa-alert--notification` as being the same as the `ibexa-alert--info` CSS class ``` scss [[= include_file('code_samples/user_management/notifications/assets/scss/notifications.scss') =]] @@ -255,11 +261,11 @@ Subscribe to this new notification type in `config/packages/notifications.yaml`: Reaching this controller in the back office (at `/admin/notification-sender`) triggers the notification as a flash message in the bottom-right corner: -![Browser back office notification example](notification-browser-admin.png "Controller message displayed as a flash message in the browser") +![Notification in back office](notification-browser-admin.png "Controller message displayed as a flash message in the browser") 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: -![Browser storefront notification example](notification-browser-storefront.png "Controller message displayed as a flash message in the browser") +![Notification in storefront](notification-browser-storefront.png "Controller message displayed as a flash message in the browser") ## Create a custom channel From 0ebcb955b02d7d520e8f5c5342d5b3a704bb38fc Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Wed, 1 Apr 2026 09:14:08 +0200 Subject: [PATCH 27/57] notification_channels.md: Apply some Copilot suggestions --- docs/users/notification_channels.md | 50 ++++++++++++++--------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/docs/users/notification_channels.md b/docs/users/notification_channels.md index 04e8df4c15..4ff986bc9f 100644 --- a/docs/users/notification_channels.md +++ b/docs/users/notification_channels.md @@ -9,7 +9,7 @@ The `ibexa/notifications` package offers an extension to the [Symfony notifier]( It allows you to subscribe to notifications and forward them to various channels such as email, SMS, communication platforms, and the [Back Office user notifications](notifications.md#create-custom-notifications). -Those notifications must not be confused with the [notification bars](notifications.md#notification-bars) or the [user notifications](notifications.md#create-custom-notifications). +These notifications must not be confused with the [notification bars](notifications.md#notification-bars) or the [user notifications](notifications.md#create-custom-notifications). | Notification family | Package | Service | Description | |--------------------------------------------------------------------|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------| @@ -17,15 +17,15 @@ Those notifications must not be confused with the [notification bars](notificati | [user notifications](notifications.md#create-custom-notifications) | `ibexa/core` | [`NotificationService`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-NotificationService.html) | Rendered as a message in the back office bell menu. | | [Channel subscribable notification](#subscribe-to-notifications) | `ibexa/notifications` | [`NotificationServiceInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-Service-NotificationServiceInterface.html) | Render depends on the channels subscribing to the notification type. | -Those notifications don't have a predefined channel like the ones from the first two bundles. -The channels you subscribe to notifications with determine the way the user recive the information. -A several channels are proposed, and you can create your owns. +These notifications don't have a predefined channel like the ones from the first two bundles. +The channels you subscribe to notifications with determine how the user receives the information. +Several channels are provided, and you can create your own. The service implementing the [`Ibexa\Contracts\Notifications\Service\NotificationServiceInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-Service-NotificationServiceInterface.html) sends notifications extending the `Symfony\Component\Notifier\Notification\Notification`. Other services can have this notification service injected and send their own typed notifications. Channel services implementing `Symfony\Component\Notifier\Channel\ChannelInterface` subscribe to a selection of notification types -and take some actions like, for example, to convey notifications to users through various transports. +and take actions, such as conveying notifications to users through various transports. ## Subscribe to notifications @@ -90,7 +90,7 @@ In a .env file, [set the DSN to target a Slack channel or a Slack user](https:// SLACK_DSN=slack://xoxb-token@default?channel=ibexa-notifications ``` -Subscribe to notification types related to Commerce like order, payment, and shipment status changes. +Subscribe to notification types related to Commerce, such as order, payment, and shipment status changes. For example, in a new `config/packages/notifications.yaml` file: ``` yaml hl_lines="12-20" @@ -103,7 +103,7 @@ A new notification class can be created to send a new type of message to a new s It must extend `Symfony\Component\Notifier\Notification\Notification` and can optionally implement interfaces required by specific channels. -- Some channels don't accept the notification if it doesn't implement its related notification interface. They have checkmark in the ⚠ column below. +- Some channels don't accept the notification if it doesn't implement their related notification interface. They have a checkmark in the ⚠ column below. - Some channels accept every notification and have a default behavior if the notification doesn't implement their related notification interface. | Channel | Notification interface | ⚠ | @@ -115,15 +115,15 @@ and can optionally implement interfaces required by specific channels. | `push` | `PushNotificationInterface` | | | `sms` | `SmsNotificationInterface` | ✔ | -Notice that the [`SystemNotificationInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-SystemNotification-SystemNotificationInterface.html) is not part of Symfony and has its own namespace. +Note that the [`SystemNotificationInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-SystemNotification-SystemNotificationInterface.html) is not part of Symfony and has its own namespace. -The `ibexa` channel sends notifications to users through their profile menu, exactly as [user notification](notifications.md#create-custom-notifications). +The `ibexa` channel sends notifications to users through their profile menu, exactly as [user notifications](notifications.md#create-custom-notifications). The [`SystemNotificationChannel` uses the core `NotificationService`](https://github.com/ibexa/notifications/blob/v5.0.6/src/lib/SystemNotification/SystemNotificationChannel.php#L51) to do so. Some channels don't need a recipient: -- `browser`: Always send a flash message to the current user -- `chat`: Always send message to the same connection resource +- `browser`: Always sends a flash message to the current user +- `chat`: Always sends a message to the same connection resource ### Notification sending @@ -133,16 +133,16 @@ The [`…\Service\NotificationServiceInterface::send()`](/api/php_api/php_api_re - The first argument is an [`…\Value\NotificationInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-Value-NotificationInterface.html). This interface is implemented by the [`…\Value\Notification\SymfonyNotificationAdapter`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-Value-Notification-SymfonyNotificationAdapter.html) - which allows to wrap any class extending `Symfony\Component\Notifier\Notification\Notification`. + which allows you to wrap any class extending `Symfony\Component\Notifier\Notification\Notification`. - The optional second argument is an array of [`…\Value\RecipientInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-Value-RecipientInterface.html). This interface is implemented by the [`…\Value\Recipent\SymfonyRecipientAdapter`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-Value-Recipent-SymfonyRecipientAdapter.html) - made to wrap `Symfony\Component\Notifier\Recipient\RecipientInterface`. + used to wrap `Symfony\Component\Notifier\Recipient\RecipientInterface`. - This Symfony interface is implemented by [`…\Value\Recipent\UserRecipient`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-Value-Recipent-UserRecipient.html) - which can wrap classes implementing the [`Ibexa\Contracts\Core\Repository\Values\User\UserReference` interface](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-User-UserReference.html) + which can wrap classes implementing the [`Ibexa\Contracts\Core\Repository\Values\User\UserReference` interface](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-User-UserReference.html), - The [`UserService` methods to load a user](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-UserService.html#method_loadUser) are returning objects implementing this `UserReference` interface. - The [`PermissionResolver::getCurrentUserReference()` method](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-PermissionResolver.html#method_getCurrentUserReference) is returning objects implementing this `UserReference` interface. -For example, to send a notification, you often use a combination like this: +For example, to send a notification, you often use a combination like the following: ```php hl_lines="8-11" use App\Notifications\MyNotification; // Extends Symfony\Component\Notifier\Notification\Notification @@ -165,8 +165,8 @@ The following example is a command that sends a notification to users on several This example could be a scheduled task or cron job that warns users about its result. First, a `CommandExecuted` notification type is created. -It is supported by two channels for the example but could be extended to more. -As constructor arguments, an instance takes the command itself, the exit code of the run, and caught exceptions. +It's supported by two channels in this example but could be extended to more. +As constructor arguments, an instance takes the command itself, the exit code of the run, and any caught exceptions. ``` php [[= include_file('code_samples/user_management/notifications/src/Notifications/CommandExecuted.php') =]] @@ -180,7 +180,7 @@ The channels subscribing to this notification are set in `config/packages/notifi The example command sends a `CommandExecuted` notification at the end of what could be a regular 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. +It could be declared as a service to set the list of recipients' logins (`$recipientLogins`) from a configuration file. ``` php [[= include_file('code_samples/user_management/notifications/src/Command/NotificationSenderCommand.php') =]] @@ -194,7 +194,7 @@ If you execute this command, it sometimes succeeds, sometimes fails. 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 only extending the base: +The following `ControllerFeedback` notification type is a class that only extends the base: ``` php [[= include_file('code_samples/user_management/notifications/src/Notifications/ControllerFeedback.php') =]] @@ -240,7 +240,7 @@ 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 +- In the `admin_group` scope with the `browser` channel - For Commerce edition, in the `storefront_group` scope with the `browser` channel ``` yaml hl_lines="13-15 43-45" @@ -250,11 +250,11 @@ Subscribe to this new notification type in `config/packages/notifications.yaml`: !!! note "Subscriptions for `storefront_group`" - Notice that when introducing subscriptions configuration for `storefront_group` scope coming with Commerce edition, + 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 this group - can be checked with the following command during the configuration: + For example, the subscriptions for the `site` SiteAccess belonging to this group + can be checked with the following command during configuration: ```bash php bin/console ibexa:debug:config notifications.subscriptions --siteaccess=site ``` @@ -271,7 +271,7 @@ Reaching the controller in the default SiteAccess on Commerce edition (at `/noti ## 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 a finer dispatch. +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. @@ -285,7 +285,7 @@ The following example is a custom channel that sends notifications to the logger [[= include_file('code_samples/user_management/notifications/config/services.yaml') =]] ``` -Now, [`CommandExecuted` notification](#commandexecuted-example) can be subscribed to with the `log` channel: +Now, the [`CommandExecuted` notification](#commandexecuted-example) can be subscribed to with the `log` channel: ``` yaml hl_lines="5" [[= include_file('code_samples/user_management/notifications/config/packages/notifications.yaml', 20, 25) =]] From 3714c678268200891a5109d505ea2a9b7e9ece5e Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Wed, 8 Apr 2026 10:26:26 +0200 Subject: [PATCH 28/57] Apply suggestions from code review Co-authored-by: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> --- docs/administration/back_office/notifications.md | 6 +++--- mkdocs.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/administration/back_office/notifications.md b/docs/administration/back_office/notifications.md index 5e41f3dd3e..153272d96f 100644 --- a/docs/administration/back_office/notifications.md +++ b/docs/administration/back_office/notifications.md @@ -118,8 +118,8 @@ ibexa: The values shown above are the defaults. `0` means the notification doesn't hide automatically. -### `ibexa` notification channel +## `ibexa` notification channel -You can also subscribe to a notification with the channel `ibexa` +You can also subscribe to a notification with the `ibexa` channel. -See [Notifications to channels](notification_channels.md) for more details about notifications and channel subscription. +For more information, see [Notifications to channels](notification_channels.md). diff --git a/mkdocs.yml b/mkdocs.yml index c2cd4a16c3..88a2b1ac72 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -480,7 +480,7 @@ nav: - User grouping: - Customer groups: users/customer_groups.md - Segment API: users/segment_api.md - - User notifications: users/notification_channels.md + - Notification channels: users/notification_channels.md - Personalization: - Personalization: personalization/personalization.md - Personalization guide : personalization/personalization_guide.md From 28c91669fb59acde6225d92e02dd4faa10649706 Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Thu, 30 Apr 2026 09:58:38 +0200 Subject: [PATCH 29/57] =?UTF-8?q?include=5Ffile=20=E2=86=92=20include=5Fco?= =?UTF-8?q?de?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/users/notification_channels.md | 33 ++++++++++++++++------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/docs/users/notification_channels.md b/docs/users/notification_channels.md index 4ff986bc9f..0c4ad36462 100644 --- a/docs/users/notification_channels.md +++ b/docs/users/notification_channels.md @@ -94,7 +94,7 @@ Subscribe to notification types related to Commerce, such as order, payment, and For example, in a new `config/packages/notifications.yaml` file: ``` yaml hl_lines="12-20" -[[= include_file('code_samples/user_management/notifications/config/packages/notifications.yaml', 0, 20) =]] +[[= include_code('code_samples/user_management/notifications/config/packages/notifications.yaml', 1, 20) =]] ``` ## Create a notification class @@ -169,13 +169,13 @@ It's supported by two channels in this example but could be extended to more. As constructor arguments, an instance takes the command itself, the exit code of the run, and any caught exceptions. ``` php -[[= include_file('code_samples/user_management/notifications/src/Notifications/CommandExecuted.php') =]] +[[= include_code('code_samples/user_management/notifications/src/Notifications/CommandExecuted.php') =]] ``` The channels subscribing to this notification are set in `config/packages/notifications.yaml`: ``` yaml hl_lines="17-20" -[[= include_file('code_samples/user_management/notifications/config/packages/notifications.yaml', 4, 24) =]] +[[= include_code('code_samples/user_management/notifications/config/packages/notifications.yaml', 5, 24) =]] ``` The example command sends a `CommandExecuted` notification at the end of what could be a regular execution. @@ -183,7 +183,7 @@ It randomly succeeds or fails to demonstrate how notifications can communicate d It could be declared as a service to set the list of recipients' logins (`$recipientLogins`) from a configuration file. ``` php -[[= include_file('code_samples/user_management/notifications/src/Command/NotificationSenderCommand.php') =]] +[[= include_code('code_samples/user_management/notifications/src/Command/NotificationSenderCommand.php') =]] ``` If you execute this command, it sometimes succeeds, sometimes fails. @@ -197,13 +197,13 @@ The following example shows a custom notification sent by a controller and displ The following `ControllerFeedback` notification type is a class that only extends the base: ``` php -[[= include_file('code_samples/user_management/notifications/src/Notifications/ControllerFeedback.php') =]] +[[= include_code('code_samples/user_management/notifications/src/Notifications/ControllerFeedback.php') =]] ``` The `ControllerFeedback` notification is sent in a controller action: ``` php -[[= include_file('code_samples/user_management/notifications/src/Controller/NotificationSenderController.php') =]] +[[= include_code('code_samples/user_management/notifications/src/Controller/NotificationSenderController.php') =]] ``` For the example, the notification is sent in a back office context for all editions and on the front end for Commerce edition. @@ -211,12 +211,12 @@ An empty template only extending the pagelayout is used for the demonstration. `templates/themes/admin/notification-sender-controller.html.twig`: ``` twig -[[= include_file('code_samples/user_management/notifications/templates/themes/admin/notification-sender-controller.html.twig') =]] +[[= include_code('code_samples/user_management/notifications/templates/themes/admin/notification-sender-controller.html.twig') =]] ``` `templates/themes/storefront/notification-sender-controller.html.twig`: ``` twig -[[= include_file('code_samples/user_management/notifications/templates/themes/storefront/notification-sender-controller.html.twig') =]] +[[= include_code('code_samples/user_management/notifications/templates/themes/storefront/notification-sender-controller.html.twig') =]] ``` In the back office, a notification sent as a flash message has the `ibexa-alert--notification` CSS class. @@ -226,13 +226,13 @@ 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 ``` scss -[[= include_file('code_samples/user_management/notifications/assets/scss/notifications.scss') =]] +[[= include_code('code_samples/user_management/notifications/assets/scss/notifications.scss') =]] ``` This `assets/scss/notifications.scss` is added to the Admin UI layout in `webpack.config.js`: ``` javascript -[[= include_file('code_samples/user_management/notifications/webpack.config.js', 49) =]] +[[= include_code('code_samples/user_management/notifications/webpack.config.js', 50) =]] ``` On the storefront, a notification sent as a flash message has the `ibexa-store-notification--notification` CSS class. @@ -244,8 +244,11 @@ Subscribe to this new notification type in `config/packages/notifications.yaml`: - For Commerce edition, in the `storefront_group` scope with the `browser` channel ``` yaml hl_lines="13-15 43-45" -[[= include_file('code_samples/user_management/notifications/config/packages/notifications.yaml', 4, 6) =]] # … -[[= include_file('code_samples/user_management/notifications/config/packages/notifications.yaml', 25, 34) =]][[= include_file('code_samples/user_management/notifications/config/packages/notifications.yaml', 35, 65) =]][[= include_file('code_samples/user_management/notifications/config/packages/notifications.yaml', 66) =]] +[[= include_code('code_samples/user_management/notifications/config/packages/notifications.yaml', 5, 6) =]] + # … +[[= include_code('code_samples/user_management/notifications/config/packages/notifications.yaml', 26, 34) =]] +[[= include_code('code_samples/user_management/notifications/config/packages/notifications.yaml', 36, 65) =]] +[[= include_code('code_samples/user_management/notifications/config/packages/notifications.yaml', 67) =]] ``` !!! note "Subscriptions for `storefront_group`" @@ -278,17 +281,17 @@ A channel is a service implementing `Symfony\Component\Notifier\Channel\ChannelI The following example is a custom channel that sends notifications to the logger. ``` php -[[= include_file('code_samples/user_management/notifications/src/Notifier/Channel/LogChannel.php') =]] +[[= include_code('code_samples/user_management/notifications/src/Notifier/Channel/LogChannel.php') =]] ``` ``` yaml -[[= include_file('code_samples/user_management/notifications/config/services.yaml') =]] +[[= include_code('code_samples/user_management/notifications/config/services.yaml') =]] ``` Now, the [`CommandExecuted` notification](#commandexecuted-example) can be subscribed to with the `log` channel: ``` yaml hl_lines="5" -[[= include_file('code_samples/user_management/notifications/config/packages/notifications.yaml', 20, 25) =]] +[[= include_code('code_samples/user_management/notifications/config/packages/notifications.yaml', 21, 25) =]] ``` The log contains the notifications From 1ebca76dd77eb7eaa91614a5f4c6c0b8d13f0738 Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Thu, 30 Apr 2026 10:05:38 +0200 Subject: [PATCH 30/57] Apply suggestion from @adriendupuis --- docs/users/notification_channels.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/users/notification_channels.md b/docs/users/notification_channels.md index 0c4ad36462..e78e30d776 100644 --- a/docs/users/notification_channels.md +++ b/docs/users/notification_channels.md @@ -115,7 +115,7 @@ and can optionally implement interfaces required by specific channels. | `push` | `PushNotificationInterface` | | | `sms` | `SmsNotificationInterface` | ✔ | -Note that the [`SystemNotificationInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-SystemNotification-SystemNotificationInterface.html) is not part of Symfony and has its own namespace. +The [`SystemNotificationInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-SystemNotification-SystemNotificationInterface.html) is not part of Symfony and has its own namespace. The `ibexa` channel sends notifications to users through their profile menu, exactly as [user notifications](notifications.md#create-custom-notifications). The [`SystemNotificationChannel` uses the core `NotificationService`](https://github.com/ibexa/notifications/blob/v5.0.6/src/lib/SystemNotification/SystemNotificationChannel.php#L51) to do so. From 8ae31a219b802cabf7f87eeec454a87a11bf19b4 Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Thu, 30 Apr 2026 15:40:07 +0200 Subject: [PATCH 31/57] Apply suggestions from code review Co-authored-by: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> --- docs/users/notification_channels.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/users/notification_channels.md b/docs/users/notification_channels.md index e78e30d776..7fc57500fd 100644 --- a/docs/users/notification_channels.md +++ b/docs/users/notification_channels.md @@ -51,6 +51,7 @@ Available notification channels: php bin/console debug:container --tag=notifier.channel ``` +* `actito` - Notification forwarded as [transactional email](transactional_emails.md) * `browser` - Notification forwarded as flash message * [`chat`]([[= symfony_doc =]]/notifier.html#chat-channel) - Notification forwarded to a communication platform like Slack, Microsoft Teams, or Google Chat * [`desktop`]([[= symfony_doc =]]/notifier.html#chat-channel) - Notification forwarded to desktop applications like JoliNotif @@ -108,6 +109,7 @@ and can optionally implement interfaces required by specific channels. | Channel | Notification interface | ⚠ | |:----------|:-------------------------------|---------------------------------------------------------------------------------------| +| `actito` | `EmailNotificationInterface` | ✔ | | `chat` | `ChatNotificationInterface` | | | `desktop` | `DesktopNotificationInterface` | | | `email` | `EmailNotificationInterface` | ✔ | From 0a1a950be510d315c4d8ffce74e0b0d2321d07d2 Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Thu, 30 Apr 2026 15:48:13 +0200 Subject: [PATCH 32/57] Apply suggestions from code review Co-authored-by: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> --- docs/users/notification_channels.md | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/docs/users/notification_channels.md b/docs/users/notification_channels.md index 7fc57500fd..17d1664ce9 100644 --- a/docs/users/notification_channels.md +++ b/docs/users/notification_channels.md @@ -31,7 +31,7 @@ and take actions, such as conveying notifications to users through various trans Some events send notifications you can subscribe to with one or more channels. -Available notifications: +### Available notifications * [`Ibexa\Contracts\FormBuilder\Notifications\FormSubmitted`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-FormBuilder-Notifications-FormSubmitted.html) * [`Ibexa\Contracts\Notifications\SystemNotification\SystemNotification`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-SystemNotification-SystemNotification.html) @@ -45,11 +45,10 @@ Available notifications: * `Ibexa\Share\Notification\ContentViewInvitationNotification` * `Ibexa\Share\Notification\ExternalParticipantContentViewInvitationNotification` -Available notification channels: +### Available notification channels + +You can list the notification channel services with the following command: -```bash -php bin/console debug:container --tag=notifier.channel -``` * `actito` - Notification forwarded as [transactional email](transactional_emails.md) * `browser` - Notification forwarded as flash message @@ -60,6 +59,8 @@ php bin/console debug:container --tag=notifier.channel * [`push`]([[= symfony_doc =]]/notifier.html#push-channel) - Notification forwarded to specific applications * [`sms`]([[= symfony_doc =]]/notifier.html#sms-channel) - Notification forwarded to phone numbers +### Subscriptions configuration + Some default subscriptions can be found in `config/packages/ibexa.yaml` and `config/packages/ibexa_admin_ui.yaml`. You can modify them or add your own subscriptions. This page contains several examples of subscriptions configuration. @@ -71,11 +72,7 @@ This page contains several examples of subscriptions configuration. To ensure you don't unsubscribe against your will, always use the following command to check subscriptions for a siteaccess before and after additions: - ```bash - php bin/console ibexa:debug:config notifications.subscriptions --siteaccess= - ``` - -### Subscription example + For example, let's subscribe to Commerce activity with a Slack channel. From 2216bec8a3620242f21b40ccd9227786a404af98 Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Thu, 30 Apr 2026 15:50:02 +0200 Subject: [PATCH 33/57] Apply suggestion from @adriendupuis --- docs/users/notification_channels.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/users/notification_channels.md b/docs/users/notification_channels.md index 17d1664ce9..8b711afe6a 100644 --- a/docs/users/notification_channels.md +++ b/docs/users/notification_channels.md @@ -72,7 +72,7 @@ This page contains several examples of subscriptions configuration. To ensure you don't unsubscribe against your will, always use the following command to check subscriptions for a siteaccess before and after additions: - +#### Subscription example For example, let's subscribe to Commerce activity with a Slack channel. From 07c2cad7112ab7c840b340c6e7b91dfc14be9974 Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Thu, 30 Apr 2026 15:55:48 +0200 Subject: [PATCH 34/57] Fix notification_channels.md command examples --- docs/users/notification_channels.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/users/notification_channels.md b/docs/users/notification_channels.md index e78e30d776..42de6d137a 100644 --- a/docs/users/notification_channels.md +++ b/docs/users/notification_channels.md @@ -31,7 +31,7 @@ and take actions, such as conveying notifications to users through various trans Some events send notifications you can subscribe to with one or more channels. -Available notifications: +### Available notifications * [`Ibexa\Contracts\FormBuilder\Notifications\FormSubmitted`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-FormBuilder-Notifications-FormSubmitted.html) * [`Ibexa\Contracts\Notifications\SystemNotification\SystemNotification`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-SystemNotification-SystemNotification.html) @@ -45,12 +45,15 @@ Available notifications: * `Ibexa\Share\Notification\ContentViewInvitationNotification` * `Ibexa\Share\Notification\ExternalParticipantContentViewInvitationNotification` -Available notification channels: +### Available notification channels + +You can list the notification channel services with the following command: ```bash php bin/console debug:container --tag=notifier.channel ``` +* `actito` - Notification forwarded as [transactional email](transactional_emails.md) * `browser` - Notification forwarded as flash message * [`chat`]([[= symfony_doc =]]/notifier.html#chat-channel) - Notification forwarded to a communication platform like Slack, Microsoft Teams, or Google Chat * [`desktop`]([[= symfony_doc =]]/notifier.html#chat-channel) - Notification forwarded to desktop applications like JoliNotif @@ -59,6 +62,8 @@ php bin/console debug:container --tag=notifier.channel * [`push`]([[= symfony_doc =]]/notifier.html#push-channel) - Notification forwarded to specific applications * [`sms`]([[= symfony_doc =]]/notifier.html#sms-channel) - Notification forwarded to phone numbers +### Subscriptions configuration + Some default subscriptions can be found in `config/packages/ibexa.yaml` and `config/packages/ibexa_admin_ui.yaml`. You can modify them or add your own subscriptions. This page contains several examples of subscriptions configuration. @@ -74,7 +79,7 @@ This page contains several examples of subscriptions configuration. php bin/console ibexa:debug:config notifications.subscriptions --siteaccess= ``` -### Subscription example +#### Subscription example For example, let's subscribe to Commerce activity with a Slack channel. @@ -108,6 +113,7 @@ and can optionally implement interfaces required by specific channels. | Channel | Notification interface | ⚠ | |:----------|:-------------------------------|---------------------------------------------------------------------------------------| +| `actito` | `EmailNotificationInterface` | ✔ | | `chat` | `ChatNotificationInterface` | | | `desktop` | `DesktopNotificationInterface` | | | `email` | `EmailNotificationInterface` | ✔ | From f37a25fa694a8182cb81a79dd6efc0aedeacabd1 Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Wed, 6 May 2026 11:19:19 +0200 Subject: [PATCH 35/57] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Marek Nocoń Co-authored-by: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> --- .../assets/scss/notifications.scss | 2 +- .../config/packages/notifications.yaml | 6 +- docs/users/notification_channels.md | 62 +++++++++---------- 3 files changed, 34 insertions(+), 36 deletions(-) diff --git a/code_samples/user_management/notifications/assets/scss/notifications.scss b/code_samples/user_management/notifications/assets/scss/notifications.scss index db5e780aac..70e58d2612 100644 --- a/code_samples/user_management/notifications/assets/scss/notifications.scss +++ b/code_samples/user_management/notifications/assets/scss/notifications.scss @@ -1,4 +1,4 @@ -@use '../../vendor/ibexa/admin-ui/src/bundle/Resources/public/scss/_alerts.scss' as *; +@use '@ibexa-admin-ui/src/bundle/Resources/public/scss/_alerts.scss' as *; .ibexa-alert { &--notification { diff --git a/code_samples/user_management/notifications/config/packages/notifications.yaml b/code_samples/user_management/notifications/config/packages/notifications.yaml index a67980b726..68473810a2 100644 --- a/code_samples/user_management/notifications/config/packages/notifications.yaml +++ b/code_samples/user_management/notifications/config/packages/notifications.yaml @@ -7,7 +7,7 @@ ibexa: default: notifier: subscriptions: - # Default subscriptions from ibexa.yaml are properly merged. + # The configuration below is added to the `default` scope without overriding the one defined in ibexa.yaml # Custom subscriptions: Ibexa\OrderManagement\Notification\OrderStatusChange: channels: @@ -26,7 +26,7 @@ ibexa: admin_group: notifier: subscriptions: - # Default subscriptions from ibexa_admin_ui.yaml are properly merged + # 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: @@ -39,7 +39,7 @@ ibexa: storefront_group: notifier: subscriptions: - # Default subscriptions from ibexa.yaml: + # The configuration defined in ibexa.yaml for `default` scope is repeated as the configuration below overrides it Ibexa\Contracts\User\Notification\UserPasswordReset: channels: - email diff --git a/docs/users/notification_channels.md b/docs/users/notification_channels.md index 42de6d137a..62ace9da1c 100644 --- a/docs/users/notification_channels.md +++ b/docs/users/notification_channels.md @@ -5,11 +5,11 @@ month_change: true # Notification channels -The `ibexa/notifications` package offers an extension to the [Symfony notifier]([[= symfony_doc =]]/notifier.html). -It allows you to subscribe to notifications and forward them to various channels such as email, SMS, communication platforms, -and the [Back Office user notifications](notifications.md#create-custom-notifications). +The `ibexa/notifications` package integrates the [Symfony Notifier]([[= symfony_doc =]]/notifier.html) with [[= product_name =]]. +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](notifications.md#create-custom-notifications). -These notifications must not be confused with the [notification bars](notifications.md#notification-bars) or the [user notifications](notifications.md#create-custom-notifications). +These notifications must not be confused with the [notification bars](notifications.md#notification-bars) or the [user notifications](notifications.md#create-custom-notifications): | Notification family | Package | Service | Description | |--------------------------------------------------------------------|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------| @@ -17,21 +17,21 @@ These notifications must not be confused with the [notification bars](notificati | [user notifications](notifications.md#create-custom-notifications) | `ibexa/core` | [`NotificationService`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-NotificationService.html) | Rendered as a message in the back office bell menu. | | [Channel subscribable notification](#subscribe-to-notifications) | `ibexa/notifications` | [`NotificationServiceInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-Service-NotificationServiceInterface.html) | Render depends on the channels subscribing to the notification type. | -These notifications don't have a predefined channel like the ones from the first two bundles. -The channels you subscribe to notifications with determine how the user receives the information. +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 service implementing the [`Ibexa\Contracts\Notifications\Service\NotificationServiceInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-Service-NotificationServiceInterface.html) -sends notifications extending the `Symfony\Component\Notifier\Notification\Notification`. -Other services can have this notification service injected and send their own typed notifications. +The [`Ibexa\Contracts\Notifications\Service\NotificationServiceInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-Service-NotificationServiceInterface.html) +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 take actions, such as conveying notifications to users through various transports. +and deliver notifications to users through various transports. ## Subscribe to notifications -Some events send notifications you can subscribe to with one or more channels. +Some events generate notifications that you can deliver to the users through one or more channels. -### Available notifications +### Available notification types * [`Ibexa\Contracts\FormBuilder\Notifications\FormSubmitted`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-FormBuilder-Notifications-FormSubmitted.html) * [`Ibexa\Contracts\Notifications\SystemNotification\SystemNotification`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-SystemNotification-SystemNotification.html) @@ -64,8 +64,8 @@ php bin/console debug:container --tag=notifier.channel ### Subscriptions configuration -Some default subscriptions can be found in `config/packages/ibexa.yaml` and `config/packages/ibexa_admin_ui.yaml`. -You can modify them or add your own subscriptions. +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. !!! caution "Scopes may not merge as expected" @@ -73,7 +73,7 @@ This page contains several examples of subscriptions configuration. 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 additions: + always use the following command to check subscriptions for a siteaccess before and after any changes: ```bash php bin/console ibexa:debug:config notifications.subscriptions --siteaccess= @@ -81,31 +81,30 @@ This page contains several examples of subscriptions configuration. #### Subscription example -For example, let's subscribe to Commerce activity with a Slack channel. +The following example shows how you can deliver notifications about Commerce-related activities through Slack: -Install the Slack Notifier package: +1. Install the Slack Notifier package: ```bash composer require symfony/slack-notifier ``` -In a .env file, [set the DSN to target a Slack channel or a Slack user](https://github.com/symfony/slack-notifier?tab=readme-ov-file#dsn-example): +2. In a .env file, [set the DSN to target a Slack channel or a Slack user](https://github.com/symfony/slack-notifier?tab=readme-ov-file#dsn-example): ```dotenv SLACK_DSN=slack://xoxb-token@default?channel=ibexa-notifications ``` -Subscribe to notification types related to Commerce, such as order, payment, and shipment status changes. -For example, in a new `config/packages/notifications.yaml` file: +3. 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: ``` yaml hl_lines="12-20" [[= include_code('code_samples/user_management/notifications/config/packages/notifications.yaml', 1, 20) =]] -``` ## 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` +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. They have a checkmark in the ⚠ column below. @@ -133,7 +132,7 @@ Some channels don't need a recipient: ### Notification sending -In the [`Ibexa\Contracts\Notifications`](/api/php_api/php_api_reference/namespaces/ibexa-contracts-notifications.html) namespace can be found everything needed to work with notifications. +Use the objects from the [`Ibexa\Contracts\Notifications`](/api/php_api/php_api_reference/namespaces/ibexa-contracts-notifications.html) namespace to work with notifications. The [`…\Service\NotificationServiceInterface::send()`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-Service-NotificationServiceInterface.html#method_send) expects two arguments: @@ -170,29 +169,28 @@ $this->notificationService->send( 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. -First, a `CommandExecuted` notification type is created. -It's supported by two channels in this example but could be extended to more. +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. ``` php [[= include_code('code_samples/user_management/notifications/src/Notifications/CommandExecuted.php') =]] ``` -The channels subscribing to this notification are set in `config/packages/notifications.yaml`: +2. Assign channels subscribed to this notification in `config/packages/notifications.yaml`: ``` yaml hl_lines="17-20" [[= include_code('code_samples/user_management/notifications/config/packages/notifications.yaml', 5, 24) =]] ``` -The example command sends a `CommandExecuted` notification at the end of what could be a regular execution. +3. 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. -``` php [[= include_code('code_samples/user_management/notifications/src/Command/NotificationSenderCommand.php') =]] ``` -If you execute this command, it sometimes succeeds, sometimes fails. +When you execute this command, it fails randomly and notifies the Administrator user about the result. ![Ibexa notification example](notification-ibexa.png "Command notifications shown in the `ibexa` channel, the back office user notification menu") @@ -294,14 +292,14 @@ The following example is a custom channel that sends notifications to the logger [[= include_code('code_samples/user_management/notifications/config/services.yaml') =]] ``` -Now, the [`CommandExecuted` notification](#commandexecuted-example) can be subscribed to with the `log` channel: +Now, the [`CommandExecuted` notification](#commandexecuted-example) can be subscribed to the `log` channel: ``` yaml hl_lines="5" [[= include_code('code_samples/user_management/notifications/config/packages/notifications.yaml', 21, 25) =]] ``` The log contains the notifications -(in `var/log/dev.log` in this example run in the `dev` Symfony environment): +(in `var/log/dev.log` when run in the `dev` Symfony environment): ```console % tail -Fn0 var/log/dev.log | grep --line-buffered CommandExecuted From b6211b17e17373865e7b561ced2fc5fcd73be231 Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Wed, 6 May 2026 12:08:59 +0200 Subject: [PATCH 36/57] back_office/notifications.md: Rework ToC --- .../back_office/notifications.md | 90 ++++++++++--------- 1 file changed, 48 insertions(+), 42 deletions(-) diff --git a/docs/administration/back_office/notifications.md b/docs/administration/back_office/notifications.md index 153272d96f..a5ba153042 100644 --- a/docs/administration/back_office/notifications.md +++ b/docs/administration/back_office/notifications.md @@ -7,22 +7,21 @@ month_change: false You can send two types of notifications to the users. -[Notification bar](#notification-bars) is displayed in specific situations as a message bar appearing at the bottom of the page. -It appears to whoever is doing a specific operation in the back office. +- [Notification bar](#notification-bars) is displayed in specific situations as a message bar appearing at the bottom of the page. + It appears to whoever is doing a specific operation in the back office. +- [User notifications](#user-notifications) are sent to a specific user. + They appear in their profile in the back office. -![Example of an info notification](notification2.png "Example of notification bar") - -[Custom notifications](#create-custom-notifications) are sent to a specific user. -They appear in their profile in the back office. - -![Notification in profile](notification3.png "Profile notification bell menu") +To send notification to other channels, see [Notification channels](notification_channels.md). ## Notification bars Notifications are displayed as a message bar in the back office. There are four types of notifications: `info`, `success`, `warning` and `error`. -### Displaying notifications from PHP +![Screenshot of a notification bar](notification2.png "Example of notification bar") + +### Display notification bar from PHP To send a notification from PHP, inject the `TranslatableNotificationHandlerInterface` into your class. @@ -37,7 +36,7 @@ $this->notificationHandler->info( To have the notification translated, provide the message strings in the translation files under the correct domain and key. -### Displaying notifications from front end +### Display notification bar from front end To create a notification from the front end (in this example, of type `info`), use the following code: @@ -52,21 +51,48 @@ const eventInfo = new CustomEvent('ibexa-notify', { Dispatch the event with `document.body.dispatchEvent(eventInfo);`. -## Create custom notifications +### Notification bar timeout + +To define the timeout for hiding Back-Office notification bars, per notification type, use the `ibexa.system..notifications..timeout` [configuration key](configuration.md#configuration-files): + +``` yaml +ibexa: + system: + admin: + notifications: + error: + timeout: 0 + warning: + timeout: 0 + success: + timeout: 5000 + info: + timeout: 0 +``` + +The values shown above are the defaults. +`0` means the notification doesn't hide automatically. + +## User notifications -You can send your own custom notifications to the user which are displayed in the user menu. +You can send notifications to the user which are displayed in the user menu. -To create a new notification you can use the [core `NotificationService::createNotification(CreateStruct $createStruct)` method](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-NotificationService.html#method_createNotification) like in the example below. +![Screenshot of the user menu with an highlight on the bell icon](notification3.png "Profile notification bell menu") -Example: +### Create a custom user notification + +To create a new notification you can use the [`NotificationService::createNotification(CreateStruct $createStruct)` method](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-NotificationService.html#method_createNotification) +like in the example below: ```php [[= include_file('code_samples/back_office/notifications/src/EventListener/ContentPublishEventListener.php') =]] ``` -### Display single notification +A new type of user notification is created: `ContentPublished` + +### Display a custom user notification -To display a single notification, write a renderer and tag it as a service. +To display a user notification, write a renderer and tag it as a service. The example below presents a renderer that uses Twig to render a view: @@ -74,13 +100,15 @@ The example below presents a renderer that uses Twig to render a view: [[= include_file('code_samples/back_office/notifications/src/Notification/MyRenderer.php') =]] ``` -You can add the template that is defined above in the `render()` method to one of your custom bundles: +You can add the template that is used in the `MyRenderer::render()` method to the `admin` theme +as `templates/themes/admin/notification.html.twig`: ```html+twig [[= include_file('code_samples/back_office/notifications/templates/themes/admin/notification.html.twig') =]] ``` -Finally, you need to add an entry to `config/services.yaml`: +Finally, you need to add an entry to `config/services.yaml` +to tag and bound the renderer service to the `ContentPublished` type: ``` yaml [[= include_file('code_samples/back_office/notifications/config/custom_services.yaml') =]] @@ -96,30 +124,8 @@ The example below presents a modified renderer that uses Twig to render a list v [[= include_file('code_samples/back_office/notifications/src/Notification/ListRenderer.php') =]] ``` -## Notification timeout - -To define the timeout for hiding Back-Office notification bars, per notification type, use the `ibexa.system..notifications..timeout` [configuration key](configuration.md#configuration-files): - -``` yaml -ibexa: - system: - admin: - notifications: - error: - timeout: 0 - warning: - timeout: 0 - success: - timeout: 5000 - info: - timeout: 0 -``` - -The values shown above are the defaults. -`0` means the notification doesn't hide automatically. - -## `ibexa` notification channel +### `ibexa` notification channel -You can also subscribe to a notification with the `ibexa` channel. +To send user notifications, you can also subscribe to a notification with the `ibexa` channel. For more information, see [Notifications to channels](notification_channels.md). From 0731c44d0062c6ba845558cecbe59accd26ad1ab Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Wed, 6 May 2026 12:21:06 +0200 Subject: [PATCH 37/57] =?UTF-8?q?Update=20#create-custom-notifications=20?= =?UTF-8?q?=E2=86=92=20#user-notifications?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/api/event_reference/other_events.md | 2 +- docs/users/notification_channels.md | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/api/event_reference/other_events.md b/docs/api/event_reference/other_events.md index 7216c52f7a..792c2ed781 100644 --- a/docs/api/event_reference/other_events.md +++ b/docs/api/event_reference/other_events.md @@ -19,7 +19,7 @@ The following events are dispatched when adding content items to bookmarks. ## Notifications -The following events refer to [notifications displayed in the user menu](notifications.md#create-custom-notifications). +The following events refer to [notifications displayed in the user menu](notifications.md#user-notifications). | Event | Dispatched by | Properties | |---|---|---| diff --git a/docs/users/notification_channels.md b/docs/users/notification_channels.md index 62ace9da1c..c912e00fe9 100644 --- a/docs/users/notification_channels.md +++ b/docs/users/notification_channels.md @@ -7,15 +7,15 @@ month_change: true The `ibexa/notifications` package integrates the [Symfony Notifier]([[= symfony_doc =]]/notifier.html) with [[= product_name =]]. 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](notifications.md#create-custom-notifications). +and the [back office user notifications](notifications.md#user-notifications). -These notifications must not be confused with the [notification bars](notifications.md#notification-bars) or the [user notifications](notifications.md#create-custom-notifications): +These notifications must not be confused with the [notification bars](notifications.md#notification-bars) or the [user notifications](notifications.md#user-notifications): -| Notification family | Package | Service | Description | -|--------------------------------------------------------------------|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------| -| [notification bars](notifications.md#notification-bars) | `ibexa/admin-ui` | [`TranslatableNotificationHandlerInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-AdminUi-Notification-TranslatableNotificationHandlerInterface.html) | Rendered as a message bar in the bottom-right corner. | -| [user notifications](notifications.md#create-custom-notifications) | `ibexa/core` | [`NotificationService`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-NotificationService.html) | Rendered as a message in the back office bell menu. | -| [Channel subscribable notification](#subscribe-to-notifications) | `ibexa/notifications` | [`NotificationServiceInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-Service-NotificationServiceInterface.html) | Render depends on the channels subscribing to the notification type. | +| Notification family | Package | Service | Description | +|------------------------------------------------------------------|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------| +| [notification bars](notifications.md#notification-bars) | `ibexa/admin-ui` | [`TranslatableNotificationHandlerInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-AdminUi-Notification-TranslatableNotificationHandlerInterface.html) | Rendered as a message bar in the bottom-right corner. | +| [user notifications](notifications.md#user-notifications) | `ibexa/core` | [`NotificationService`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-NotificationService.html) | Rendered as a message in the back office bell menu. | +| [Channel subscribable notification](#subscribe-to-notifications) | `ibexa/notifications` | [`NotificationServiceInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-Service-NotificationServiceInterface.html) | Render depends on the channels subscribing 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. @@ -23,7 +23,7 @@ Several channels are provided, and you can create your own. The [`Ibexa\Contracts\Notifications\Service\NotificationServiceInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-Service-NotificationServiceInterface.html) 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. +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. @@ -81,7 +81,7 @@ This page contains several examples of subscriptions configuration. #### Subscription example -The following example shows how you can deliver notifications about Commerce-related activities through Slack: +The following example shows how you can deliver notifications about Commerce-related activities through Slack: 1. Install the Slack Notifier package: @@ -103,7 +103,7 @@ For example, define the following configuration in a new `config/packages/notifi ## 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. +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. @@ -122,7 +122,7 @@ and can optionally implement interfaces required by specific channels. The [`SystemNotificationInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-SystemNotification-SystemNotificationInterface.html) is not part of Symfony and has its own namespace. -The `ibexa` channel sends notifications to users through their profile menu, exactly as [user notifications](notifications.md#create-custom-notifications). +The `ibexa` channel sends notifications to users through their profile menu, exactly as [user notifications](notifications.md#user-notifications). The [`SystemNotificationChannel` uses the core `NotificationService`](https://github.com/ibexa/notifications/blob/v5.0.6/src/lib/SystemNotification/SystemNotificationChannel.php#L51) to do so. Some channels don't need a recipient: From 187caf1c0fbe0163672977cfd0d47ac65d8c9daf Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Wed, 6 May 2026 13:38:01 +0200 Subject: [PATCH 38/57] notifications.md: About `browser` notification channel --- docs/administration/back_office/notifications.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/administration/back_office/notifications.md b/docs/administration/back_office/notifications.md index a5ba153042..b711834981 100644 --- a/docs/administration/back_office/notifications.md +++ b/docs/administration/back_office/notifications.md @@ -73,6 +73,12 @@ ibexa: The values shown above are the defaults. `0` means the notification doesn't hide automatically. +### `browser` notification channel + +To send notification bars, you can also subscribe to a notification with the `browser` channel. + +For more information, see [Notifications to channels](notification_channels.md). + ## User notifications You can send notifications to the user which are displayed in the user menu. From 8084cff773090fdef8dafb7428bf2e16abe3aa2d Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Wed, 6 May 2026 13:39:38 +0200 Subject: [PATCH 39/57] Fix broken code blocks --- docs/users/notification_channels.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/users/notification_channels.md b/docs/users/notification_channels.md index c912e00fe9..45fa5fb1d1 100644 --- a/docs/users/notification_channels.md +++ b/docs/users/notification_channels.md @@ -100,6 +100,7 @@ For example, define the following configuration in a new `config/packages/notifi ``` yaml hl_lines="12-20" [[= include_code('code_samples/user_management/notifications/config/packages/notifications.yaml', 1, 20) =]] +``` ## Create a notification class @@ -187,6 +188,7 @@ As constructor arguments, an instance takes the command itself, the exit code of 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. +``` php [[= include_code('code_samples/user_management/notifications/src/Command/NotificationSenderCommand.php') =]] ``` From b970254d6ba324c69a4152e494c98b3482af548f Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Wed, 6 May 2026 14:01:50 +0200 Subject: [PATCH 40/57] Simplify CommandExecuted notif content `document.getElementsByClassName('ibexa-notifications-modal__count')[0].innerText='(2)'; document.getElementsByClassName('ibexa-header-user-menu__notice-dot')[0].dataset.count=2;` --- .../src/Notifications/CommandExecuted.php | 7 +------ docs/users/img/notification-ibexa.png | Bin 37065 -> 41825 bytes 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/code_samples/user_management/notifications/src/Notifications/CommandExecuted.php b/code_samples/user_management/notifications/src/Notifications/CommandExecuted.php index ac20a127e6..6f01028477 100644 --- a/code_samples/user_management/notifications/src/Notifications/CommandExecuted.php +++ b/code_samples/user_management/notifications/src/Notifications/CommandExecuted.php @@ -42,16 +42,11 @@ public function asEmailMessage(EmailRecipientInterface $recipient, ?string $tran public function asSystemNotification(UserRecipientInterface $recipient, ?string $transport = null): ?SystemMessage { - $errorCount = 'No error'; - if (count($this->exceptions) > 0) { - $plural = count($this->exceptions) > 1 ? 's' : ''; - $errorCount = count($this->exceptions) . ' error' . $plural; - } $message = new SystemMessage($recipient->getUser()); $message->setContext([ 'icon' => Command::SUCCESS === $this->exitCode ? 'check-circle' : 'discard-circle', 'subject' => $this->command->getName(), - 'content' => $errorCount, + 'content' => 'Number of errors: ' . count($this->exceptions), ]); return $message; diff --git a/docs/users/img/notification-ibexa.png b/docs/users/img/notification-ibexa.png index 1528635c9c6430012c1223ebd97abc4ba022f5fe..a0fd778c1a4c56e38d458cdffeb88310ad879b80 100644 GIT binary patch literal 41825 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

{H+N$q?_dp1tf~PnMFYIh)->~v*E87C$4RT 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 Y@PkMS17Ig3BuA2C|bYGo~_ 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 vOD2|CyvzN^|IWAoRu+f)p=9- Date: Wed, 6 May 2026 14:11:00 +0200 Subject: [PATCH 41/57] notification_channels.md: Rework channel/notif'interface table --- docs/users/notification_channels.md | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/docs/users/notification_channels.md b/docs/users/notification_channels.md index 45fa5fb1d1..dcfedb1729 100644 --- a/docs/users/notification_channels.md +++ b/docs/users/notification_channels.md @@ -108,18 +108,19 @@ You can define a new notification type and assign a new set of channels to it, c 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. They have a checkmark in the ⚠ column below. -- Some channels accept every notification and have a default behavior if the notification doesn't implement their related notification interface. - -| Channel | Notification interface | ⚠ | -|:----------|:-------------------------------|---------------------------------------------------------------------------------------| -| `actito` | `EmailNotificationInterface` | ✔ | -| `chat` | `ChatNotificationInterface` | | -| `desktop` | `DesktopNotificationInterface` | | -| `email` | `EmailNotificationInterface` | ✔ | -| `ibexa` | `SystemNotificationInterface` | ✔ | -| `push` | `PushNotificationInterface` | | -| `sms` | `SmsNotificationInterface` | ✔ | +- 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` | `EmailNotificationInterface` | **No** | +| `chat` | `ChatNotificationInterface` | Yes | +| `desktop` | `DesktopNotificationInterface` | Yes | +| `email` | `EmailNotificationInterface` | **No** | +| `ibexa` | `SystemNotificationInterface` | **No** | +| `push` | `PushNotificationInterface` | Yes | +| `sms` | `SmsNotificationInterface` | **No** | The [`SystemNotificationInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-SystemNotification-SystemNotificationInterface.html) is not part of Symfony and has its own namespace. From fdf97b7b05b7e91062b69261769ca890a7a9af77 Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Wed, 6 May 2026 14:30:50 +0200 Subject: [PATCH 42/57] notification_channels.md: Move notification sending early example to code_samples/ --- .../notifications/notification_send.php | 16 ++++++++++++++++ docs/users/notification_channels.md | 15 ++------------- phpstan-baseline.neon | 12 ++++++++++++ 3 files changed, 30 insertions(+), 13 deletions(-) create mode 100644 code_samples/user_management/notifications/notification_send.php diff --git a/code_samples/user_management/notifications/notification_send.php b/code_samples/user_management/notifications/notification_send.php new file mode 100644 index 0000000000..c0d4b42169 --- /dev/null +++ b/code_samples/user_management/notifications/notification_send.php @@ -0,0 +1,16 @@ +send( + new SymfonyNotificationAdapter(new MyNotification($subject)), + [new SymfonyRecipientAdapter(new UserRecipient($userService->loadUser($permissionResolver->getCurrentUserReference()->getUserId())))], +); diff --git a/docs/users/notification_channels.md b/docs/users/notification_channels.md index dcfedb1729..c00c4cedba 100644 --- a/docs/users/notification_channels.md +++ b/docs/users/notification_channels.md @@ -151,19 +151,8 @@ The [`…\Service\NotificationServiceInterface::send()`](/api/php_api/php_api_re For example, to send a notification, you often use a combination like the following: -```php hl_lines="8-11" -use App\Notifications\MyNotification; // Extends Symfony\Component\Notifier\Notification\Notification -use Ibexa\Contracts\Core\Repository\PermissionResolver; -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; -//… -$this->notificationService->send( - new SymfonyNotificationAdapter(new MyNotification($subject)), - [new SymfonyRecipientAdapter(new UserRecipient($this->permissionResolver->getCurrentUserReference()))], -); -//… +```php hl_lines="11-14" +[[= include_code('code_samples/user_management/notifications/notification_send.php', 2) =]] ``` ### `CommandExecuted` example diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 144c093655..114b6f840d 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -684,6 +684,18 @@ parameters: count: 1 path: code_samples/tutorials/page_tutorial_starting_point/src/QueryType/MenuQueryType.php + - + message: '#^Instantiated class App\\Notifications\\MyNotification not found\.$#' + identifier: class.notFound + count: 1 + path: code_samples/user_management/notifications/notification_send.php + + - + message: '#^Parameter \#1 \$notification of class Ibexa\\Contracts\\Notifications\\Value\\Notification\\SymfonyNotificationAdapter constructor expects Symfony\\Component\\Notifier\\Notification\\Notification, App\\Notifications\\MyNotification given\.$#' + identifier: argument.type + count: 1 + path: code_samples/user_management/notifications/notification_send.php + - message: '#^Parameter \#2 \$email of method Ibexa\\Contracts\\OAuth2Client\\Repository\\OAuth2UserService\:\:newOAuth2UserCreateStruct\(\) expects string, string\|null given\.$#' identifier: argument.type From a3a5bb251a6c27102146cd02788e98cbe94ac7cc Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Wed, 6 May 2026 14:47:54 +0200 Subject: [PATCH 43/57] Apply suggestions from code review Co-authored-by: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> --- docs/users/notification_channels.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/users/notification_channels.md b/docs/users/notification_channels.md index c00c4cedba..b3fe4300e5 100644 --- a/docs/users/notification_channels.md +++ b/docs/users/notification_channels.md @@ -54,11 +54,11 @@ php bin/console debug:container --tag=notifier.channel ``` * `actito` - Notification forwarded as [transactional email](transactional_emails.md) -* `browser` - Notification forwarded as flash message +* `browser` - Notification forwarded as [flash message]([[= symfony_doc =]]/session.html#flash-messages) * [`chat`]([[= symfony_doc =]]/notifier.html#chat-channel) - Notification forwarded to a communication platform like Slack, Microsoft Teams, or Google Chat -* [`desktop`]([[= symfony_doc =]]/notifier.html#chat-channel) - Notification forwarded to desktop applications like JoliNotif -* `email` - Notification forwarded to email addresses -* `ibexa` - Notification forwarded to back office user profiles +* [`desktop`]([[= symfony_doc =]]/notifier.html#desktop-channel) - Notification forwarded to desktop applications like JoliNotif +* [`email`]([[= symfony_doc =]]/notifier.html#email-channel) - Notification forwarded to email addresses +* `ibexa` - Notification forwarded as [back office user notifications](notifications.md#user-notifications) * [`push`]([[= symfony_doc =]]/notifier.html#push-channel) - Notification forwarded to specific applications * [`sms`]([[= symfony_doc =]]/notifier.html#sms-channel) - Notification forwarded to phone numbers From 5154c1324fda0bbc47bebcaf35a7c381e40a5958 Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Wed, 6 May 2026 15:24:16 +0200 Subject: [PATCH 44/57] =?UTF-8?q?Notification=20family=20=E2=86=92=20Notif?= =?UTF-8?q?ication=20category?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/users/notification_channels.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/users/notification_channels.md b/docs/users/notification_channels.md index b3fe4300e5..1959c59dc6 100644 --- a/docs/users/notification_channels.md +++ b/docs/users/notification_channels.md @@ -11,11 +11,11 @@ and the [back office user notifications](notifications.md#user-notifications). These notifications must not be confused with the [notification bars](notifications.md#notification-bars) or the [user notifications](notifications.md#user-notifications): -| Notification family | Package | Service | Description | -|------------------------------------------------------------------|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------| -| [notification bars](notifications.md#notification-bars) | `ibexa/admin-ui` | [`TranslatableNotificationHandlerInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-AdminUi-Notification-TranslatableNotificationHandlerInterface.html) | Rendered as a message bar in the bottom-right corner. | -| [user notifications](notifications.md#user-notifications) | `ibexa/core` | [`NotificationService`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-NotificationService.html) | Rendered as a message in the back office bell menu. | -| [Channel subscribable notification](#subscribe-to-notifications) | `ibexa/notifications` | [`NotificationServiceInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-Service-NotificationServiceInterface.html) | Render depends on the channels subscribing to the notification type. | +| Notification category | Sent with | Description | +|------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------| +| [Notification bars](notifications.md#notification-bars) | [`TranslatableNotificationHandlerInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-AdminUi-Notification-TranslatableNotificationHandlerInterface.html) | Rendered as a message bar in the bottom-right corner. | +| [User notifications](notifications.md#user-notifications) | [`NotificationService`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-NotificationService.html) | Rendered as [back office notification](https://doc.ibexa.co/projects/userguide/en/latest/getting_started/notifications/) | +| [Channel subscribable notification](#subscribe-to-notifications) | [`NotificationServiceInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-Service-NotificationServiceInterface.html) | Render depends on the channels subscribing 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. From 7611cbe35cc07fb1ecc7276d7c99bb5f2fc5c9a9 Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Wed, 6 May 2026 15:29:49 +0200 Subject: [PATCH 45/57] =?UTF-8?q?Channel=20subscribable=20notification=20?= =?UTF-8?q?=E2=86=92=20Channel-based=20notifications?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/users/notification_channels.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/users/notification_channels.md b/docs/users/notification_channels.md index 1959c59dc6..d8ec059bed 100644 --- a/docs/users/notification_channels.md +++ b/docs/users/notification_channels.md @@ -11,11 +11,11 @@ and the [back office user notifications](notifications.md#user-notifications). These notifications must not be confused with the [notification bars](notifications.md#notification-bars) or the [user notifications](notifications.md#user-notifications): -| Notification category | Sent with | Description | -|------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------| -| [Notification bars](notifications.md#notification-bars) | [`TranslatableNotificationHandlerInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-AdminUi-Notification-TranslatableNotificationHandlerInterface.html) | Rendered as a message bar in the bottom-right corner. | -| [User notifications](notifications.md#user-notifications) | [`NotificationService`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-NotificationService.html) | Rendered as [back office notification](https://doc.ibexa.co/projects/userguide/en/latest/getting_started/notifications/) | -| [Channel subscribable notification](#subscribe-to-notifications) | [`NotificationServiceInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-Service-NotificationServiceInterface.html) | Render depends on the channels subscribing to the notification type. | +| Notification category | Sent with | Description | +|------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------| +| [Notification bars](notifications.md#notification-bars) | [`TranslatableNotificationHandlerInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-AdminUi-Notification-TranslatableNotificationHandlerInterface.html) | Rendered as a message bar in the bottom-right corner. | +| [User notifications](notifications.md#user-notifications) | [`NotificationService`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-NotificationService.html) | Rendered as [back office notification]([[= user_doc =]]/getting_started/notifications/). | +| [Channel-based notifications](#subscribe-to-notifications) | [`NotificationServiceInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-Service-NotificationServiceInterface.html) | 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. From 0e8e0668f5126b3e6c02568af1b848b25d3c733b Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Wed, 6 May 2026 16:01:45 +0200 Subject: [PATCH 46/57] FQCN in Channel/Interface table --- docs/users/notification_channels.md | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/docs/users/notification_channels.md b/docs/users/notification_channels.md index d8ec059bed..cdacd69cf2 100644 --- a/docs/users/notification_channels.md +++ b/docs/users/notification_channels.md @@ -112,20 +112,18 @@ and can optionally implement interfaces required by specific channels. 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` | `EmailNotificationInterface` | **No** | -| `chat` | `ChatNotificationInterface` | Yes | -| `desktop` | `DesktopNotificationInterface` | Yes | -| `email` | `EmailNotificationInterface` | **No** | -| `ibexa` | `SystemNotificationInterface` | **No** | -| `push` | `PushNotificationInterface` | Yes | -| `sms` | `SmsNotificationInterface` | **No** | - -The [`SystemNotificationInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-SystemNotification-SystemNotificationInterface.html) is not part of Symfony and has its own namespace. +| 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`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-SystemNotification-SystemNotificationInterface.html) | **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](notifications.md#user-notifications). -The [`SystemNotificationChannel` uses the core `NotificationService`](https://github.com/ibexa/notifications/blob/v5.0.6/src/lib/SystemNotification/SystemNotificationChannel.php#L51) to do so. +The [`SystemNotificationChannel` uses the core `NotificationService`](https://github.com/ibexa/notifications/blob/v5.0.7/src/lib/SystemNotification/SystemNotificationChannel.php#L51) to do so. Some channels don't need a recipient: From 70038edd33d0b737099af2597667c150e1158546 Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Wed, 6 May 2026 16:13:30 +0200 Subject: [PATCH 47/57] mkdocs.yml: Move "Notification channels" (easy) --- mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index dfe97a82ab..e42a4e1480 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -106,6 +106,7 @@ nav: - Collaboration events: api/event_reference/collaboration_events.md - Integrated help events: api/event_reference/integrated_help_events.md - Other events: api/event_reference/other_events.md + - Notification channels: users/notification_channels.md - Administration: - Administration: administration/administration.md - Project organization: @@ -494,7 +495,6 @@ nav: - User grouping: - Customer groups: users/customer_groups.md - Segment API: users/segment_api.md - - Notification channels: users/notification_channels.md - Personalization: - Personalization: personalization/personalization.md - Personalization guide : personalization/personalization_guide.md From eef6daa6d0d3448bfe49974ba8552e5d53729354 Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Wed, 6 May 2026 16:17:22 +0200 Subject: [PATCH 48/57] mkdocs.yml: Move "Notification channels" (normal) --- docs/{users => api}/notification_channels.md | 0 mkdocs.yml | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename docs/{users => api}/notification_channels.md (100%) diff --git a/docs/users/notification_channels.md b/docs/api/notification_channels.md similarity index 100% rename from docs/users/notification_channels.md rename to docs/api/notification_channels.md diff --git a/mkdocs.yml b/mkdocs.yml index e42a4e1480..ca8e299822 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -106,7 +106,7 @@ nav: - Collaboration events: api/event_reference/collaboration_events.md - Integrated help events: api/event_reference/integrated_help_events.md - Other events: api/event_reference/other_events.md - - Notification channels: users/notification_channels.md + - Notification channels: api/notification_channels.md - Administration: - Administration: administration/administration.md - Project organization: From 58a4e2aef28cfd8cfdaddaaf5ebac2af4e35e86b Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Wed, 6 May 2026 16:23:36 +0200 Subject: [PATCH 49/57] mkdocs.yml: Move "Notification channels" (hard) --- .../assets/scss/notifications.scss | 0 .../config/packages/notifications.yaml | 0 .../notifications/config/services.yaml | 0 .../notifications/notification_send.php | 0 .../src/Command/NotificationSenderCommand.php | 4 +-- .../NotificationSenderController.php | 4 +-- .../src/Notifications/CommandExecuted.php | 2 +- .../src/Notifications/ControllerFeedback.php | 2 +- .../src/Notifier/Channel/LogChannel.php | 2 +- .../notification-sender-controller.html.twig | 0 .../notification-sender-controller.html.twig | 0 .../notifications/webpack.config.js | 0 docs/api/notification_channels.md | 36 +++++++++---------- 13 files changed, 25 insertions(+), 25 deletions(-) rename code_samples/{user_management => api}/notifications/assets/scss/notifications.scss (100%) rename code_samples/{user_management => api}/notifications/config/packages/notifications.yaml (100%) rename code_samples/{user_management => api}/notifications/config/services.yaml (100%) rename code_samples/{user_management => api}/notifications/notification_send.php (100%) rename code_samples/{user_management => api}/notifications/src/Command/NotificationSenderCommand.php (95%) rename code_samples/{user_management => api}/notifications/src/Controller/NotificationSenderController.php (88%) rename code_samples/{user_management => api}/notifications/src/Notifications/CommandExecuted.php (97%) rename code_samples/{user_management => api}/notifications/src/Notifications/ControllerFeedback.php (73%) rename code_samples/{user_management => api}/notifications/src/Notifier/Channel/LogChannel.php (94%) rename code_samples/{user_management => api}/notifications/templates/themes/admin/notification-sender-controller.html.twig (100%) rename code_samples/{user_management => api}/notifications/templates/themes/storefront/notification-sender-controller.html.twig (100%) rename code_samples/{user_management => api}/notifications/webpack.config.js (100%) diff --git a/code_samples/user_management/notifications/assets/scss/notifications.scss b/code_samples/api/notifications/assets/scss/notifications.scss similarity index 100% rename from code_samples/user_management/notifications/assets/scss/notifications.scss rename to code_samples/api/notifications/assets/scss/notifications.scss diff --git a/code_samples/user_management/notifications/config/packages/notifications.yaml b/code_samples/api/notifications/config/packages/notifications.yaml similarity index 100% rename from code_samples/user_management/notifications/config/packages/notifications.yaml rename to code_samples/api/notifications/config/packages/notifications.yaml diff --git a/code_samples/user_management/notifications/config/services.yaml b/code_samples/api/notifications/config/services.yaml similarity index 100% rename from code_samples/user_management/notifications/config/services.yaml rename to code_samples/api/notifications/config/services.yaml diff --git a/code_samples/user_management/notifications/notification_send.php b/code_samples/api/notifications/notification_send.php similarity index 100% rename from code_samples/user_management/notifications/notification_send.php rename to code_samples/api/notifications/notification_send.php diff --git a/code_samples/user_management/notifications/src/Command/NotificationSenderCommand.php b/code_samples/api/notifications/src/Command/NotificationSenderCommand.php similarity index 95% rename from code_samples/user_management/notifications/src/Command/NotificationSenderCommand.php rename to code_samples/api/notifications/src/Command/NotificationSenderCommand.php index ddd75f2e24..9138e05e26 100644 --- a/code_samples/user_management/notifications/src/Command/NotificationSenderCommand.php +++ b/code_samples/api/notifications/src/Command/NotificationSenderCommand.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace App\Command; +namespace App\api\notifications\src\Command; -use App\Notifications\CommandExecuted; +use App\api\notifications\src\Notifications\CommandExecuted; use Ibexa\Contracts\Core\Repository\UserService; use Ibexa\Contracts\Notifications\Service\NotificationServiceInterface; use Ibexa\Contracts\Notifications\Value\Notification\SymfonyNotificationAdapter; diff --git a/code_samples/user_management/notifications/src/Controller/NotificationSenderController.php b/code_samples/api/notifications/src/Controller/NotificationSenderController.php similarity index 88% rename from code_samples/user_management/notifications/src/Controller/NotificationSenderController.php rename to code_samples/api/notifications/src/Controller/NotificationSenderController.php index 2f8ee32037..4847694821 100644 --- a/code_samples/user_management/notifications/src/Controller/NotificationSenderController.php +++ b/code_samples/api/notifications/src/Controller/NotificationSenderController.php @@ -1,8 +1,8 @@ Date: Wed, 6 May 2026 16:28:49 +0200 Subject: [PATCH 50/57] mkdocs.yml: Move "Notification channels" (nightmare) --- .../notifications/src/Command/NotificationSenderCommand.php | 4 ++-- .../src/Controller/NotificationSenderController.php | 4 ++-- .../api/notifications/src/Notifications/CommandExecuted.php | 2 +- .../notifications/src/Notifications/ControllerFeedback.php | 2 +- .../api/notifications/src/Notifier/Channel/LogChannel.php | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/code_samples/api/notifications/src/Command/NotificationSenderCommand.php b/code_samples/api/notifications/src/Command/NotificationSenderCommand.php index 9138e05e26..ddd75f2e24 100644 --- a/code_samples/api/notifications/src/Command/NotificationSenderCommand.php +++ b/code_samples/api/notifications/src/Command/NotificationSenderCommand.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace App\api\notifications\src\Command; +namespace App\Command; -use App\api\notifications\src\Notifications\CommandExecuted; +use App\Notifications\CommandExecuted; use Ibexa\Contracts\Core\Repository\UserService; use Ibexa\Contracts\Notifications\Service\NotificationServiceInterface; use Ibexa\Contracts\Notifications\Value\Notification\SymfonyNotificationAdapter; diff --git a/code_samples/api/notifications/src/Controller/NotificationSenderController.php b/code_samples/api/notifications/src/Controller/NotificationSenderController.php index 4847694821..2f8ee32037 100644 --- a/code_samples/api/notifications/src/Controller/NotificationSenderController.php +++ b/code_samples/api/notifications/src/Controller/NotificationSenderController.php @@ -1,8 +1,8 @@ Date: Wed, 13 May 2026 09:18:40 +0200 Subject: [PATCH 51/57] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Marek Nocoń --- docs/administration/back_office/notifications.md | 10 +++++----- docs/api/notification_channels.md | 14 +++++++------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/administration/back_office/notifications.md b/docs/administration/back_office/notifications.md index b711834981..84ac4be9f8 100644 --- a/docs/administration/back_office/notifications.md +++ b/docs/administration/back_office/notifications.md @@ -5,7 +5,7 @@ month_change: false # Notifications -You can send two types of notifications to the users. +You can send two types of notifications to the users: - [Notification bar](#notification-bars) is displayed in specific situations as a message bar appearing at the bottom of the page. It appears to whoever is doing a specific operation in the back office. @@ -77,11 +77,11 @@ The values shown above are the defaults. To send notification bars, you can also subscribe to a notification with the `browser` channel. -For more information, see [Notifications to channels](notification_channels.md). +For more information, see [Notifications channels](notification_channels.md). ## User notifications -You can send notifications to the user which are displayed in the user menu. +You can send notifications to users which are displayed in the user menu. ![Screenshot of the user menu with an highlight on the bell icon](notification3.png "Profile notification bell menu") @@ -94,7 +94,7 @@ like in the example below: [[= include_file('code_samples/back_office/notifications/src/EventListener/ContentPublishEventListener.php') =]] ``` -A new type of user notification is created: `ContentPublished` +A new type of user notification is created: `ContentPublished`. ### Display a custom user notification @@ -134,4 +134,4 @@ The example below presents a modified renderer that uses Twig to render a list v To send user notifications, you can also subscribe to a notification with the `ibexa` channel. -For more information, see [Notifications to channels](notification_channels.md). +For more information, see [Notifications channels](notification_channels.md). diff --git a/docs/api/notification_channels.md b/docs/api/notification_channels.md index b951a78eb9..30145b8f1b 100644 --- a/docs/api/notification_channels.md +++ b/docs/api/notification_channels.md @@ -72,7 +72,7 @@ This page contains several examples of subscriptions configuration. 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, + To ensure you don't unsubscribe channels by mistake, always use the following command to check subscriptions for a siteaccess before and after any changes: ```bash @@ -108,11 +108,11 @@ You can define a new notification type and assign a new set of channels to it, c 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. +- Some channels don't accept the notification if it doesn't implement their specific notification interface. + These 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 specific notification interface. -| Channel | Specific notification interface | Accept any notification | +| Channel | Specific notification interface | Accepts any notification object| |:----------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------| | `actito` | `Symfony\Component\Notifier\Notification\EmailNotificationInterface` | **No** | | `chat` | `Symfony\Component\Notifier\Notification\ChatNotificationInterface` | Yes | @@ -256,11 +256,11 @@ Subscribe to this new notification type in `config/packages/notifications.yaml`: 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: +Visiting this controller's route in the back office (at `/admin/notification-sender`) triggers the notification as a flash message in the bottom-right corner: ![Notification in back office](notification-browser-admin.png "Controller message displayed as a flash message in the browser") -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: +Visiting the controller's route 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](notification-browser-storefront.png "Controller message displayed as a flash message in the browser") From 0ed506d09333cf547c1529599ad50f1046efb5f8 Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Wed, 13 May 2026 09:28:08 +0200 Subject: [PATCH 52/57] Fix vale [Ibexa.FutureTense] --- docs/api/notification_channels.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/notification_channels.md b/docs/api/notification_channels.md index 30145b8f1b..3c8567a14e 100644 --- a/docs/api/notification_channels.md +++ b/docs/api/notification_channels.md @@ -215,7 +215,7 @@ An empty template only extending the pagelayout is used for the demonstration. 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. +For this example, the style is 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 From b8dfa4d22e8fff6b9c212501fe611bf3a1c7e7a9 Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Wed, 13 May 2026 09:37:35 +0200 Subject: [PATCH 53/57] Empty commit From 0a4db32cdb9e39aba9b0d9ff7958cb1f5dbc2a6f Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Wed, 13 May 2026 10:02:20 +0200 Subject: [PATCH 54/57] Format notification_channels.md --- docs/api/notification_channels.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/api/notification_channels.md b/docs/api/notification_channels.md index 3c8567a14e..24589b40bd 100644 --- a/docs/api/notification_channels.md +++ b/docs/api/notification_channels.md @@ -112,15 +112,15 @@ and can optionally implement interfaces required by specific channels. These 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 specific notification interface. -| Channel | Specific notification interface | Accepts any notification object| -|:----------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------| -| `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`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-SystemNotification-SystemNotificationInterface.html) | **No** | -| `push` | `Symfony\Component\Notifier\Notification\PushNotificationInterface` | Yes | -| `sms` | `Symfony\Component\Notifier\Notification\SmsNotificationInterface` | **No** | +| Channel | Specific notification interface | Accepts any notification object | +|:----------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------| +| `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`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-SystemNotification-SystemNotificationInterface.html) | **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](notifications.md#user-notifications). The [`SystemNotificationChannel` uses the core `NotificationService`](https://github.com/ibexa/notifications/blob/v5.0.7/src/lib/SystemNotification/SystemNotificationChannel.php#L51) to do so. @@ -293,6 +293,6 @@ The log contains the notifications ```console % 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":""} [] +[2026-03-26T01:01:23.888014+01:00] app.INFO: ✖app:send_notification {"class":"App\\Notifications\\CommandExecuted","importance":"high","content":""} [] +[2026-03-27T01:02:54.123431+01:00] app.INFO: ✔app:send_notification {"class":"App\\Notifications\\CommandExecuted","importance":"low","content":""} [] ``` From d12055f9860080acb715b72c0b9b8b8395012655 Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Wed, 13 May 2026 10:10:22 +0200 Subject: [PATCH 55/57] Fix vale [Ibexa.ArticlesInHeadings] --- docs/api/notification_channels.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api/notification_channels.md b/docs/api/notification_channels.md index 24589b40bd..951de544a1 100644 --- a/docs/api/notification_channels.md +++ b/docs/api/notification_channels.md @@ -102,7 +102,7 @@ For example, define the following configuration in a new `config/packages/notifi [[= include_code('code_samples/api/notifications/config/packages/notifications.yaml', 1, 20) =]] ``` -## Create a notification class +## Create 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 @@ -265,7 +265,7 @@ Visiting the controller's route in the default SiteAccess on Commerce edition (a ![Notification in storefront](notification-browser-storefront.png "Controller message displayed as a flash message in the browser") -## Create a custom channel +## Create 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. From f25faeba5d8509819b69f115e18f3b38c41a90bb Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Wed, 13 May 2026 10:30:34 +0200 Subject: [PATCH 56/57] notification_channels.md: Fix ordered lists --- docs/api/notification_channels.md | 46 +++++++++++++++---------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/docs/api/notification_channels.md b/docs/api/notification_channels.md index 951de544a1..b1dd8addcb 100644 --- a/docs/api/notification_channels.md +++ b/docs/api/notification_channels.md @@ -85,22 +85,22 @@ The following example shows how you can deliver notifications about Commerce-rel 1. Install the Slack Notifier package: -```bash -composer require symfony/slack-notifier -``` + ```bash + composer require symfony/slack-notifier + ``` 2. In a .env file, [set the DSN to target a Slack channel or a Slack user](https://github.com/symfony/slack-notifier?tab=readme-ov-file#dsn-example): -```dotenv -SLACK_DSN=slack://xoxb-token@default?channel=ibexa-notifications -``` + ```dotenv + SLACK_DSN=slack://xoxb-token@default?channel=ibexa-notifications + ``` 3. 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: + For example, define the following configuration in a new `config/packages/notifications.yaml` file: -``` yaml hl_lines="12-20" -[[= include_code('code_samples/api/notifications/config/packages/notifications.yaml', 1, 20) =]] -``` + ``` yaml hl_lines="12-20" + [[= include_code('code_samples/api/notifications/config/packages/notifications.yaml', 1, 20, indent_level=1) =]] + ``` ## Create notification class @@ -159,26 +159,26 @@ The following example is a command that sends a notification to users on several 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. + 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. -``` php -[[= include_code('code_samples/api/notifications/src/Notifications/CommandExecuted.php') =]] -``` + ``` php + [[= include_code('code_samples/api/notifications/src/Notifications/CommandExecuted.php', indent_level=1) =]] + ``` 2. Assign channels subscribed to this notification in `config/packages/notifications.yaml`: -``` yaml hl_lines="17-20" -[[= include_code('code_samples/api/notifications/config/packages/notifications.yaml', 5, 24) =]] -``` + ``` yaml hl_lines="17-20" + [[= include_code('code_samples/api/notifications/config/packages/notifications.yaml', 5, 24, indent_level=1) =]] + ``` 3. 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. + 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. -``` php -[[= include_code('code_samples/api/notifications/src/Command/NotificationSenderCommand.php') =]] -``` + ``` php + [[= include_code('code_samples/api/notifications/src/Command/NotificationSenderCommand.php', indent_level=1) =]] + ``` When you execute this command, it fails randomly and notifies the Administrator user about the result. From 65a4bddbd1de7aeb7b07ab256510222f383a0d5e Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Wed, 13 May 2026 10:40:34 +0200 Subject: [PATCH 57/57] notification_channels.md: Fix ordered lists --- docs/api/notification_channels.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/api/notification_channels.md b/docs/api/notification_channels.md index b1dd8addcb..d73ccf31b4 100644 --- a/docs/api/notification_channels.md +++ b/docs/api/notification_channels.md @@ -96,7 +96,7 @@ The following example shows how you can deliver notifications about Commerce-rel ``` 3. 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: + For example, define the following configuration in a new `config/packages/notifications.yaml` file: ``` yaml hl_lines="12-20" [[= include_code('code_samples/api/notifications/config/packages/notifications.yaml', 1, 20, indent_level=1) =]] @@ -159,8 +159,8 @@ The following example is a command that sends a notification to users on several 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. + 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. ``` php [[= include_code('code_samples/api/notifications/src/Notifications/CommandExecuted.php', indent_level=1) =]]