From 71d0785ab4de88a75e7580412b63686cfe3c4967 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 15 Mar 2023 14:52:25 +0100 Subject: [PATCH] Feature: add api endpoint for listing all geographical units covering an address --- ...GeographicalUnitByAddressApiController.php | 66 +++++++++++++++++++ .../SimpleGeographicalUnitDTO.php | 6 ++ .../Repository/GeographicalUnitRepository.php | 46 +++++++++++-- .../GeographicalUnitRepositoryInterface.php | 15 +++++ ...raphicalUnitByAddressApiControllerTest.php | 40 +++++++++++ 5 files changed, 169 insertions(+), 4 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/Controller/GeographicalUnitByAddressApiController.php create mode 100644 src/Bundle/ChillMainBundle/Tests/Controller/GeographicalUnitByAddressApiControllerTest.php diff --git a/src/Bundle/ChillMainBundle/Controller/GeographicalUnitByAddressApiController.php b/src/Bundle/ChillMainBundle/Controller/GeographicalUnitByAddressApiController.php new file mode 100644 index 000000000..bb0ef2cb1 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Controller/GeographicalUnitByAddressApiController.php @@ -0,0 +1,66 @@ +paginatorFactory = $paginatorFactory; + $this->geographicalUnitRepository = $geographicalUnitRepository; + $this->security = $security; + $this->serializer = $serializer; + } + + /** + * @Route("/api/1.0/main/geographical-unit/by-address/{id}.{_format}", requirements={"_format": "json"}) + */ + public function getGeographicalUnitCoveringAddress(Address $address): JsonResponse + { + if (!$this->security->isGranted('ROLE_USER')) { + throw new AccessDeniedHttpException(); + } + + $count = $this->geographicalUnitRepository->countGeographicalUnitContainingAddress($address); + $pagination = $this->paginatorFactory->create($count); + $units = $this->geographicalUnitRepository->findGeographicalUnitContainingAddress($address, $pagination->getCurrentPageFirstItemNumber(), $pagination->getItemsPerPage()); + + $collection = new Collection($units, $pagination); + + return new JsonResponse( + $this->serializer->serialize($collection, 'json', [AbstractNormalizer::GROUPS => ['read']]), + JsonResponse::HTTP_OK, + [], + true + ); + } +} diff --git a/src/Bundle/ChillMainBundle/Entity/GeographicalUnit/SimpleGeographicalUnitDTO.php b/src/Bundle/ChillMainBundle/Entity/GeographicalUnit/SimpleGeographicalUnitDTO.php index 34f16a0fb..7add53066 100644 --- a/src/Bundle/ChillMainBundle/Entity/GeographicalUnit/SimpleGeographicalUnitDTO.php +++ b/src/Bundle/ChillMainBundle/Entity/GeographicalUnit/SimpleGeographicalUnitDTO.php @@ -11,6 +11,8 @@ declare(strict_types=1); namespace Chill\MainBundle\Entity\GeographicalUnit; +use Symfony\Component\Serializer\Annotation as Serializer; + /** * Simple GeographialUnit Data Transfer Object. * @@ -21,24 +23,28 @@ class SimpleGeographicalUnitDTO /** * @readonly * @psalm-readonly + * @Serializer\Groups({"read"}) */ public int $id; /** * @readonly * @psalm-readonly + * @Serializer\Groups({"read"}) */ public int $layerId; /** * @readonly * @psalm-readonly + * @Serializer\Groups({"read"}) */ public string $unitName; /** * @readonly * @psalm-readonly + * @Serializer\Groups({"read"}) */ public string $unitRefId; diff --git a/src/Bundle/ChillMainBundle/Repository/GeographicalUnitRepository.php b/src/Bundle/ChillMainBundle/Repository/GeographicalUnitRepository.php index b422b8649..1bd15e60e 100644 --- a/src/Bundle/ChillMainBundle/Repository/GeographicalUnitRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/GeographicalUnitRepository.php @@ -11,20 +11,58 @@ declare(strict_types=1); namespace Chill\MainBundle\Repository; +use Chill\MainBundle\Entity\Address; use Chill\MainBundle\Entity\GeographicalUnit; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\Query\Expr\Join; +use Doctrine\ORM\QueryBuilder; -class GeographicalUnitRepository implements GeographicalUnitRepositoryInterface +final class GeographicalUnitRepository implements GeographicalUnitRepositoryInterface { - private EntityManagerInterface $em; - private EntityRepository $repository; public function __construct(EntityManagerInterface $em) { $this->repository = $em->getRepository($this->getClassName()); - $this->em = $em; + } + + + public function countGeographicalUnitContainingAddress(Address $address): int + { + $qb = $this->buildQueryGeographicalUnitContainingAddress($address); + + return $qb + ->select('COUNT(gu)') + ->getQuery() + ->getSingleScalarResult(); + } + + public function findGeographicalUnitContainingAddress(Address $address, int $offset = 0, int $limit = 50): array + { + $qb = $this->buildQueryGeographicalUnitContainingAddress($address); + + return $qb + ->select(sprintf('NEW %s(gu.id, gu.unitName, gu.unitRefId, IDENTITY(gu.layer))', GeographicalUnit\SimpleGeographicalUnitDTO::class)) + ->addOrderBy('IDENTITY(gu.layer)') + ->addOrderBy(('gu.unitName')) + ->getQuery() + ->setFirstResult($offset) + ->setMaxResults($limit) + ->getResult(); + } + + private function buildQueryGeographicalUnitContainingAddress(Address $address): QueryBuilder + { + $qb = $this->repository + ->createQueryBuilder('gu') + ; + return $qb + ->select(sprintf('NEW %s(gu.id, gu.unitName, gu.unitRefId, IDENTITY(gu.layer))', GeographicalUnit\SimpleGeographicalUnitDTO::class)) + ->innerJoin(Address::class, 'address', Join::WITH, 'ST_CONTAINS(gu.geom, address.point) = TRUE') + ->where($qb->expr()->eq('address', ':address')) + ->setParameter('address', $address) + ; } public function find($id): ?GeographicalUnit diff --git a/src/Bundle/ChillMainBundle/Repository/GeographicalUnitRepositoryInterface.php b/src/Bundle/ChillMainBundle/Repository/GeographicalUnitRepositoryInterface.php index cdfb057e2..f2c102407 100644 --- a/src/Bundle/ChillMainBundle/Repository/GeographicalUnitRepositoryInterface.php +++ b/src/Bundle/ChillMainBundle/Repository/GeographicalUnitRepositoryInterface.php @@ -11,8 +11,23 @@ declare(strict_types=1); namespace Chill\MainBundle\Repository; +use Chill\MainBundle\Entity\Address; +use Chill\MainBundle\Entity\GeographicalUnit\SimpleGeographicalUnitDTO; use Doctrine\Persistence\ObjectRepository; interface GeographicalUnitRepositoryInterface extends ObjectRepository { + /** + * Return the geographical units as @link{SimpleGeographicalUnitDTO} whithin the address is contained. + * + * This query is executed in real time (without the refresh of the materialized view which load the addresses). + * + * @param Address $address + * @param int $offset + * @param int $limit + * @return SimpleGeographicalUnitDTO[] + */ + public function findGeographicalUnitContainingAddress(Address $address, int $offset = 0, int $limit = 50): array; + + public function countGeographicalUnitContainingAddress(Address $address): int; } diff --git a/src/Bundle/ChillMainBundle/Tests/Controller/GeographicalUnitByAddressApiControllerTest.php b/src/Bundle/ChillMainBundle/Tests/Controller/GeographicalUnitByAddressApiControllerTest.php new file mode 100644 index 000000000..b464d142e --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Controller/GeographicalUnitByAddressApiControllerTest.php @@ -0,0 +1,40 @@ +getClientAuthenticated(); + + $client->request('GET', '/api/1.0/main/geographical-unit/by-address/'.$addressId.'.json'); + + $this->assertResponseIsSuccessful(); + } + + public static function generateRandomAddress(): iterable + { + self::bootKernel(); + $em = self::$container->get(EntityManagerInterface::class); + + $nb = $em->createQuery('SELECT COUNT(a) FROM '.Address::class.' a')->getSingleScalarResult(); + /** @var \Chill\MainBundle\Entity\Address $random */ + $random = $em->createQuery('SELECT a FROM '.Address::class.' a') + ->setFirstResult(rand(0, $nb)) + ->setMaxResults(1) + ->getSingleResult(); + + yield [$random->getId()]; + } +}