diff --git a/src/Controller/Admin/AddressBookController.php b/src/Controller/Admin/AddressBookController.php index c43c1ebe..0e743d99 100644 --- a/src/Controller/Admin/AddressBookController.php +++ b/src/Controller/Admin/AddressBookController.php @@ -26,8 +26,13 @@ public function addressBooks(ManagerRegistry $doctrine, #[MapEntity(id: 'userId' $principal = $doctrine->getRepository(Principal::class)->findOneByUri($principalUri); $addressbooks = $doctrine->getRepository(AddressBook::class)->findByPrincipalUri($principalUri); + $cardCounts = $doctrine->getRepository(AddressBook::class)->countCardsByAddressBook( + array_map(fn (AddressBook $addressbook) => $addressbook->getId(), $addressbooks) + ); + return $this->render('addressbooks/index.html.twig', [ 'addressbooks' => $addressbooks, + 'cardCounts' => $cardCounts, 'principal' => $principal, 'userId' => $userId, ]); diff --git a/src/Controller/Admin/CalendarController.php b/src/Controller/Admin/CalendarController.php index 96960fb9..71716196 100644 --- a/src/Controller/Admin/CalendarController.php +++ b/src/Controller/Admin/CalendarController.php @@ -30,30 +30,31 @@ public function calendars(ManagerRegistry $doctrine, UrlGeneratorInterface $rout $principalUri = $user->getPrincipalUri(); $principal = $doctrine->getRepository(Principal::class)->findOneByUri($principalUri); - $allCalendars = $doctrine->getRepository(CalendarInstance::class)->findByPrincipalUri($principalUri); + $allCalendars = $doctrine->getRepository(CalendarInstance::class)->findByPrincipalUriWithCalendars($principalUri); $subscriptions = $doctrine->getRepository(CalendarSubscription::class)->findByPrincipalUri($principalUri); + $objectCounts = $doctrine->getRepository(CalendarInstance::class)->countObjectsByCalendar( + array_map(fn (CalendarInstance $instance) => $instance->getCalendar()->getId(), $allCalendars) + ); + // Separate shared calendars $calendars = []; $shared = []; $auto = []; foreach ($allCalendars as $calendar) { + $compoundObject = [ + 'entity' => $calendar, + 'uri' => $router->generate('dav', ['path' => 'calendars/'.$username.'/'.$calendar->getUri()], UrlGeneratorInterface::ABSOLUTE_URL), + 'objectCount' => $objectCounts[$calendar->getCalendar()->getId()], + ]; + if ($calendar->isAutomaticallyGenerated()) { - $auto[] = [ - 'entity' => $calendar, - 'uri' => $router->generate('dav', ['path' => 'calendars/'.$username.'/'.$calendar->getUri()], UrlGeneratorInterface::ABSOLUTE_URL), - ]; + $auto[] = $compoundObject; } elseif (!$calendar->isShared()) { - $calendars[] = [ - 'entity' => $calendar, - 'uri' => $router->generate('dav', ['path' => 'calendars/'.$username.'/'.$calendar->getUri()], UrlGeneratorInterface::ABSOLUTE_URL), - ]; + $calendars[] = $compoundObject; } else { - $shared[] = [ - 'entity' => $calendar, - 'uri' => $router->generate('dav', ['path' => 'calendars/'.$username.'/'.$calendar->getUri()], UrlGeneratorInterface::ABSOLUTE_URL), - ]; + $shared[] = $compoundObject; } } diff --git a/src/Controller/Admin/UserController.php b/src/Controller/Admin/UserController.php index 6619c422..4943bfca 100644 --- a/src/Controller/Admin/UserController.php +++ b/src/Controller/Admin/UserController.php @@ -152,7 +152,7 @@ public function userDelete(ManagerRegistry $doctrine, Request $request, #[MapEnt $principalUri = $user->getPrincipalUri(); // Remove calendars and addressbooks - $calendars = $doctrine->getRepository(CalendarInstance::class)->findByPrincipalUri($principalUri); + $calendars = $doctrine->getRepository(CalendarInstance::class)->findByPrincipalUriWithCalendars($principalUri); foreach ($calendars ?? [] as $instance) { // We're only removing the calendar objects / changes / and calendar if the deleted user is an owner, // which means that the underlying calendar instance should not have another principal as owner. diff --git a/src/Controller/Api/ApiController.php b/src/Controller/Api/ApiController.php index 59908b4c..597b08a1 100644 --- a/src/Controller/Api/ApiController.php +++ b/src/Controller/Api/ApiController.php @@ -168,7 +168,7 @@ public function getUserCalendars(Request $request, int $userId, ManagerRegistry return $this->json(['status' => 'error', 'message' => 'Principal Not Found', 'timestamp' => $this->getTimestamp()], 404); } - $allCalendars = $doctrine->getRepository(CalendarInstance::class)->findByPrincipalUri($principalUri); + $allCalendars = $doctrine->getRepository(CalendarInstance::class)->findByPrincipalUriWithCalendars($principalUri); $allSubscriptions = $doctrine->getRepository(CalendarSubscription::class)->findByPrincipalUri($principalUri); $calendars = []; @@ -243,7 +243,7 @@ public function getUserCalendarDetails(Request $request, int $userId, int $calen return $this->json(['status' => 'error', 'message' => 'Principal Not Found', 'timestamp' => $this->getTimestamp()], 404); } - $allCalendars = $doctrine->getRepository(CalendarInstance::class)->findByPrincipalUri($principalUri); + $allCalendars = $doctrine->getRepository(CalendarInstance::class)->findByPrincipalUriWithCalendars($principalUri); $calendar_details = []; foreach ($allCalendars as $calendar) { diff --git a/src/Entity/AddressBook.php b/src/Entity/AddressBook.php index 50009119..31accf52 100644 --- a/src/Entity/AddressBook.php +++ b/src/Entity/AddressBook.php @@ -2,13 +2,14 @@ namespace App\Entity; +use App\Repository\AddressBookRepository; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Validator\Constraints as Assert; -#[ORM\Entity()] +#[ORM\Entity(repositoryClass: AddressBookRepository::class)] #[ORM\Table(name: 'addressbooks')] #[UniqueEntity(fields: ['principalUri', 'uri'], errorPath: 'uri', message: 'form.uri.unique')] class AddressBook diff --git a/src/Repository/AddressBookRepository.php b/src/Repository/AddressBookRepository.php new file mode 100644 index 00000000..75e1aad3 --- /dev/null +++ b/src/Repository/AddressBookRepository.php @@ -0,0 +1,54 @@ + count per address book id, including the address books that hold nothing + */ + public function countCardsByAddressBook(array $addressBookIds): array + { + $counts = array_fill_keys($addressBookIds, 0); + + if (!$addressBookIds) { + return $counts; + } + + $results = $this->getEntityManager()->getRepository(Card::class) + ->createQueryBuilder('c') + ->select('IDENTITY(c.addressBook) AS addressBookId, COUNT(c.id) AS count') + ->where('c.addressBook IN (:addressBookIds)') + ->setParameter('addressBookIds', $addressBookIds) + ->groupBy('c.addressBook') + ->getQuery() + ->getResult(); + + foreach ($results as $result) { + $counts[(int) $result['addressBookId']] = (int) $result['count']; + } + + return $counts; + } +} diff --git a/src/Repository/CalendarInstanceRepository.php b/src/Repository/CalendarInstanceRepository.php index 013b4b08..61ac3246 100644 --- a/src/Repository/CalendarInstanceRepository.php +++ b/src/Repository/CalendarInstanceRepository.php @@ -23,6 +23,23 @@ public function __construct(ManagerRegistry $registry) parent::__construct($registry, CalendarInstance::class); } + /** + * Returns every instance the principal has, with its `calendars` row already loaded. Every + * caller reads it, so leaving it lazy costs one extra query per calendar. + * + * @return CalendarInstance[] + */ + public function findByPrincipalUriWithCalendars(string $principalUri): array + { + return $this->createQueryBuilder('c') + ->addSelect('cal') + ->join('c.calendar', 'cal') + ->where('c.principalUri = :principalUri') + ->setParameter('principalUri', $principalUri) + ->getQuery() + ->getResult(); + } + /** * @return CalendarInstance[] Returns an array of CalendarInstance objects */ @@ -118,6 +135,38 @@ public function findAllSchedulingObjectsForCalendar(int $calendarInstanceId, str ->getResult(); } + /** + * Counts the objects of several calendars at once, so that listing a principal's calendars + * costs a single query instead of one per calendar. + * + * @param int[] $calendarIds + * + * @return array count per calendar id, including the calendars that hold nothing + */ + public function countObjectsByCalendar(array $calendarIds): array + { + $counts = array_fill_keys($calendarIds, 0); + + if (!$calendarIds) { + return $counts; + } + + $results = $this->getEntityManager()->getRepository(CalendarObject::class) + ->createQueryBuilder('o') + ->select('IDENTITY(o.calendar) AS calendarId, COUNT(o.id) AS count') + ->where('o.calendar IN (:calendarIds)') + ->setParameter('calendarIds', $calendarIds) + ->groupBy('o.calendar') + ->getQuery() + ->getResult(); + + foreach ($results as $result) { + $counts[(int) $result['calendarId']] = (int) $result['count']; + } + + return $counts; + } + /** * Get counts of calendar objects by component type for a calendar instance. * diff --git a/templates/addressbooks/index.html.twig b/templates/addressbooks/index.html.twig index 3d2ddd80..bdd479b9 100644 --- a/templates/addressbooks/index.html.twig +++ b/templates/addressbooks/index.html.twig @@ -23,7 +23,7 @@

{{ addressbook.description }}

- {{ "addressbooks.uri"|trans }} : {{ addressbook.uri }} — {{ "addressbooks.contacts"|trans({'%count%': addressbook.cards|length}) }} + {{ "addressbooks.uri"|trans }} : {{ addressbook.uri }} — {{ "addressbooks.contacts"|trans({'%count%': cardCounts[addressbook.id]}) }}
✎ {{ "edit"|trans }} {{ "calendars.component.notes"|trans }}{% endif %} {% if constant('\\App\\Entity\\Calendar::COMPONENT_TODOS') in calendar.calendar.components %}{{ "calendars.component.todos"|trans }}{% endif %} {% endif %} - — {{ "calendars.entries"|trans({'%count%': calendar.calendar.objects|length}) }} + — {{ "calendars.entries"|trans({'%count%': compoundObject.objectCount}) }}
{% if not calendar.isPublic() %} @@ -103,7 +103,7 @@ {% if constant('\\App\\Entity\\Calendar::COMPONENT_NOTES') in calendar.calendar.components %}{{ "calendars.component.notes"|trans }}{% endif %} {% if constant('\\App\\Entity\\Calendar::COMPONENT_TODOS') in calendar.calendar.components %}{{ "calendars.component.todos"|trans }}{% endif %} {% endif %} - — {{ "calendars.entries"|trans({'%count%': calendar.calendar.objects|length}) }} + — {{ "calendars.entries"|trans({'%count%': compoundObject.objectCount}) }}
✎ {{ "edit"|trans }} @@ -147,7 +147,7 @@ {% if constant('\\App\\Entity\\Calendar::COMPONENT_NOTES') in calendar.calendar.components %}{{ "calendars.component.notes"|trans }}{% endif %} {% if constant('\\App\\Entity\\Calendar::COMPONENT_TODOS') in calendar.calendar.components %}{{ "calendars.component.todos"|trans }}{% endif %} {% endif %} - — {{ "calendars.entries"|trans({'%count%': calendar.calendar.objects|length}) }} + — {{ "calendars.entries"|trans({'%count%': compoundObject.objectCount}) }}