diff --git a/code_samples/api/notifications/assets/scss/notifications.scss b/code_samples/api/notifications/assets/scss/notifications.scss new file mode 100644 index 0000000000..70e58d2612 --- /dev/null +++ b/code_samples/api/notifications/assets/scss/notifications.scss @@ -0,0 +1,7 @@ +@use '@ibexa-admin-ui/src/bundle/Resources/public/scss/_alerts.scss' as *; + +.ibexa-alert { + &--notification { + @extend .ibexa-alert--info; + } +} diff --git a/code_samples/api/notifications/config/packages/notifications.yaml b/code_samples/api/notifications/config/packages/notifications.yaml new file mode 100644 index 0000000000..68473810a2 --- /dev/null +++ b/code_samples/api/notifications/config/packages/notifications.yaml @@ -0,0 +1,69 @@ +framework: + notifier: + chatter_transports: + slack: '%env(SLACK_DSN)%' +ibexa: + system: + default: + notifier: + subscriptions: + # The configuration below is added to the `default` scope without overriding the one defined in ibexa.yaml + # Custom subscriptions: + Ibexa\OrderManagement\Notification\OrderStatusChange: + channels: + - chat + Ibexa\Payment\Notification\PaymentStatusChange: + channels: + - chat + Ibexa\Shipping\Notification\ShipmentStatusChange: + channels: + - chat + App\Notifications\CommandExecuted: + channels: + - ibexa + - email + - log + admin_group: + notifier: + subscriptions: + # The configuration below is added to the `admin_group` scope without overriding the one defined in ibexa_admin_ui.yaml + # Custom subscriptions: + App\Notifications\CommandExecuted: + channels: + - ibexa + - email + - log + App\Notifications\ControllerFeedback: + channels: + - browser + storefront_group: + notifier: + subscriptions: + # The configuration defined in ibexa.yaml for `default` scope is repeated as the configuration below overrides it + Ibexa\Contracts\User\Notification\UserPasswordReset: + channels: + - email + Ibexa\Contracts\User\Notification\UserInvitation: + channels: + - email + Ibexa\Contracts\FormBuilder\Notifications\FormSubmitted: + channels: + - email + # Custom subscriptions: + Ibexa\OrderManagement\Notification\OrderStatusChange: + channels: + - chat + Ibexa\Payment\Notification\PaymentStatusChange: + channels: + - chat + Ibexa\Shipping\Notification\ShipmentStatusChange: + channels: + - chat + App\Notifications\CommandExecuted: + channels: + - ibexa + - email + - log + App\Notifications\ControllerFeedback: + channels: + - browser diff --git a/code_samples/api/notifications/config/services.yaml b/code_samples/api/notifications/config/services.yaml new file mode 100644 index 0000000000..51d97cdfbe --- /dev/null +++ b/code_samples/api/notifications/config/services.yaml @@ -0,0 +1,4 @@ +services: + App\Notifier\Channel\LogChannel: + tags: + - { name: 'notifier.channel', channel: 'log' } diff --git a/code_samples/api/notifications/notification_send.php b/code_samples/api/notifications/notification_send.php new file mode 100644 index 0000000000..c0d4b42169 --- /dev/null +++ b/code_samples/api/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/code_samples/api/notifications/src/Command/NotificationSenderCommand.php b/code_samples/api/notifications/src/Command/NotificationSenderCommand.php new file mode 100644 index 0000000000..ddd75f2e24 --- /dev/null +++ b/code_samples/api/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 (random_int(0, 1) == 1) { + throw new \RuntimeException('Something went wrong'); + } + $exitCode = Command::SUCCESS; + } catch (\Exception $exception) { + $exceptions[] = $exception; + $exitCode = Command::FAILURE; + } + + $recipients = []; + foreach ($this->recipientLogins as $login) { + try { + $user = $this->userService->loadUserByLogin($login); + $recipients[] = new UserRecipient($user); + } catch (\Exception $exception) { + $exceptions[] = $exception; + } + } + + $this->notificationService->send( + new SymfonyNotificationAdapter(new CommandExecuted($this, $exitCode, $exceptions)), + array_map( + static fn (RecipientInterface $recipient): SymfonyRecipientAdapter => new SymfonyRecipientAdapter($recipient), + $recipients + ) + ); + + return $exitCode; + } +} diff --git a/code_samples/api/notifications/src/Controller/NotificationSenderController.php b/code_samples/api/notifications/src/Controller/NotificationSenderController.php new file mode 100644 index 0000000000..2f8ee32037 --- /dev/null +++ b/code_samples/api/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/api/notifications/src/Notifications/CommandExecuted.php b/code_samples/api/notifications/src/Notifications/CommandExecuted.php new file mode 100644 index 0000000000..6f01028477 --- /dev/null +++ b/code_samples/api/notifications/src/Notifications/CommandExecuted.php @@ -0,0 +1,54 @@ + $exceptions */ + public function __construct( + private readonly Command $command, + private readonly int $exitCode, + private readonly array $exceptions + ) { + parent::__construct((Command::SUCCESS === $this->exitCode ? '✔' : '✖') . $this->command->getName()); + $this->importance(Command::SUCCESS === $this->exitCode ? Notification::IMPORTANCE_LOW : Notification::IMPORTANCE_HIGH); + } + + public function asEmailMessage(EmailRecipientInterface $recipient, ?string $transport = null): ?EmailMessage + { + $body = ''; + foreach ($this->exceptions as $exception) { + $body .= $exception->getMessage() . '
'; + } + + $email = NotificationEmail::asPublicEmail() + ->to($recipient->getEmail()) + ->subject($this->getSubject()) + ->html($body); + + return new EmailMessage($email); + } + + public function asSystemNotification(UserRecipientInterface $recipient, ?string $transport = null): ?SystemMessage + { + $message = new SystemMessage($recipient->getUser()); + $message->setContext([ + 'icon' => Command::SUCCESS === $this->exitCode ? 'check-circle' : 'discard-circle', + 'subject' => $this->command->getName(), + 'content' => 'Number of errors: ' . count($this->exceptions), + ]); + + return $message; + } +} diff --git a/code_samples/api/notifications/src/Notifications/ControllerFeedback.php b/code_samples/api/notifications/src/Notifications/ControllerFeedback.php new file mode 100644 index 0000000000..0c82a0be11 --- /dev/null +++ b/code_samples/api/notifications/src/Notifications/ControllerFeedback.php @@ -0,0 +1,9 @@ +logger)) { + $this->logger->info($notification->getSubject(), [ + 'class' => $notification::class, + 'importance' => $notification->getImportance(), + 'content' => $notification->getContent(), + ]); + } + } + + public function supports(Notification $notification, RecipientInterface $recipient): bool + { + return true; + } +} diff --git a/code_samples/api/notifications/templates/themes/admin/notification-sender-controller.html.twig b/code_samples/api/notifications/templates/themes/admin/notification-sender-controller.html.twig new file mode 100644 index 0000000000..563a43accd --- /dev/null +++ b/code_samples/api/notifications/templates/themes/admin/notification-sender-controller.html.twig @@ -0,0 +1 @@ +{% extends '@ibexadesign/ui/layout.html.twig' %} diff --git a/code_samples/api/notifications/templates/themes/storefront/notification-sender-controller.html.twig b/code_samples/api/notifications/templates/themes/storefront/notification-sender-controller.html.twig new file mode 100644 index 0000000000..89078dd236 --- /dev/null +++ b/code_samples/api/notifications/templates/themes/storefront/notification-sender-controller.html.twig @@ -0,0 +1 @@ +{% extends '@ibexadesign/storefront/layout.html.twig' %} diff --git a/code_samples/api/notifications/webpack.config.js b/code_samples/api/notifications/webpack.config.js new file mode 100644 index 0000000000..ddb7f0a1d0 --- /dev/null +++ b/code_samples/api/notifications/webpack.config.js @@ -0,0 +1,62 @@ +const fs = require('fs'); +const path = require('path'); +const Encore = require('@symfony/webpack-encore'); +const getWebpackConfigs = require('@ibexa/frontend-config/webpack-config/get-configs'); +const customConfigsPaths = require('./var/encore/ibexa.webpack.custom.config.js'); + +const customConfigs = getWebpackConfigs(Encore, customConfigsPaths); +const isReactBlockPathCreated = fs.existsSync('./assets/page-builder/react/blocks'); + +Encore.reset(); +Encore + .setOutputPath('public/build/') + .setPublicPath('/build') + .enableSassLoader() + .enableReactPreset((options) => { + 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/administration/back_office/notifications.md b/docs/administration/back_office/notifications.md index 29c2d69f54..b711834981 100644 --- a/docs/administration/back_office/notifications.md +++ b/docs/administration/back_office/notifications.md @@ -5,24 +5,23 @@ 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. +- [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 the 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) +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,54 @@ 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. + +### `browser` notification channel -You can send your own custom notifications to the user which are displayed in the user menu. +To send notification bars, you can also subscribe to a notification with the `browser` channel. -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`. +For more information, see [Notifications to channels](notification_channels.md). -Example: +## User notifications + +You can send notifications to the user 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") + +### 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 +106,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,24 +130,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 +### `ibexa` notification channel -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): +To send user notifications, you can also subscribe to a notification with the `ibexa` channel. -``` 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. +For more information, see [Notifications to channels](notification_channels.md). diff --git a/docs/administration/project_organization/bundles.md b/docs/administration/project_organization/bundles.md index 25a1e7ad06..7c8f789342 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/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/api/notification_channels.md b/docs/api/notification_channels.md new file mode 100644 index 0000000000..b951a78eb9 --- /dev/null +++ b/docs/api/notification_channels.md @@ -0,0 +1,298 @@ +--- +description: Notify users through several channels. +month_change: true +--- + +# Notification channels + +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#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]([[= 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. +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. +Channel services implementing `Symfony\Component\Notifier\Channel\ChannelInterface` subscribe to a selection of notification types +and deliver notifications to users through various transports. + +## Subscribe to notifications + +Some events generate notifications that you can deliver to the users through one or more channels. + +### Available notification types + +* [`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) +* `Ibexa\Share\Notification\ContentEditInvitationNotification` +* `Ibexa\Share\Notification\ContentViewInvitationNotification` +* `Ibexa\Share\Notification\ExternalParticipantContentViewInvitationNotification` + +### 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]([[= 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#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 + +### Subscriptions configuration + +You can find the default configuration in `config/packages/ibexa.yaml` and `config/packages/ibexa_admin_ui.yaml`. +You can modify it to define your own subscriptions. +This page contains several examples of subscriptions configuration. + +!!! caution "Scopes may not merge as expected" + + Subscriptions defined for a scope may not merge with subscriptions from other scopes or from other files. + For example, `default` scope might not be merged within a siteaccess group scope. + To ensure you don't unsubscribe against your will, + always use the following command to check subscriptions for a siteaccess before and after any changes: + + ```bash + php bin/console ibexa:debug:config notifications.subscriptions --siteaccess= + ``` + +#### Subscription example + +The following example shows how you can deliver notifications about Commerce-related activities through Slack: + +1. Install the Slack Notifier package: + +```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 +``` + +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/api/notifications/config/packages/notifications.yaml', 1, 20) =]] +``` + +## Create a notification class + +You can define a new notification type and assign a new set of channels to it, customizing how it's delivered. +It must extend the `Symfony\Component\Notifier\Notification\Notification` class +and can optionally implement interfaces required by specific channels. + +- Some channels don't accept the notification if it doesn't implement their related notification interface. + Those interfaces come with a method to specifically format the notification for the channel. +- Some channels accept every notification and have a default formatting if the notification doesn't implement their related notification interface. + +| Channel | Specific notification interface | Accept any notification | +|:----------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------| +| `actito` | `Symfony\Component\Notifier\Notification\EmailNotificationInterface` | **No** | +| `chat` | `Symfony\Component\Notifier\Notification\ChatNotificationInterface` | Yes | +| `desktop` | `Symfony\Component\Notifier\Notification\DesktopNotificationInterface` | Yes | +| `email` | `Symfony\Component\Notifier\Notification\EmailNotificationInterface` | **No** | +| `ibexa` | [`Ibexa\Contracts\Notifications\SystemNotification\SystemNotificationInterface`](/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. + +Some channels don't need a recipient: + +- `browser`: Always sends a flash message to the current user +- `chat`: Always sends a message to the same connection resource + +### Notification sending + +Use the objects from the [`Ibexa\Contracts\Notifications`](/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: + +- 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 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) + 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), + - 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 the following: + +```php hl_lines="11-14" +[[= include_code('code_samples/api/notifications/notification_send.php', 2) =]] +``` + +### `CommandExecuted` example + +The following example is a command that sends a notification to users on several channels simultaneously. +This example could be a scheduled task or cron job that warns users about its result. + +1. First, create a `CommandExecuted` notification type. +It supports two channels (`ibexa`, `email`), but could be extended to support more. +As constructor arguments, an instance takes the command itself, the exit code of the run, and any caught exceptions. + +``` php +[[= include_code('code_samples/api/notifications/src/Notifications/CommandExecuted.php') =]] +``` + +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) =]] +``` + +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/api/notifications/src/Command/NotificationSenderCommand.php') =]] +``` + +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") + +### `ControllerFeedback` example + +The following example shows a custom notification sent by a controller and displayed as a flash message on the corresponding page in the browser. + +The following `ControllerFeedback` notification type is a class that only extends the base: + +``` php +[[= include_code('code_samples/api/notifications/src/Notifications/ControllerFeedback.php') =]] +``` + +The `ControllerFeedback` notification is sent in a controller action: + +``` php +[[= include_code('code_samples/api/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. +An empty template only extending the pagelayout is used for the demonstration. + +`templates/themes/admin/notification-sender-controller.html.twig`: +``` twig +[[= include_code('code_samples/api/notifications/templates/themes/admin/notification-sender-controller.html.twig') =]] +``` + +`templates/themes/storefront/notification-sender-controller.html.twig`: +``` twig +[[= include_code('code_samples/api/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. +This doesn't have a default style. +For this example, the style will be the same as an existing alert message type. + +The `assets/scss/notifications.scss` declares the CSS class `ibexa-alert--notification` as being the same as the `ibexa-alert--info` CSS class + +``` scss +[[= include_code('code_samples/api/notifications/assets/scss/notifications.scss') =]] +``` + +This `assets/scss/notifications.scss` is added to the Admin UI layout in `webpack.config.js`: + +``` javascript +[[= include_code('code_samples/api/notifications/webpack.config.js', 50) =]] +``` + +On the storefront, a notification sent as a flash message has the `ibexa-store-notification--notification` CSS class. +This class already has a default style applied. + +Subscribe to this new notification type in `config/packages/notifications.yaml`: + +- In the `admin_group` scope with the `browser` channel +- For Commerce edition, in the `storefront_group` scope with the `browser` channel + +``` yaml hl_lines="13-15 43-45" +[[= include_code('code_samples/api/notifications/config/packages/notifications.yaml', 5, 6) =]] + # … +[[= include_code('code_samples/api/notifications/config/packages/notifications.yaml', 26, 34) =]] +[[= include_code('code_samples/api/notifications/config/packages/notifications.yaml', 36, 65) =]] +[[= include_code('code_samples/api/notifications/config/packages/notifications.yaml', 67) =]] +``` + +!!! note "Subscriptions for `storefront_group`" + + Note that when introducing subscriptions configuration for the `storefront_group` scope that comes with Commerce edition, + several subscriptions had to be copy-pasted into this SiteAccess group to have the same subscriptions as before + when it was configured only by the `default` scope. + For example, the subscriptions for the `site` SiteAccess belonging to this group + can be checked with the following command during configuration: + ```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: + +![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: + +![Notification in storefront](notification-browser-storefront.png "Controller message displayed as a flash message in the browser") + + +## Create a custom channel + +You may need to create new channels to subscribe to notifications and send them to new destinations. +For example, you could create a new channel for Slack that takes more than one DSN for finer dispatching. + +A channel is a service implementing `Symfony\Component\Notifier\Channel\ChannelInterface`, and tagged `notifier.channel` alongside a `channel` identifier. + +The following example is a custom channel that sends notifications to the logger. + +``` php +[[= include_code('code_samples/api/notifications/src/Notifier/Channel/LogChannel.php') =]] +``` + +``` yaml +[[= include_code('code_samples/api/notifications/config/services.yaml') =]] +``` + +Now, the [`CommandExecuted` notification](#commandexecuted-example) can be subscribed to the `log` channel: + +``` yaml hl_lines="5" +[[= include_code('code_samples/api/notifications/config/packages/notifications.yaml', 21, 25) =]] +``` + +The log contains the notifications +(in `var/log/dev.log` when run in the `dev` Symfony environment): + +```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":""} [] +``` diff --git a/docs/getting_started/install_with_ddev.md b/docs/getting_started/install_with_ddev.md index 6a890c54d9..f2af829f50 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://docs.ddev.com/en/stable/users/install/performance/#mutagen) to improve performance. diff --git a/docs/users/img/notification-browser-admin.png b/docs/users/img/notification-browser-admin.png new file mode 100644 index 0000000000..912237897c Binary files /dev/null and b/docs/users/img/notification-browser-admin.png differ diff --git a/docs/users/img/notification-browser-storefront.png b/docs/users/img/notification-browser-storefront.png new file mode 100644 index 0000000000..83ac41a37c Binary files /dev/null and b/docs/users/img/notification-browser-storefront.png differ diff --git a/docs/users/img/notification-ibexa.png b/docs/users/img/notification-ibexa.png new file mode 100644 index 0000000000..a0fd778c1a Binary files /dev/null and b/docs/users/img/notification-ibexa.png differ diff --git a/mkdocs.yml b/mkdocs.yml index a2082cc594..ca8e299822 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: api/notification_channels.md - Administration: - Administration: administration/administration.md - Project organization: diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 1b50172c7f..fb9caacfb0 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -678,6 +678,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