diff --git a/src/bundle/Resources/config/services.yaml b/src/bundle/Resources/config/services.yaml index 346884c..9d9b19c 100644 --- a/src/bundle/Resources/config/services.yaml +++ b/src/bundle/Resources/config/services.yaml @@ -2,3 +2,4 @@ imports: - { resource: services/mappers.yaml } - { resource: services/papi.yaml } - { resource: services/subscription_resolver.yaml } + - { resource: services/system_notifications.yaml } diff --git a/src/bundle/Resources/config/services/system_notifications.yaml b/src/bundle/Resources/config/services/system_notifications.yaml new file mode 100644 index 0000000..a279826 --- /dev/null +++ b/src/bundle/Resources/config/services/system_notifications.yaml @@ -0,0 +1,16 @@ +services: + _defaults: + autowire: true + autoconfigure: true + public: false + + Ibexa\Notifications\SystemNotification\SystemNotificationChannel: + tags: + - name: notifier.channel + channel: ibexa + + Ibexa\Notifications\SystemNotification\SystemNotificationRenderer: + tags: + - name: ibexa.notification.renderer + alias: system + diff --git a/src/bundle/Resources/views/themes/admin/.gitkeep b/src/bundle/Resources/views/themes/admin/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/bundle/Resources/views/themes/admin/notification/system_notification.html.twig b/src/bundle/Resources/views/themes/admin/notification/system_notification.html.twig new file mode 100644 index 0000000..a605cc3 --- /dev/null +++ b/src/bundle/Resources/views/themes/admin/notification/system_notification.html.twig @@ -0,0 +1,26 @@ +{% extends '@ibexadesign/account/notifications/list_item.html.twig' %} + +{% trans_default_domain 'ibexa_notification' %} + +{% block icon %} + {% set icon = icon|default('info') %} + + + + + +{% endblock %} + +{% block notification_type %} + + {{ subject|default('') }} + +{% endblock %} + +{% block message %} + {% embed '@ibexadesign/ui/component/table/table_body_cell.html.twig' with { class: 'ibexa-notifications-modal__description' } %} + {% block content %} +

{{ content|default('') }}

+ {% endblock %} + {% endembed %} +{% endblock %} diff --git a/src/contracts/SystemNotification/SystemMessage.php b/src/contracts/SystemNotification/SystemMessage.php new file mode 100644 index 0000000..d65c831 --- /dev/null +++ b/src/contracts/SystemNotification/SystemMessage.php @@ -0,0 +1,97 @@ + */ + private array $context; + + private string $subject = ''; + + /** + * @param array $context + */ + public function __construct(UserReference $user, array $context = []) + { + $this->user = $user; + $this->context = $context; + } + + public function getUser(): UserReference + { + return $this->user; + } + + public function setUser(UserReference $user): void + { + $this->user = $user; + } + + public function getRecipientId(): ?string + { + return (string) $this->user->getUserId(); + } + + public function getSubject(): string + { + return $this->subject; + } + + public function setSubject(string $subject): void + { + $this->subject = $subject; + } + + public function getTransport(): ?string + { + return null; + } + + public function getOptions(): ?MessageOptionsInterface + { + return null; + } + + /** + * @return array + */ + public function getContext(): array + { + return $this->context; + } + + /** + * @param array $context + */ + public function setContext(array $context): void + { + $this->context = $context; + } + + public function getType(): string + { + return $this->type; + } + + public function setType(string $type): void + { + $this->type = $type; + } +} diff --git a/src/contracts/SystemNotification/SystemNotification.php b/src/contracts/SystemNotification/SystemNotification.php new file mode 100644 index 0000000..5793f5c --- /dev/null +++ b/src/contracts/SystemNotification/SystemNotification.php @@ -0,0 +1,64 @@ +icon; + } + + public function setIcon(?string $icon): void + { + $this->icon = $icon; + } + + public function getRoute(): ?RouteReference + { + return $this->route; + } + + public function setRoute(?RouteReference $route): void + { + $this->route = $route; + } + + public function setContent(string $content): void + { + $this->content($content); + } + + public function asSystemNotification(UserRecipientInterface $recipient, ?string $transport = null): SystemMessage + { + $context = [ + 'subject' => $this->getSubject(), + 'content' => $this->getContent(), + ]; + + if ($this->icon !== null) { + $context['icon'] = $this->icon; + } + + if ($this->route !== null) { + $context['route_name'] = $this->route->getRoute(); + $context['route_params'] = $this->route->getParams(); + } + + return new SystemMessage($recipient->getUser(), $context); + } +} diff --git a/src/contracts/SystemNotification/SystemNotificationInterface.php b/src/contracts/SystemNotification/SystemNotificationInterface.php new file mode 100644 index 0000000..e84dadb --- /dev/null +++ b/src/contracts/SystemNotification/SystemNotificationInterface.php @@ -0,0 +1,19 @@ +user->email; } + + public function getUser(): UserReference + { + return $this->user; + } } diff --git a/src/contracts/Value/Recipent/UserRecipientInterface.php b/src/contracts/Value/Recipent/UserRecipientInterface.php new file mode 100644 index 0000000..716a60a --- /dev/null +++ b/src/contracts/Value/Recipent/UserRecipientInterface.php @@ -0,0 +1,17 @@ +repository = $repository; + $this->notificationService = $notificationService; + } + + /** + * @param \Symfony\Component\Notifier\Notification\Notification&\Ibexa\Contracts\Notifications\SystemNotification\SystemNotificationInterface $notification + * @param \Ibexa\Contracts\Notifications\Value\Recipent\UserRecipientInterface $recipient + */ + public function notify(Notification $notification, RecipientInterface $recipient, ?string $transportName = null): void + { + $message = $notification->asSystemNotification($recipient, $transportName); + if ($message === null) { + return; + } + + $createStruct = new CreateStruct(); + $createStruct->ownerId = $message->getUser()->getUserId(); + $createStruct->type = $message->getType(); + $createStruct->data = $message->getContext(); + + $this->repository->beginTransaction(); + try { + $this->notificationService->createNotification($createStruct); + $this->repository->commit(); + } catch (Throwable $e) { + $this->repository->rollback(); + throw $e; + } + } + + public function supports(Notification $notification, RecipientInterface $recipient): bool + { + return $notification instanceof SystemNotificationInterface && $recipient instanceof UserRecipientInterface; + } +} diff --git a/src/lib/SystemNotification/SystemNotificationRenderer.php b/src/lib/SystemNotification/SystemNotificationRenderer.php new file mode 100644 index 0000000..c00712e --- /dev/null +++ b/src/lib/SystemNotification/SystemNotificationRenderer.php @@ -0,0 +1,59 @@ +twig = $twig; + $this->urlGenerator = $urlGenerator; + } + + public function render(Notification $notification): string + { + return $this->twig->render( + '@ibexadesign/notification/system_notification.html.twig', + [ + 'notification' => $notification, + 'icon' => $notification->data[self::KEY_ICON] ?? null, + 'content' => $notification->data[self::KEY_CONTENT] ?? null, + 'subject' => $notification->data[self::KEY_SUBJECT] ?? null, + 'created_at' => $notification->created, + ] + ); + } + + public function generateUrl(Notification $notification): ?string + { + if (!isset($notification->data[self::KEY_ROUTE_NAME])) { + return null; + } + + return $this->urlGenerator->generate( + $notification->data[self::KEY_ROUTE_NAME], + $notification->data[self::KEY_ROUTE_PARAMS] ?? [] + ); + } +} diff --git a/tests/integration/.gitkeep b/tests/integration/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tests/integration/Notifications/SystemNotification/SystemNotificationTest.php b/tests/integration/Notifications/SystemNotification/SystemNotificationTest.php new file mode 100644 index 0000000..db02764 --- /dev/null +++ b/tests/integration/Notifications/SystemNotification/SystemNotificationTest.php @@ -0,0 +1,47 @@ +setIcon('icon'); + $notification->setContent('It works!'); + $notification->setRoute(new RouteReference('example', ['foo' => 'bar', 'baz' => 'qux'])); + + $user = new UserReference(self::EXAMPLE_USER_ID); + + $recipient = $this->createMock(UserRecipientInterface::class); + $recipient->method('getUser')->willReturn($user); + + $systemMessage = $notification->asSystemNotification($recipient); + + self::assertNotNull($systemMessage); + self::assertSame($user, $systemMessage->getUser()); + self::assertEquals(SystemMessage::DEFAULT_TYPE, $systemMessage->getType()); + self::assertEquals([ + 'icon' => 'icon', + 'subject' => 'Hello World', + 'content' => 'It works!', + 'route_name' => 'example', + 'route_params' => ['foo' => 'bar', 'baz' => 'qux'], + ], $systemMessage->getContext()); + } +} diff --git a/tests/lib/Channel/SystemNotificationChannelTest.php b/tests/lib/Channel/SystemNotificationChannelTest.php new file mode 100644 index 0000000..ac257c9 --- /dev/null +++ b/tests/lib/Channel/SystemNotificationChannelTest.php @@ -0,0 +1,137 @@ +repository = $this->createMock(Repository::class); + $this->notificationService = $this->createMock(NotificationService::class); + + $this->channel = new SystemNotificationChannel($this->repository, $this->notificationService); + } + + /** + * @dataProvider dataProviderForTestSupports + */ + public function testSupports(Notification $notification, RecipientInterface $recipient, bool $expectedResult): void + { + self::assertEquals($expectedResult, $this->channel->supports($notification, $recipient)); + } + + /** + * @return iterable + */ + public function dataProviderForTestSupports(): iterable + { + yield 'supported' => [ + $this->createSupportedNotification(), + $this->createSupportedRecipient(), + true, + ]; + + yield 'unsupported recipient' => [ + $this->createSupportedNotification(), + $this->createMock(RecipientInterface::class), + false, + ]; + + yield 'unsupported notification' => [ + $this->createMock(Notification::class), + $this->createSupportedRecipient(), + false, + ]; + } + + public function testNotify(): void + { + $expectedCreateStruct = new CreateStruct(); + $expectedCreateStruct->ownerId = self::EXAMPLE_USER_ID; + $expectedCreateStruct->data = ['foo' => 'bar']; + $expectedCreateStruct->type = 'example'; + + $this->notificationService + ->expects(self::once()) + ->method('createNotification') + ->with($expectedCreateStruct) + ->willReturn(new SystemNotification()); + + $user = $this->createMock(UserReference::class); + $user->method('getUserId')->willReturn(self::EXAMPLE_USER_ID); + + $message = new SystemMessage($user, ['foo' => 'bar']); + $message->setType('example'); + + $this->channel->notify( + $this->createSupportedNotification($message), + $this->createSupportedRecipient(self::EXAMPLE_USER_ID) + ); + } + + /** + * @return \Symfony\Component\Notifier\Notification\Notification&\Ibexa\Contracts\Notifications\SystemNotification\SystemNotificationInterface + */ + private function createSupportedNotification(?SystemMessage $message = null): Notification + { + return new class($message) extends Notification implements SystemNotificationInterface { + private ?SystemMessage $message; + + public function __construct(?SystemMessage $message) + { + parent::__construct('example'); + + $this->message = $message; + } + + public function asSystemNotification( + UserRecipientInterface $recipient, + ?string $transport = null + ): ?SystemMessage { + return $this->message; + } + }; + } + + private function createSupportedRecipient(?int $userId = null): UserRecipientInterface + { + $recipient = $this->createMock(UserRecipientInterface::class); + if ($userId !== null) { + $user = $this->createMock(UserReference::class); + $user->method('getUserId')->willReturn($userId); + + $recipient->method('getUser')->willReturn($user); + } + + return $recipient; + } +}