Notification: fix counter, and allow to add more related entity in a single query

Sometimes, there are entities which embed other entities, which in turn have notification. This more parameter allow to fetch notification and counter for those embedded entities in a single query.
This commit is contained in:
Julien Fastré 2023-06-13 23:03:15 +02:00
parent cb4de1f3d2
commit 3879e5cd9b
Signed by: julienfastre
GPG Key ID: BDE2190974723FCB
4 changed files with 105 additions and 42 deletions

View File

@ -34,9 +34,13 @@ class NotificationPresence
$this->notificationRepository = $notificationRepository; $this->notificationRepository = $notificationRepository;
} }
public function countNotificationsForClassAndEntity(string $relatedEntityClass, int $relatedEntityId): array /**
* @param list<array{relatedEntityClass: class-string, relatedEntityId: int}> $more
* @return array{unread: int, sent: int, total: int}
*/
public function countNotificationsForClassAndEntity(string $relatedEntityClass, int $relatedEntityId, array $more = [], array $options = []): array
{ {
if (array_key_exists($relatedEntityClass, $this->cache) && array_key_exists($relatedEntityId, $this->cache[$relatedEntityClass])) { if ([] === $more && array_key_exists($relatedEntityClass, $this->cache) && array_key_exists($relatedEntityId, $this->cache[$relatedEntityClass])) {
return $this->cache[$relatedEntityClass][$relatedEntityId]; return $this->cache[$relatedEntityClass][$relatedEntityId];
} }
@ -46,21 +50,25 @@ class NotificationPresence
$counter = $this->notificationRepository->countNotificationByRelatedEntityAndUserAssociated( $counter = $this->notificationRepository->countNotificationByRelatedEntityAndUserAssociated(
$relatedEntityClass, $relatedEntityClass,
$relatedEntityId, $relatedEntityId,
$user $user,
$more
); );
$this->cache[$relatedEntityClass][$relatedEntityId] = $counter; if ([] === $more) {
$this->cache[$relatedEntityClass][$relatedEntityId] = $counter;
}
return $counter; return $counter;
} }
return ['unread' => 0, 'read' => 0]; return ['unread' => 0, 'sent' => 0, 'total' => 0];
} }
/** /**
* @param list<array{relatedEntityClass: class-string, relatedEntityId: int}> $more
* @return array|Notification[] * @return array|Notification[]
*/ */
public function getNotificationsForClassAndEntity(string $relatedEntityClass, int $relatedEntityId): array public function getNotificationsForClassAndEntity(string $relatedEntityClass, int $relatedEntityId, array $more = []): array
{ {
$user = $this->security->getUser(); $user = $this->security->getUser();
@ -68,7 +76,8 @@ class NotificationPresence
return $this->notificationRepository->findNotificationByRelatedEntityAndUserAssociated( return $this->notificationRepository->findNotificationByRelatedEntityAndUserAssociated(
$relatedEntityClass, $relatedEntityClass,
$relatedEntityId, $relatedEntityId,
$user $user,
$more
); );
} }

View File

@ -34,24 +34,30 @@ class NotificationTwigExtensionRuntime implements RuntimeExtensionInterface
$this->urlGenerator = $urlGenerator; $this->urlGenerator = $urlGenerator;
} }
public function counterNotificationFor(Environment $environment, string $relatedEntityClass, int $relatedEntityId, array $options = []): string public function counterNotificationFor(Environment $environment, string $relatedEntityClass, int $relatedEntityId, array $more = [], array $options = []): string
{ {
return $environment->render( return $environment->render(
'@ChillMain/Notification/extension_counter_notifications_for.html.twig', '@ChillMain/Notification/extension_counter_notifications_for.html.twig',
[ [
'counter' => $this->notificationPresence->countNotificationsForClassAndEntity($relatedEntityClass, $relatedEntityId), 'counter' => $this->notificationPresence->countNotificationsForClassAndEntity($relatedEntityClass, $relatedEntityId, $more),
] ]
); );
} }
public function countNotificationsFor(string $relatedEntityClass, int $relatedEntityId, array $options = []): array /**
* @param list<array{relatedEntityClass: class-string, relatedEntityId: int}> $more
*/
public function countNotificationsFor(string $relatedEntityClass, int $relatedEntityId, array $more = [], array $options = []): array
{ {
return $this->notificationPresence->countNotificationsForClassAndEntity($relatedEntityClass, $relatedEntityId); return $this->notificationPresence->countNotificationsForClassAndEntity($relatedEntityClass, $relatedEntityId, $more);
} }
public function listNotificationsFor(Environment $environment, string $relatedEntityClass, int $relatedEntityId, array $options = []): string /**
* @param list<array{relatedEntityClass: class-string, relatedEntityId: int}> $more
*/
public function listNotificationsFor(Environment $environment, string $relatedEntityClass, int $relatedEntityId, array $more = [], array $options = []): string
{ {
$notifications = $this->notificationPresence->getNotificationsForClassAndEntity($relatedEntityClass, $relatedEntityId); $notifications = $this->notificationPresence->getNotificationsForClassAndEntity($relatedEntityClass, $relatedEntityId, $more);
if ([] === $notifications) { if ([] === $notifications) {
return ''; return '';

View File

@ -29,6 +29,15 @@ final class NotificationRepository implements ObjectRepository
private EntityRepository $repository; private EntityRepository $repository;
private const BASE_COUNTER_SQL = <<<'SQL'
SELECT
SUM((EXISTS (SELECT 1 AS c FROM chill_main_notification_addresses_unread cmnau WHERE user_id = :userid and cmnau.notification_id = cmn.id))::int) AS unread,
SUM((cmn.sender_id = :userid)::int) AS sent,
SUM((EXISTS (SELECT 1 AS c FROM chill_main_notification_addresses_user cmnau_all WHERE user_id = :userid and cmnau_all.notification_id = cmn.id))::int) + SUM((cmn.sender_id = :userid)::int) AS total
FROM chill_main_notification cmn
SQL;
public function __construct(EntityManagerInterface $entityManager) public function __construct(EntityManagerInterface $entityManager)
{ {
$this->em = $entityManager; $this->em = $entityManager;
@ -51,29 +60,45 @@ final class NotificationRepository implements ObjectRepository
->getSingleScalarResult(); ->getSingleScalarResult();
} }
public function countNotificationByRelatedEntityAndUserAssociated(string $relatedEntityClass, int $relatedEntityId, User $user): array /**
* @param list<array{relatedEntityClass: class-string, relatedEntityId: int}> $more
* @return array{unread: int, sent: int, total: int}
*/
public function countNotificationByRelatedEntityAndUserAssociated(string $relatedEntityClass, int $relatedEntityId, User $user, array $more = []): array
{ {
if (null === $this->notificationByRelatedEntityAndUserAssociatedStatement) { $sqlParams = ['relatedEntityClass' => $relatedEntityClass, 'relatedEntityId' => $relatedEntityId, 'userid' => $user->getId()];
$sql =
'SELECT
SUM((EXISTS (SELECT 1 AS c FROM chill_main_notification_addresses_unread cmnau WHERE user_id = :userid and cmnau.notification_id = cmn.id))::int) AS unread,
SUM((cmn.sender_id = :userid)::int) AS sent,
COUNT(cmn.*) AS total
FROM chill_main_notification cmn
WHERE relatedentityclass = :relatedEntityClass AND relatedentityid = :relatedEntityId AND sender_id IS NOT NULL';
$this->notificationByRelatedEntityAndUserAssociatedStatement = if ([] === $more) {
$this->em->getConnection()->prepare($sql); if (null === $this->notificationByRelatedEntityAndUserAssociatedStatement) {
$sql = self::BASE_COUNTER_SQL . ' WHERE relatedentityclass = :relatedEntityClass AND relatedentityid = :relatedEntityId AND sender_id IS NOT NULL';
$this->notificationByRelatedEntityAndUserAssociatedStatement =
$this->em->getConnection()->prepare($sql);
}
$results = $this->notificationByRelatedEntityAndUserAssociatedStatement
->executeQuery($sqlParams);
$result = $results->fetchAssociative();
$results->free();
} else {
$wheres = [];
foreach ([
['relatedEntityClass' => $relatedEntityClass, 'relatedEntityId' => $relatedEntityId],
...$more
] as $k => ['relatedEntityClass' => $relClass, 'relatedEntityId' => $relId]) {
$wheres[] = "(relatedEntityClass = :relatedEntityClass_{$k} AND relatedEntityId = :relatedEntityId_{$k})";
$sqlParams["relatedEntityClass_{$k}"] = $relClass;
$sqlParams["relatedEntityId_{$k}"] = $relId;
}
$sql = self::BASE_COUNTER_SQL . ' WHERE sender_id IS NOT NULL AND (' . implode(' OR ', $wheres) . ')';
$result = $this->em->getConnection()->fetchAssociative($sql, $sqlParams);
} }
$results = $this->notificationByRelatedEntityAndUserAssociatedStatement return array_map(fn (?int $number) => $number ?? 0, $result);
->executeQuery(['relatedEntityClass' => $relatedEntityClass, 'relatedEntityId' => $relatedEntityId, 'userid' => $user->getId()]);
$result = $results->fetchAssociative();
$results->free();
return $result;
} }
public function countUnreadByUser(User $user): int public function countUnreadByUser(User $user): int
@ -167,8 +192,8 @@ final class NotificationRepository implements ObjectRepository
} }
/** /**
* @param mixed|null $limit * @param int|null $limit
* @param mixed|null $offset * @param int|null $offset
* *
* @return Notification[] * @return Notification[]
*/ */
@ -178,13 +203,15 @@ final class NotificationRepository implements ObjectRepository
} }
/** /**
* @param list<array{relatedEntityClass: class-string, relatedEntityId: int}> $more
* @return array|Notification[] * @return array|Notification[]
*/ */
public function findNotificationByRelatedEntityAndUserAssociated(string $relatedEntityClass, int $relatedEntityId, User $user): array public function findNotificationByRelatedEntityAndUserAssociated(string $relatedEntityClass, int $relatedEntityId, User $user, array $more): array
{ {
return return
$this->buildQueryNotificationByRelatedEntityAndUserAssociated($relatedEntityClass, $relatedEntityId, $user) $this->buildQueryNotificationByRelatedEntityAndUserAssociated($relatedEntityClass, $relatedEntityId, $user, $more)
->select('n') ->select('n')
->addOrderBy('n.date', 'DESC')
->getQuery() ->getQuery()
->getResult(); ->getResult();
} }
@ -222,13 +249,36 @@ final class NotificationRepository implements ObjectRepository
return Notification::class; return Notification::class;
} }
private function buildQueryNotificationByRelatedEntityAndUserAssociated(string $relatedEntityClass, int $relatedEntityId, User $user): QueryBuilder /**
* @param list<array{relatedEntityClass: class-string, relatedEntityId: int}> $more
*/
private function buildQueryNotificationByRelatedEntityAndUserAssociated(string $relatedEntityClass, int $relatedEntityId, User $user, array $more = []): QueryBuilder
{ {
$qb = $this->repository->createQueryBuilder('n'); $qb = $this->repository->createQueryBuilder('n');
// add condition for related entity (in main arguments, and in more)
$or = $qb->expr()->orX($qb->expr()->andX(
$qb->expr()->eq('n.relatedEntityClass', ':relatedEntityClass'),
$qb->expr()->eq('n.relatedEntityId', ':relatedEntityId')
));
$qb $qb
->where($qb->expr()->eq('n.relatedEntityClass', ':relatedEntityClass')) ->setParameter('relatedEntityClass', $relatedEntityClass)
->andWhere($qb->expr()->eq('n.relatedEntityId', ':relatedEntityId')) ->setParameter('relatedEntityId', $relatedEntityId);
foreach ($more as $k => ['relatedEntityClass' => $relatedClass, 'relatedEntityId' => $relatedId]) {
$or->add(
$qb->expr()->andX(
$qb->expr()->eq('n.relatedEntityClass', ':relatedEntityClass_'.$k),
$qb->expr()->eq('n.relatedEntityId', ':relatedEntityId_'.$k)
)
);
$qb
->setParameter('relatedEntityClass_'.$k, $relatedClass)
->setParameter('relatedEntityId_'.$k, $relatedId);
}
$qb
->andWhere($or)
->andWhere($qb->expr()->isNotNull('n.sender')) ->andWhere($qb->expr()->isNotNull('n.sender'))
->andWhere( ->andWhere(
$qb->expr()->orX( $qb->expr()->orX(
@ -236,8 +286,6 @@ final class NotificationRepository implements ObjectRepository
$qb->expr()->eq('n.sender', ':user') $qb->expr()->eq('n.sender', ':user')
) )
) )
->setParameter('relatedEntityClass', $relatedEntityClass)
->setParameter('relatedEntityId', $relatedEntityId)
->setParameter('user', $user); ->setParameter('user', $user);
return $qb; return $qb;