mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-10-05 21:09:43 +00:00
Introduced `PostalCodeForAddressReferenceRepository` and its interface to support optimized postal code search using materialized views. Updated `AddressReferenceRepository` to improve query handling. Added test coverage for the new repository functionality.
271 lines
9.4 KiB
PHP
271 lines
9.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/*
|
|
* Chill is a software for social workers
|
|
*
|
|
* For the full copyright and license information, please view
|
|
* the LICENSE file that was distributed with this source code.
|
|
*/
|
|
|
|
namespace Chill\MainBundle\Repository;
|
|
|
|
use Chill\MainBundle\Entity\AddressReference;
|
|
use Chill\MainBundle\Entity\PostalCode;
|
|
use Chill\MainBundle\Search\SearchApiQuery;
|
|
use Doctrine\DBAL\Types\Types;
|
|
use Doctrine\ORM\EntityManagerInterface;
|
|
use Doctrine\ORM\EntityRepository;
|
|
use Doctrine\ORM\NativeQuery;
|
|
use Doctrine\ORM\Query\ResultSetMapping;
|
|
use Doctrine\ORM\Query\ResultSetMappingBuilder;
|
|
|
|
final readonly class AddressReferenceRepository implements AddressReferenceRepositoryInterface
|
|
{
|
|
private EntityManagerInterface $entityManager;
|
|
|
|
private EntityRepository $repository;
|
|
|
|
public function __construct(EntityManagerInterface $entityManager)
|
|
{
|
|
$this->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<AddressReference>
|
|
*/
|
|
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;
|
|
}
|
|
}
|