Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
services:
App\Notifier\Channel\LogChannel:
tags:
- { name: 'notifier.channel', channel: 'log' }
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

declare(strict_types=1);

namespace App\src\Command;

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

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

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

try {
// Do something
if (rand(0, 1) == 1) {
throw new \RuntimeException('Something went wrong');

Check warning on line 39 in code_samples/user_management/notifications/src/Command/NotificationSenderCommand.php

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define and throw a dedicated exception instead of using a generic one.

See more on https://sonarcloud.io/project/issues?id=ezsystems_developer-documentation&issues=AZz7BK7JSbHSMhyKnsyM&open=AZz7BK7JSbHSMhyKnsyM&pullRequest=3090
}
$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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php declare(strict_types=1);

namespace App\Notifications;

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

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

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

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

return new EmailMessage($email);
}

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

Check warning on line 49 in code_samples/user_management/notifications/src/Notifications/CommandExecuted.php

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=ezsystems_developer-documentation&issues=AZz8SjXllynT5ak2mLm6&open=AZz8SjXllynT5ak2mLm6&pullRequest=3090
]);

return $message;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php declare(strict_types=1);

namespace App\Notifier\Channel;

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

class LogChannel implements ChannelInterface, LoggerAwareInterface
{
use LoggerAwareTrait;

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

public function supports(Notification $notification, RecipientInterface $recipient): bool
{
return true;
}
}
14 changes: 10 additions & 4 deletions docs/administration/back_office/notifications.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:

Expand Down Expand Up @@ -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.
8 changes: 8 additions & 0 deletions docs/getting_started/install_with_ddev.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
131 changes: 131 additions & 0 deletions docs/users/notifications.md
Original file line number Diff line number Diff line change
@@ -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` | &#10004; | TODO |
| `ibexa` | [`SystemNotificationInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-Notifications-SystemNotification-SystemNotificationInterface.html) | &#10004; | TODO |
| `sms` | `SmsNotificationInterface` | &#10004; | 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') =]]
```
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down