From 1eb79e1413ae6e65a7bacd8146fe9907f8d7cd5b Mon Sep 17 00:00:00 2001 From: bernard-ng Date: Fri, 1 Jul 2022 02:29:48 +0200 Subject: [PATCH] feat(notification): web push notification support --- config/domains/notification.yaml | 1 + .../SetAllNotificationsReadCommand.php | 3 +- .../Service/NotificationService.php | 14 ++++-- .../Notification/Entity/Notification.php | 2 +- .../Notification/Entity/PushSubscription.php | 1 + .../NotificationRepositoryInterface.php | 2 +- .../PushSubscriptionRepositoryInterface.php | 1 + .../ValueObject/PushSubscriptionKeys.php | 4 +- .../Repository/PushSubscriptionRepository.php | 41 +++++++++++------ .../Mercure/MercureEventSubscriber.php | 2 - .../Controller/NotificationController.php | 4 +- .../Controller/PushSubscriptionController.php | 21 ++------- .../Symfony/Twig/NotificationExtension.php | 8 ++-- .../Notification/WebPushService.php | 46 ++++++++++--------- .../Doctrine/Repository/ReportRepository.php | 30 ++++++------ .../Symfony/Twig/Sidebar/ManagerSidebar.php | 1 - .../Symfony/Twig/Sidebar/SidebarExtension.php | 2 +- 17 files changed, 95 insertions(+), 88 deletions(-) diff --git a/config/domains/notification.yaml b/config/domains/notification.yaml index af73e0d..71359df 100644 --- a/config/domains/notification.yaml +++ b/config/domains/notification.yaml @@ -7,3 +7,4 @@ services: # Doctrine Repositories Domain\Notification\Repository\NotificationRepositoryInterface: '@Infrastructure\Notification\Doctrine\Repository\NotificationRepository' + Domain\Notification\Repository\PushSubscriptionRepositoryInterface: '@Infrastructure\Notification\Doctrine\Repository\PushSubscriptionRepository' diff --git a/src/Application/Notification/Command/SetAllNotificationsReadCommand.php b/src/Application/Notification/Command/SetAllNotificationsReadCommand.php index d025a37..151131a 100644 --- a/src/Application/Notification/Command/SetAllNotificationsReadCommand.php +++ b/src/Application/Notification/Command/SetAllNotificationsReadCommand.php @@ -15,7 +15,6 @@ final class SetAllNotificationsReadCommand { public function __construct( public readonly User $user - ) - { + ) { } } diff --git a/src/Application/Notification/Service/NotificationService.php b/src/Application/Notification/Service/NotificationService.php index 8109e13..2de7de7 100644 --- a/src/Application/Notification/Service/NotificationService.php +++ b/src/Application/Notification/Service/NotificationService.php @@ -115,15 +115,15 @@ private function getHashForEntity(object $entity): string { $hash = $entity::class; if (method_exists($entity, 'getId')) { - $hash .= sprintf('::%s', (string)$entity->getId()); + $hash .= sprintf('::%s', (string) $entity->getId()); } return $hash; } - private function getUrlForEntityChannel(object $entity): ?string + private function getUrlForEntityChannel(object $entity): string { - return null; + return ''; } private function getUrlForEntityUser(object $entity, User $user): string @@ -132,8 +132,12 @@ private function getUrlForEntityUser(object $entity, User $user): string 'report_employee_report_show' : 'report_manager_report_show'; $parameters = match (true) { - $entity instanceof Evaluation => ['uuid' => $entity->getReport()->getUuid()], - $entity instanceof Report => ['uuid' => $entity->getUuid()], + $entity instanceof Evaluation => [ + 'uuid' => $entity->getReport()?->getUuid(), + ], + $entity instanceof Report => [ + 'uuid' => $entity->getUuid(), + ], default => [] }; diff --git a/src/Domain/Notification/Entity/Notification.php b/src/Domain/Notification/Entity/Notification.php index 6b32271..d9fd6a6 100644 --- a/src/Domain/Notification/Entity/Notification.php +++ b/src/Domain/Notification/Entity/Notification.php @@ -133,7 +133,7 @@ public function getIsRead(): bool return $this->is_read; } - public function setIsRead(?bool $is_read): self + public function setIsRead(bool $is_read): self { $this->is_read = $is_read; diff --git a/src/Domain/Notification/Entity/PushSubscription.php b/src/Domain/Notification/Entity/PushSubscription.php index 4980f13..e302c50 100644 --- a/src/Domain/Notification/Entity/PushSubscription.php +++ b/src/Domain/Notification/Entity/PushSubscription.php @@ -77,6 +77,7 @@ public function getUser(): ?User public function setUser(?User $user): self { $this->user = $user; + return $this; } } diff --git a/src/Domain/Notification/Repository/NotificationRepositoryInterface.php b/src/Domain/Notification/Repository/NotificationRepositoryInterface.php index cf59348..21b2299 100644 --- a/src/Domain/Notification/Repository/NotificationRepositoryInterface.php +++ b/src/Domain/Notification/Repository/NotificationRepositoryInterface.php @@ -22,5 +22,5 @@ public function findRecentForUser(User $user, array $channels = ['public']): arr public function countUnreadForUser(User $user): int; - public function setAllReadForUser(User $user); + public function setAllReadForUser(User $user): int; } diff --git a/src/Domain/Notification/Repository/PushSubscriptionRepositoryInterface.php b/src/Domain/Notification/Repository/PushSubscriptionRepositoryInterface.php index 96f82f0..e53c283 100644 --- a/src/Domain/Notification/Repository/PushSubscriptionRepositoryInterface.php +++ b/src/Domain/Notification/Repository/PushSubscriptionRepositoryInterface.php @@ -13,4 +13,5 @@ */ interface PushSubscriptionRepositoryInterface extends DataRepositoryInterface { + public function deleteSubscriptionByEndpoint(string $endpoint): int; } diff --git a/src/Domain/Notification/ValueObject/PushSubscriptionKeys.php b/src/Domain/Notification/ValueObject/PushSubscriptionKeys.php index 374b5ee..474e260 100644 --- a/src/Domain/Notification/ValueObject/PushSubscriptionKeys.php +++ b/src/Domain/Notification/ValueObject/PushSubscriptionKeys.php @@ -16,7 +16,7 @@ class PushSubscriptionKeys public readonly string $p256dh; public readonly string $auth; - private function __construct($p256dh, $auth) + private function __construct(string $p256dh, string $auth) { Assert::notEmpty($p256dh); Assert::notEmpty($auth); @@ -37,7 +37,7 @@ public function toArray(): array { return [ 'auth' => $this->auth, - 'p256dh' => $this->p256dh + 'p256dh' => $this->p256dh, ]; } } diff --git a/src/Infrastructure/Notification/Doctrine/Repository/PushSubscriptionRepository.php b/src/Infrastructure/Notification/Doctrine/Repository/PushSubscriptionRepository.php index 6f0ebb7..dfad8a6 100644 --- a/src/Infrastructure/Notification/Doctrine/Repository/PushSubscriptionRepository.php +++ b/src/Infrastructure/Notification/Doctrine/Repository/PushSubscriptionRepository.php @@ -21,24 +21,35 @@ public function __construct(ManagerRegistry $registry) parent::__construct($registry, PushSubscription::class); } - /** - * @param PushSubscription $entity - * @return void - */ public function save(object $entity): void { - $subscription = $this->findOneBy(['endpoint' => $entity->getEndpoint()]); - - if ($subscription) { - $subscription - ->setExpirationTime($entity->getExpirationTime()) - ->setKeys($entity->getKeys()) - ->setUpdatedAt($entity->getCreatedAt()); - } else { - $subscription = $entity; + if ($entity instanceof PushSubscription) { + /** @var PushSubscription|null $subscription */ + $subscription = $this->findOneBy([ + 'endpoint' => $entity->getEndpoint(), + ]); + + if ($subscription) { + $subscription + ->setExpirationTime($entity->getExpirationTime()) + ->setKeys($entity->getKeys()) + ->setUpdatedAt($entity->getCreatedAt()); + } else { + $subscription = $entity; + } + + $this->getEntityManager()->persist($subscription); + $this->getEntityManager()->flush(); } + } - $this->getEntityManager()->persist($subscription); - $this->getEntityManager()->flush(); + public function deleteSubscriptionByEndpoint(string $endpoint): int + { + return intval($this->createQueryBuilder('ps') + ->delete(PushSubscription::class, 'ps') + ->where('ps.endpoint = :endpoint') + ->setParameter('endpoint', $endpoint) + ->getQuery() + ->execute()); } } diff --git a/src/Infrastructure/Notification/Mercure/MercureEventSubscriber.php b/src/Infrastructure/Notification/Mercure/MercureEventSubscriber.php index fa5fe2f..1592338 100644 --- a/src/Infrastructure/Notification/Mercure/MercureEventSubscriber.php +++ b/src/Infrastructure/Notification/Mercure/MercureEventSubscriber.php @@ -7,7 +7,6 @@ use Domain\Authentication\Entity\User; use Domain\Notification\Event\NotificationCreatedEvent; use Domain\Notification\Event\NotificationReadEvent; -use Infrastructure\Notification\WebPushService; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Mercure\HubInterface; use Symfony\Component\Mercure\Update; @@ -21,7 +20,6 @@ final class MercureEventSubscriber implements EventSubscriberInterface { public function __construct( private readonly HubInterface $hub, - private readonly WebPushService $push ) { } diff --git a/src/Infrastructure/Notification/Symfony/Controller/NotificationController.php b/src/Infrastructure/Notification/Symfony/Controller/NotificationController.php index 8766d94..bc5fabd 100644 --- a/src/Infrastructure/Notification/Symfony/Controller/NotificationController.php +++ b/src/Infrastructure/Notification/Symfony/Controller/NotificationController.php @@ -39,7 +39,7 @@ public function index( target: $repository->findRecentForUser($user), page: $request->query->getInt('page', 1), limit: 20 - ) + ), ] ); } @@ -60,7 +60,7 @@ public function show(Notification $notification): Response $this->handleUnexpectedException($e); } - return $this->redirect($notification->getUrl(), status: Response::HTTP_PERMANENTLY_REDIRECT); + return $this->redirect((string) $notification->getUrl(), status: Response::HTTP_PERMANENTLY_REDIRECT); } #[Route('/set_as_read', name: 'read', methods: ['POST'])] diff --git a/src/Infrastructure/Notification/Symfony/Controller/PushSubscriptionController.php b/src/Infrastructure/Notification/Symfony/Controller/PushSubscriptionController.php index 464e7aa..e788eaf 100644 --- a/src/Infrastructure/Notification/Symfony/Controller/PushSubscriptionController.php +++ b/src/Infrastructure/Notification/Symfony/Controller/PushSubscriptionController.php @@ -5,10 +5,8 @@ namespace Infrastructure\Notification\Symfony\Controller; use Domain\Authentication\Entity\User; -use Domain\Notification\Entity\Notification; use Domain\Notification\Entity\PushSubscription; use Domain\Notification\Repository\PushSubscriptionRepositoryInterface; -use Infrastructure\Notification\WebPushService; use Infrastructure\Shared\Symfony\Controller\AbstractController; use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; use Symfony\Component\HttpFoundation\JsonResponse; @@ -30,28 +28,19 @@ public function subscribe(Request $request, PushSubscriptionRepositoryInterface /** @var User $user */ $user = $this->getUser(); $subscription = PushSubscription::fromArray( - data: json_decode($request->getContent(), associative: true), + data: (array) json_decode($request->getContent(), associative: true), user: $user ); $repository->save($subscription); + return new JsonResponse(status: Response::HTTP_CREATED); } #[Route('/notification/push/key', name: 'notification_push_key', methods: ['GET'])] public function key(): Response { - return new JsonResponse(['key' => $_ENV['VAPID_PUBLIC_KEY']]); - } - - #[Route('notification/ping')] - public function ping(WebPushService $service): Response - { - $notification = (new Notification()) - ->setMessage("Hello world") - ->setUrl("hello"); - - $service->notifyChannel($notification); - - return $this->redirectSeeOther('notification_index'); + return new JsonResponse([ + 'key' => $_ENV['VAPID_PUBLIC_KEY'], + ]); } } diff --git a/src/Infrastructure/Notification/Symfony/Twig/NotificationExtension.php b/src/Infrastructure/Notification/Symfony/Twig/NotificationExtension.php index 794048b..c373cc1 100644 --- a/src/Infrastructure/Notification/Symfony/Twig/NotificationExtension.php +++ b/src/Infrastructure/Notification/Symfony/Twig/NotificationExtension.php @@ -20,16 +20,15 @@ final class NotificationExtension extends AbstractExtension { public function __construct( private readonly NotificationService $notificationService, - private readonly Security $security - ) - { + private readonly Security $security + ) { } public function getFunctions(): array { return [ new TwigFunction('recent_notifications', [$this, 'notifications']), - new TwigFunction('count_notifications', [$this, 'count']) + new TwigFunction('count_notifications', [$this, 'count']), ]; } @@ -48,6 +47,7 @@ public function count(): int { /** @var User $user */ $user = $this->security->getUser(); + return $this->notificationService->countForUser($user); } } diff --git a/src/Infrastructure/Notification/WebPushService.php b/src/Infrastructure/Notification/WebPushService.php index e4ffe0d..6a3631e 100644 --- a/src/Infrastructure/Notification/WebPushService.php +++ b/src/Infrastructure/Notification/WebPushService.php @@ -7,7 +7,7 @@ use Domain\Authentication\Entity\User; use Domain\Notification\Entity\Notification; use Domain\Notification\Entity\PushSubscription; -use Domain\Notification\Repository\PushSubscriptionRepositoryInterface; +use Infrastructure\Notification\Doctrine\Repository\PushSubscriptionRepository; use Minishlink\WebPush\Subscription; use Minishlink\WebPush\WebPush; use Psr\Log\LoggerInterface; @@ -24,7 +24,7 @@ final class WebPushService private ?string $icon; public function __construct( - private readonly PushSubscriptionRepositoryInterface $repository, + private readonly PushSubscriptionRepository $repository, private readonly LoggerInterface $logger, RequestStack $stack ) { @@ -32,15 +32,15 @@ public function __construct( 'VAPID' => [ 'subject' => 'mailto:rapport@unhorizons.org', 'publicKey' => $_ENV['VAPID_PUBLIC_KEY'], - 'privateKey' => $_ENV['VAPID_PRIVATE_KEY'] - ] + 'privateKey' => $_ENV['VAPID_PRIVATE_KEY'], + ], ]); $this->icon = $stack->getCurrentRequest()?->getUriForPath('/images/logo_icon.png'); } public function notifyChannel(Notification $notification): void { - /** @var PushSubscription[] $subscription */ + /** @var PushSubscription[] $subscriptions */ $subscriptions = $this->repository->findAll(); try { @@ -49,11 +49,8 @@ public function notifyChannel(Notification $notification): void } foreach ($this->webPush->flush() as $report) { - if (!$report->isSuccess()) { - $this->repository->delete($subscription); - dd($report); - } else { - dd($report); + if (! $report->isSuccess()) { + $this->repository->deleteSubscriptionByEndpoint($report->getEndpoint()); } } } catch (\Throwable $e) { @@ -64,8 +61,10 @@ public function notifyChannel(Notification $notification): void public function notifyUser(Notification $notification, User $user): void { - /** @var PushSubscription[] $subscription */ - $subscriptions = $this->repository->findBy(['user' => $user]); + /** @var PushSubscription[] $subscriptions */ + $subscriptions = $this->repository->findBy([ + 'user' => $user, + ]); try { foreach ($subscriptions as $subscription) { @@ -73,8 +72,8 @@ public function notifyUser(Notification $notification, User $user): void } foreach ($this->webPush->flush() as $report) { - if (!$report->isSuccess()) { - $this->repository->delete($subscription); + if (! $report->isSuccess()) { + $this->repository->deleteSubscriptionByEndpoint($report->getEndpoint()); } } } catch (\Throwable $e) { @@ -90,25 +89,28 @@ private function push(PushSubscription $subscription, Notification $notification $this->webPush->queueNotification( subscription: Subscription::create([ 'endpoint' => $subscription->getEndpoint(), - 'publicKey' => $subscription->getKeys()->p256dh, - 'authToken' => $subscription->getKeys()->auth + 'publicKey' => $subscription->getKeys()?->p256dh, + 'authToken' => $subscription->getKeys()?->auth, ]), payload: json_encode([ 'title' => 'UNH Rapport', 'options' => [ 'body' => $notification->getMessage(), 'data' => [ - "url" => $notification->getUrl(), + 'url' => $notification->getUrl(), ], 'actions' => [ - ["action" => "show", "title" => "Voir les détails"] + [ + 'action' => 'show', + 'title' => 'Voir les détails', + ], ], - "icon" => $this->icon, + 'icon' => $this->icon, 'requireInteraction' => true, - 'timestamp' => $notification->getCreatedAt()->format('u'), - 'lang' => 'FR' + 'timestamp' => $notification->getCreatedAt()?->format('u'), + 'lang' => 'FR', ], - ]) + ]) ?: null ); } } diff --git a/src/Infrastructure/Report/Doctrine/Repository/ReportRepository.php b/src/Infrastructure/Report/Doctrine/Repository/ReportRepository.php index 563a1ef..a1b766a 100644 --- a/src/Infrastructure/Report/Doctrine/Repository/ReportRepository.php +++ b/src/Infrastructure/Report/Doctrine/Repository/ReportRepository.php @@ -297,6 +297,22 @@ public function findCurrentYearFrequencyForEmployee(User $employee): array ], false); } + /** + * @todo cache result for optimization + */ + public function countUnseenForManager(User $manager): int + { + $sql = <<execute($sql, [ + 'user' => $manager->getId(), + ], false)['count']; + } + private function findAllUnseenQuery(): QueryBuilder { return $this->createQueryBuilder('r') @@ -457,18 +473,4 @@ private function createMonthSumSQL(string $date): string SUM(MONTH({$date}) = 12) AS 'Dec' SQL; } - - /** - * @todo cache result for optimization - */ - public function countUnseenForManager(User $manager): int - { - $sql = <<execute($sql, ['user' => $manager->getId()], false)['count']; - } } diff --git a/src/Infrastructure/Report/Symfony/Twig/Sidebar/ManagerSidebar.php b/src/Infrastructure/Report/Symfony/Twig/Sidebar/ManagerSidebar.php index 92c4c51..65e75b6 100644 --- a/src/Infrastructure/Report/Symfony/Twig/Sidebar/ManagerSidebar.php +++ b/src/Infrastructure/Report/Symfony/Twig/Sidebar/ManagerSidebar.php @@ -4,7 +4,6 @@ namespace Infrastructure\Report\Symfony\Twig\Sidebar; -use Application\Notification\Service\NotificationService; use Domain\Authentication\Entity\User; use Domain\Notification\Repository\NotificationRepositoryInterface; use Domain\Report\Repository\ReportRepositoryInterface; diff --git a/src/Infrastructure/Shared/Symfony/Twig/Sidebar/SidebarExtension.php b/src/Infrastructure/Shared/Symfony/Twig/Sidebar/SidebarExtension.php index 04f25f2..c21501e 100644 --- a/src/Infrastructure/Shared/Symfony/Twig/Sidebar/SidebarExtension.php +++ b/src/Infrastructure/Shared/Symfony/Twig/Sidebar/SidebarExtension.php @@ -140,7 +140,7 @@ class="nk-menu-link nk-menu-toggle" return $s; } - throw new \RuntimeException(sprintf('The %s must be an instance of %s', $sidebar::class, AbstractSidebar::class)); + throw new \RuntimeException(sprintf('The sidebar must be an instance of %s', AbstractSidebar::class)); } /**