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/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/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..489cce61bb --- /dev/null +++ b/code_samples/user_management/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' => Command::SUCCESS === $this->exitCode ? 'No error' : count($this->exceptions) . ' error' . (count($this->exceptions) > 1 ? 's' : ''), + ]); + + return $message; + } +} 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..3bf3bda913 --- /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/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. 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. diff --git a/docs/users/notifications.md b/docs/users/notifications.md new file mode 100644 index 0000000000..fb2e10864a --- /dev/null +++ b/docs/users/notifications.md @@ -0,0 +1,131 @@ +--- +description: Notify users TODO. +month_change: true +--- + +# 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](/administration/back_office/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)). + +TODO: Introduce the [`Ibexa\Contracts\Notifications\Service\NotificationServiceInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-Service-NotificationServiceInterface.html) + +## Subscribe to notifications + +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` + +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 + +A new notification class can be created to send a new type of message to a new set of channels. +It must extend `Symfony\Component\Notifier\Notification\Notification` +and optionally implements some interfaces depending on the channels it could be sent to. + +- Some channels don't accept the notification if it doesn't implement its related notification interface. +- Some channels accept every notification and have a default behavior if the notification doesn't implement their related notification interface. + +TODO: List what type of channel notification interfaces can be implemented +TODO: Namespaces, Ibexa custom vs Symfony native + +| Channel | Notification interface | ! | Description | +|:--------|:----------------------------------------------------------------------------------------------------------------------------------------------------------|---|:------------| +| `chat` | `ChatNotificationInterface` | | TODO | +| `email` | `EmailNotificationInterface` | ✔ | TODO | +| `ibexa` | [`SystemNotificationInterface`](/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 + +TODO: How to deal with channels not needing a user like `chat` + Slack channel? + +TODO: About `SymfonyNotificationAdapter` and `SymfonyRecipientAdapter` + +### Example + +The following example is a command that sends a notification to users on several channels simultaneously. +it could be a scheduled task, a cronjob, warning users about its final result. + +First, a `CommandExecuted` notification type is created. +It is supported by two channels for the example but could be extended to more. + +``` 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`: + +``` 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 + +``` 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') =]] +``` 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