Add shortcut person <-> current address, and update api for

re-implements searching

* add geographic function ST_CONTAINS
* add a link between the current valid address and person, optimized on
database side;
* update PersonACLAwareRepository for re-using methods elsewhere.
This commit is contained in:
Julien Fastré 2021-09-16 16:43:38 +02:00
parent 50b7554aea
commit ebb2f5d243
5 changed files with 122 additions and 41 deletions

View File

@ -19,6 +19,7 @@
namespace Chill\MainBundle\DependencyInjection;
use Chill\MainBundle\Doctrine\DQL\STContains;
use Chill\MainBundle\Doctrine\DQL\StrictWordSimilarityOPS;
use Chill\MainBundle\Entity\UserJob;
use Chill\MainBundle\Form\UserJobType;
@ -186,7 +187,8 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface,
'JSONB_EXISTS_IN_ARRAY' => JsonbExistsInArray::class,
'SIMILARITY' => Similarity::class,
'OVERLAPSI' => OverlapsI::class,
'STRICT_WORD_SIMILARITY_OPS' => StrictWordSimilarityOPS::class
'STRICT_WORD_SIMILARITY_OPS' => StrictWordSimilarityOPS::class,
'ST_CONTAINS' => STContains::class,
],
],
'hydrators' => [

View File

@ -0,0 +1,52 @@
<?php
/*
* Chill is a software for social workers
* Copyright (C) 2018 Champs-Libres Coopérative <info@champs-libres.coop>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Chill\MainBundle\Doctrine\DQL;
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\Lexer;
/**
* Geometry function 'ST_CONTAINS', added by postgis
*/
class STContains extends FunctionNode
{
private $firstPart;
private $secondPart;
public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker)
{
return 'ST_CONTAINS('.$this->firstPart->dispatch($sqlWalker).
', ' . $this->secondPart->dispatch($sqlWalker) .")";
}
public function parse(\Doctrine\ORM\Query\Parser $parser)
{
$parser->match(Lexer::T_IDENTIFIER);
$parser->match(Lexer::T_OPEN_PARENTHESIS);
$this->firstPart = $parser->StringPrimary();
$parser->match(Lexer::T_COMMA);
$this->secondPart = $parser->StringPrimary();
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
}
}

View File

@ -419,6 +419,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* This is computed through database and is optimized on database side.
*
* @var PersonCurrentAddress|null
* @ORM\OneToOne(targetEntity=PersonCurrentAddress::class, mappedBy="person")
*/
private ?PersonCurrentAddress $currentPersonAddress = null;

View File

@ -22,7 +22,8 @@ class PersonCurrentAddress
{
/**
* @ORM\Id
* @ORM\OneToOne(targetEntity=Person::class)
* @ORM\OneToOne(targetEntity=Person::class, inversedBy="currentPersonAddress")
* @ORM\JoinColumn(name="person_id", referencedColumnName="id")
*/
protected Person $person;

View File

@ -60,17 +60,32 @@ final class PersonACLAwareRepository implements PersonACLAwareRepositoryInterfac
$countryCode);
$this->addACLClauses($qb, 'p');
return $this->getQueryResult($qb, $simplify, $limit, $start);
}
/**
* Helper method to prepare and return the search query for PersonACL.
*
* This method replace the select clause with required parameters, depending on the
* "simplify" parameter. It also add query limits.
*
* The given alias must represent the person alias.
*
* @return array|Person[]
*/
public function getQueryResult(QueryBuilder $qb, string $alias, bool $simplify, int $limit, int $start): array
{
if ($simplify) {
$qb->select(
'p.id',
$alias.'.id',
$qb->expr()->concat(
'p.firstName',
$alias.'.firstName',
$qb->expr()->literal(' '),
'p.lastName'
$alias.'.lastName'
).'AS text'
);
} else {
$qb->select('p');
$qb->select($alias);
}
$qb
@ -79,8 +94,8 @@ final class PersonACLAwareRepository implements PersonACLAwareRepositoryInterfac
//order by firstname, lastname
$qb
->orderBy('p.firstName')
->addOrderBy('p.lastName');
->orderBy($alias.'.firstName')
->addOrderBy($alias.'.lastName');
if ($simplify) {
return $qb->getQuery()->getResult(Query::HYDRATE_ARRAY);
@ -104,7 +119,20 @@ final class PersonACLAwareRepository implements PersonACLAwareRepositoryInterfac
$countryCode);
$this->addACLClauses($qb, 'p');
$qb->select('COUNT(p.id)');
return $this->getCountQueryResult($qb);
}
/**
* Helper method to prepare and return the count for search query
*
* This method replace the select clause with required parameters, depending on the
* "simplify" parameter.
*
* The given alias must represent the person alias in the query builder.
*/
public function getCountQueryResult(QueryBuilder $qb, $alias): int
{
$qb->select('COUNT('.$alias.'.id)');
return $qb->getQuery()->getSingleScalarResult();
}
@ -115,33 +143,7 @@ final class PersonACLAwareRepository implements PersonACLAwareRepositoryInterfac
$qb = $this->createSimilarityQuery($pattern);
$this->addACLClauses($qb, 'sp');
if ($simplify) {
$qb->select(
'sp.id',
$qb->expr()->concat(
'sp.firstName',
$qb->expr()->literal(' '),
'sp.lastName'
).'AS text'
);
} else {
$qb->select('sp');
}
$qb
->setMaxResults($maxResult)
->setFirstResult($firstResult);
//order by firstname, lastname
$qb
->orderBy('sp.firstName')
->addOrderBy('sp.lastName');
if ($simplify) {
return $qb->getQuery()->getResult(Query::HYDRATE_ARRAY);
} else {
return $qb->getQuery()->getResult();
}
return $this->getQueryResult($qb, 'sp', $simplify, $maxResult, $firstResult);
}
public function countBySimilaritySearch(string $pattern)
@ -149,12 +151,27 @@ final class PersonACLAwareRepository implements PersonACLAwareRepositoryInterfac
$qb = $this->createSimilarityQuery($pattern);
$this->addACLClauses($qb, 'sp');
$qb->select('COUNT(sp.id)');
return $qb->getQuery()->getSingleScalarResult();
return $this->getCountQueryResult($qb, 'sp');
}
private function createSearchQuery(
/**
* Create a search query without ACL
*
* The person alias is a "p"
*
* @param string|null $default
* @param string|null $firstname
* @param string|null $lastname
* @param \DateTime|null $birthdate
* @param \DateTime|null $birthdateBefore
* @param \DateTime|null $birthdateAfter
* @param string|null $gender
* @param string|null $countryCode
* @return QueryBuilder
* @throws NonUniqueResultException
* @throws ParsingException
*/
public function createSearchQuery(
string $default = null,
string $firstname = null,
string $lastname = null,
@ -244,7 +261,15 @@ final class PersonACLAwareRepository implements PersonACLAwareRepositoryInterfac
$qb->setParameter('centers', $reachableCenters);
}
private function createSimilarityQuery($pattern): QueryBuilder
/**
* Create a query for searching by similarity.
*
* The person alias is "sp".
*
* @param $pattern
* @return QueryBuilder
*/
public function createSimilarityQuery($pattern): QueryBuilder
{
$qb = $this->em->createQueryBuilder();