Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
89296c0
Draft ibexa/notifications doc
adriendupuis Mar 17, 2026
0e7ca02
install_with_ddev.md: Mailer
adriendupuis Mar 17, 2026
a137ac0
PHP & JS CS Fixes
adriendupuis Mar 17, 2026
c3936c5
BO notifications.md: Minor fixes
adriendupuis Mar 17, 2026
43b3df8
Users notifications.md: Continue
adriendupuis Mar 17, 2026
96f9146
CommandExecuted enhancement
adriendupuis Mar 17, 2026
ffe904d
Continue users/notifications.md; Add channel examples
adriendupuis Mar 17, 2026
0d9abdc
PHP & JS CS Fixes
adriendupuis Mar 17, 2026
a76baed
Merge branch '5.0' into notifications
adriendupuis Mar 18, 2026
233029e
users/notifications.md → users/notification_channels.md
adriendupuis Mar 18, 2026
f5dd2d9
Merge remote-tracking branch 'origin/notifications' into notifications
adriendupuis Mar 18, 2026
7aa8f3a
LogChannel: $this->logger isn't guaranteed
adriendupuis Mar 18, 2026
c79653c
Merge branch '5.0' into notifications
adriendupuis Mar 26, 2026
0840840
NotificationSenderCommand: Fix namespace
adriendupuis Mar 26, 2026
54254ab
Continue notification_channels.md
adriendupuis Mar 27, 2026
5f6bc24
PHP & JS CS Fixes
adriendupuis Mar 27, 2026
a4818cd
About `SymfonyNotificationAdapter` and `SymfonyRecipientAdapter`
adriendupuis Mar 30, 2026
6965996
Extends ControllerFeedback example to storefront
adriendupuis Mar 30, 2026
29b09c6
Apply Rector suggestions
adriendupuis Mar 30, 2026
9c186a7
About subscriptions scopes overrides
adriendupuis Mar 30, 2026
fad8c6a
About subscriptions scopes overrides
adriendupuis Mar 30, 2026
91d8af7
About subscriptions scopes overrides
adriendupuis Mar 31, 2026
251d463
About subscriptions scopes overrides
adriendupuis Mar 31, 2026
2a748a5
About subscriptions scopes overrides
adriendupuis Mar 31, 2026
fb3774a
Merge branch '5.0' into notifications
adriendupuis Mar 31, 2026
1161d9f
CommandExecuted: Apply SonarCloud suggestion
adriendupuis Mar 31, 2026
64edb65
notification_channels.md: Minor addition
adriendupuis Mar 31, 2026
f463636
Apply suggestions from code review
adriendupuis Mar 31, 2026
b7ef37e
notification_channels.md: Apply some Copilot suggestions
adriendupuis Mar 31, 2026
818ae24
notification_channels.md: Apply some Copilot suggestions
adriendupuis Mar 31, 2026
0ebcb95
notification_channels.md: Apply some Copilot suggestions
adriendupuis Apr 1, 2026
3714c67
Apply suggestions from code review
adriendupuis Apr 8, 2026
836f178
Merge branch '5.0' into notifications
adriendupuis Apr 30, 2026
28c9166
include_file → include_code
adriendupuis Apr 30, 2026
1ebca76
Apply suggestion from @adriendupuis
adriendupuis Apr 30, 2026
8ae31a2
Apply suggestions from code review
adriendupuis Apr 30, 2026
0a1a950
Apply suggestions from code review
adriendupuis Apr 30, 2026
2216bec
Apply suggestion from @adriendupuis
adriendupuis Apr 30, 2026
07c2cad
Fix notification_channels.md command examples
adriendupuis Apr 30, 2026
6c097a8
Merge remote-tracking branch 'origin/notifications' into notifications
adriendupuis Apr 30, 2026
f37a25f
Apply suggestions from code review
adriendupuis May 6, 2026
b6211b1
back_office/notifications.md: Rework ToC
adriendupuis May 6, 2026
0731c44
Update #create-custom-notifications → #user-notifications
adriendupuis May 6, 2026
187caf1
notifications.md: About `browser` notification channel
adriendupuis May 6, 2026
8084cff
Fix broken code blocks
adriendupuis May 6, 2026
b970254
Simplify CommandExecuted notif content
adriendupuis May 6, 2026
f08d1da
notification_channels.md: Rework channel/notif'interface table
adriendupuis May 6, 2026
fdf97b7
notification_channels.md: Move notification sending early example to …
adriendupuis May 6, 2026
a3a5bb2
Apply suggestions from code review
adriendupuis May 6, 2026
5154c13
Notification family → Notification category
adriendupuis May 6, 2026
7611cbe
Channel subscribable notification → Channel-based notifications
adriendupuis May 6, 2026
0e8e066
FQCN in Channel/Interface table
adriendupuis May 6, 2026
70038ed
mkdocs.yml: Move "Notification channels" (easy)
adriendupuis May 6, 2026
eef6daa
mkdocs.yml: Move "Notification channels" (normal)
adriendupuis May 6, 2026
58a4e2a
mkdocs.yml: Move "Notification channels" (hard)
adriendupuis May 6, 2026
23fd359
mkdocs.yml: Move "Notification channels" (nightmare)
adriendupuis May 6, 2026
3857901
Merge branch '5.0' into notifications
adriendupuis May 6, 2026
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
7 changes: 7 additions & 0 deletions code_samples/api/notifications/assets/scss/notifications.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
@use '@ibexa-admin-ui/src/bundle/Resources/public/scss/_alerts.scss' as *;

.ibexa-alert {
&--notification {
@extend .ibexa-alert--info;
}
}
69 changes: 69 additions & 0 deletions code_samples/api/notifications/config/packages/notifications.yaml
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions code_samples/api/notifications/config/services.yaml
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' }
16 changes: 16 additions & 0 deletions code_samples/api/notifications/notification_send.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php declare(strict_types=1);

use App\Notifications\MyNotification; // extends Symfony\Component\Notifier\Notification\Notification
use Ibexa\Contracts\Notifications\Value\Notification\SymfonyNotificationAdapter;
use Ibexa\Contracts\Notifications\Value\Recipent\SymfonyRecipientAdapter;
use Ibexa\Contracts\Notifications\Value\Recipent\UserRecipient;

$subject = 'My subject';

/** @var \Ibexa\Contracts\Notifications\Service\NotificationServiceInterface $notificationService */
/** @var \Ibexa\Contracts\Core\Repository\UserService $userService */
/** @var \Ibexa\Contracts\Core\Repository\PermissionResolver $permissionResolver */
$notificationService->send(
new SymfonyNotificationAdapter(new MyNotification($subject)),
[new SymfonyRecipientAdapter(new UserRecipient($userService->loadUser($permissionResolver->getCurrentUserReference()->getUserId())))],
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

declare(strict_types=1);

namespace App\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 (random_int(0, 1) == 1) {
throw new \RuntimeException('Something went wrong');

Check warning on line 39 in code_samples/api/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,28 @@
<?php declare(strict_types=1);

namespace App\Controller;

use App\Notifications\ControllerFeedback;
use Ibexa\Contracts\Notifications\Service\NotificationServiceInterface;
use Ibexa\Contracts\Notifications\Value\Notification\SymfonyNotificationAdapter;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

class NotificationSenderController extends AbstractController
{
public function __construct(
private readonly NotificationServiceInterface $notificationService,
) {
}

#[Route('/notification-sender')]
public function index(): Response
{
$this->notificationService->send(
new SymfonyNotificationAdapter((new ControllerFeedback('Message sent from controller'))->emoji('👍')),
);

return $this->render('@ibexadesign/notification-sender-controller.html.twig');
}
}
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' => 'Number of errors: ' . count($this->exceptions),
]);

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

namespace App\Notifications;

use Symfony\Component\Notifier\Notification\Notification;

class ControllerFeedback extends Notification
{
}
30 changes: 30 additions & 0 deletions code_samples/api/notifications/src/Notifier/Channel/LogChannel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php declare(strict_types=1);

namespace App\Notifier\Channel;

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

class LogChannel implements ChannelInterface, LoggerAwareInterface
{
use LoggerAwareTrait;

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

public function supports(Notification $notification, RecipientInterface $recipient): bool
{
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{% extends '@ibexadesign/ui/layout.html.twig' %}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{% extends '@ibexadesign/storefront/layout.html.twig' %}
62 changes: 62 additions & 0 deletions code_samples/api/notifications/webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
const fs = require('fs');

Check warning on line 1 in code_samples/api/notifications/webpack.config.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `node:fs` over `fs`.

See more on https://sonarcloud.io/project/issues?id=ezsystems_developer-documentation&issues=AZ0wAGJxd3ELMRzxQl9o&open=AZ0wAGJxd3ELMRzxQl9o&pullRequest=3090
const path = require('path');

Check warning on line 2 in code_samples/api/notifications/webpack.config.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `node:path` over `path`.

See more on https://sonarcloud.io/project/issues?id=ezsystems_developer-documentation&issues=AZ0wAGJxd3ELMRzxQl9p&open=AZ0wAGJxd3ELMRzxQl9p&pullRequest=3090
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];
Loading
Loading