repository = $entityManager->getRepository(AddressReference::class); $this->entityManager = $entityManager; } public function countAll(): int { $qb = $this->repository->createQueryBuilder('ar'); $qb->select('count(ar.id)'); return $qb->getQuery()->getSingleScalarResult(); } public function countByPostalCodePattern(PostalCode $postalCode, string $pattern): int { $query = $this->buildQueryByPostalCodePattern($postalCode, $pattern); $sql = $query->buildQuery(true); $rsm = new ResultSetMapping(); $rsm->addScalarResult('c', 'c'); $nq = $this->entityManager->createNativeQuery($sql, $rsm)->setParameters($query->buildParameters(true)); return (int) $nq->getSingleResult()['c']; } public function find($id, $lockMode = null, $lockVersion = null): ?AddressReference { return $this->repository->find($id, $lockMode, $lockVersion); } /** * @return AddressReference[] */ public function findAll(): array { return $this->repository->findAll(); } public function findAggregatedBySearchString(string $search, PostalCode|int|null $postalCode = null, int $firstResult = 0, int $maxResults = 50): iterable { $terms = $this->buildTermsFromSearchString($search); if ([] === $terms) { return []; } $connection = $this->entityManager->getConnection(); $qb = $connection->createQueryBuilder(); $qb->select('row_number() OVER () AS row_number', 'var.street AS street', 'cmpc.id AS postcode_id', 'cmpc.code AS code', 'cmpc.label AS label', 'jsonb_object_agg(var.address_id, var.streetnumber ORDER BY var.row_number) AS positions') ->from('view_chill_main_address_reference', 'var') ->innerJoin('var', 'chill_main_postal_code', 'cmpc', 'cmpc.id = var.postcode_id') ->groupBy('cmpc.id', 'var.street') ->setFirstResult($firstResult) ->setMaxResults($maxResults); $paramId = 0; foreach ($terms as $term) { $qb->andWhere('var.address like ?'); $qb->setParameter(++$paramId, "%{$term}%"); } if (null !== $postalCode) { $qb->andWhere('var.postcode_id = ?'); $qb->setParameter(++$paramId, $postalCode instanceof PostalCode ? $postalCode->getId() : $postalCode); } $result = $qb->executeQuery(); return $result->iterateAssociative(); } /** * @return iterable */ public function findBySearchString(string $search, PostalCode|int|null $postalCode = null, int $firstResult = 0, int $maxResults = 50): iterable { $terms = $this->buildTermsFromSearchString($search); if ([] === $terms) { return []; } $rsm = new ResultSetMappingBuilder($this->entityManager); $rsm->addRootEntityFromClassMetadata(AddressReference::class, 'ar'); $baseSql = 'SELECT '.$rsm->generateSelectClause(['ar' => 'ar']).' FROM chill_main_address_reference ar JOIN view_chill_main_address_reference var ON var.address_id = ar.id'; $nql = $this->buildQueryBySearchString($rsm, $baseSql, $terms, $postalCode); $orderBy = []; $pertinence = []; foreach ($terms as $k => $term) { $pertinence[] = "(EXISTS (SELECT 1 FROM unnest(string_to_array(address, ' ')) AS t WHERE starts_with(t, UNACCENT(LOWER(:order{$k})))))::int"; $pertinence[] = "(address LIKE UNACCENT(LOWER(:order{$k})))::int"; $nql->setParameter('order'.$k, $term); } $orderBy[] = implode(' + ', $pertinence).' ASC'; $orderBy[] = implode('row_number ASC', $orderBy); $nql->setSQL($nql->getSQL().' ORDER BY '.implode(', ', $orderBy)); return $nql->toIterable(); } public function countBySearchString(string $search, PostalCode|int|null $postalCode = null): int { $terms = $this->buildTermsFromSearchString($search); if ([] === $terms) { return 0; } $rsm = new ResultSetMappingBuilder($this->entityManager); $rsm->addScalarResult('c', 'c', Types::INTEGER); $nql = $this->buildQueryBySearchString($rsm, 'SELECT COUNT(var.*) AS c FROM view_chill_main_address_reference var', $terms, $postalCode); return $nql->getSingleScalarResult(); } private function buildTermsFromSearchString(string $search): array { return array_filter( array_map( static fn (string $term) => trim($term), explode(' ', $search) ), static fn (string $term) => '' !== $term ); } private function buildQueryBySearchString(ResultSetMapping $rsm, string $select, array $terms, PostalCode|int|null $postalCode = null): NativeQuery { $nql = $this->entityManager->createNativeQuery('', $rsm); $sql = $select.' WHERE '; $wheres = []; foreach ($terms as $k => $term) { $wheres[] = "var.address like :w{$k}"; $nql->setParameter("w{$k}", '%'.$term.'%', Types::STRING); } if (null !== $postalCode) { $wheres[] = 'var.postcode_id = :postalCode'; $nql->setParameter('postalCode', $postalCode instanceof PostalCode ? $postalCode->getId() : $postalCode); } $nql->setSQL($sql.implode(' AND ', $wheres)); return $nql; } /** * @param mixed|null $limit * @param mixed|null $offset * * @return AddressReference[] */ public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array { return $this->repository->findBy($criteria, $orderBy, $limit, $offset); } /** * @return AddressReference[]|array */ public function findByPostalCodePattern(PostalCode $postalCode, string $pattern, bool $simplify = false, int $start = 0, int $limit = 50): array { $query = $this->buildQueryByPostalCodePattern($postalCode, $pattern); if (!$simplify) { $rsm = new ResultSetMappingBuilder($this->entityManager); $rsm->addRootEntityFromClassMetadata(AddressReference::class, 'cma'); $query->addSelectClause($rsm->generateSelectClause()); } else { throw new \RuntimeException('not implemented'); } $sql = \strtr( $query->buildQuery().'ORDER BY pertinence DESC, lpad(streetnumber, 10, \'0\') ASC OFFSET ? LIMIT ? ', // little hack for adding sql method to point ['cma.point AS point' => 'ST_AsGeojson(cma.point) AS point'] ); $parameters = [...$query->buildParameters(), $start, $limit]; return $this->entityManager->createNativeQuery($sql, $rsm) ->setParameters($parameters) ->getResult(); } public function findOneBy(array $criteria, ?array $orderBy = null): ?AddressReference { return $this->repository->findOneBy($criteria, $orderBy); } public function getClassName() { return AddressReference::class; } private function buildQueryByPostalCodePattern(PostalCode $postalCode, string $pattern): SearchApiQuery { $pattern = \trim($pattern); if ('' === $pattern) { throw new \RuntimeException('the search pattern must not be empty'); } $query = new SearchApiQuery(); $query ->setFromClause('chill_main_address_reference cma') ->andWhereClause('postcode_id = ?', [$postalCode->getId()]) ->andWhereClause('deletedAt IS NULL', []); $pertinenceClause = ['STRICT_WORD_SIMILARITY(addresscanonical, UNACCENT(?))']; $pertinenceArgs = [$pattern]; $andWhere = []; $andWhereArgs = []; foreach (\explode(' ', $pattern) as $part) { $part = \trim($part); if ('' === $part) { continue; } $andWhere[] = "(addresscanonical LIKE '%' || UNACCENT(LOWER(?)) || '%')"; $andWhereArgs[] = $part; $pertinenceClause[] = "(EXISTS (SELECT 1 FROM unnest(string_to_array(addresscanonical, ' ')) AS t WHERE starts_with(t, UNACCENT(LOWER(?)))))::int"; $pertinenceClause[] = '(addresscanonical LIKE UNACCENT(LOWER(?)))::int'; array_push($pertinenceArgs, $part, $part); } $query ->setSelectPertinence(\implode(' + ', $pertinenceClause), $pertinenceArgs) ->andWhereClause(\implode(' AND ', $andWhere), $andWhereArgs); return $query; } }