Refactor PersonSearch and create PersonACLAwareRepository

The search api delegates the query to a person acl aware "repository"
(although this does not implements ObjectRepository interface).
This commit is contained in:
2021-09-10 17:44:01 +02:00
parent f87f03b5c0
commit 6bc83edfe9
10 changed files with 497 additions and 416 deletions

View File

@@ -0,0 +1,272 @@
<?php
namespace Chill\PersonBundle\Repository;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Repository\CountryRepository;
use Chill\MainBundle\Search\ParsingException;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\PersonBundle\Entity\Person;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\NonUniqueResultException;
use Doctrine\ORM\NoResultException;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Security\Core\Role\Role;
use Symfony\Component\Security\Core\Security;
final class PersonACLAwareRepository implements PersonACLAwareRepositoryInterface
{
private Security $security;
private EntityManagerInterface $em;
private CountryRepository $countryRepository;
private AuthorizationHelper $authorizationHelper;
public function __construct(
Security $security,
EntityManagerInterface $em,
CountryRepository $countryRepository,
AuthorizationHelper $authorizationHelper
) {
$this->security = $security;
$this->em = $em;
$this->countryRepository = $countryRepository;
$this->authorizationHelper = $authorizationHelper;
}
/**
* @return array|Person[]
* @throws NonUniqueResultException
* @throws ParsingException
*/
public function findBySearchCriteria(
int $start,
int $limit,
bool $simplify = false,
string $default = null,
string $firstname = null,
string $lastname = null,
?\DateTime $birthdate = null,
?\DateTime $birthdateBefore = null,
?\DateTime $birthdateAfter = null,
string $gender = null,
string $countryCode = null
): array {
$qb = $this->createSearchQuery($default, $firstname, $lastname,
$birthdate, $birthdateBefore, $birthdateAfter, $gender,
$countryCode);
$this->addACLClauses($qb, 'p');
if ($simplify) {
$qb->select(
'p.id',
$qb->expr()->concat(
'p.firstName',
$qb->expr()->literal(' '),
'p.lastName'
).'AS text'
);
} else {
$qb->select('p');
}
$qb
->setMaxResults($limit)
->setFirstResult($start);
//order by firstname, lastname
$qb
->orderBy('p.firstName')
->addOrderBy('p.lastName');
if ($simplify) {
return $qb->getQuery()->getResult(Query::HYDRATE_ARRAY);
} else {
return $qb->getQuery()->getResult();
}
}
public function countBySearchCriteria(
string $default = null,
string $firstname = null,
string $lastname = null,
?\DateTime $birthdate = null,
?\DateTime $birthdateBefore = null,
?\DateTime $birthdateAfter = null,
string $gender = null,
string $countryCode = null
): int {
$qb = $this->createSearchQuery($default, $firstname, $lastname,
$birthdate, $birthdateBefore, $birthdateAfter, $gender,
$countryCode);
$this->addACLClauses($qb, 'p');
$qb->select('COUNT(p.id)');
return $qb->getQuery()->getSingleScalarResult();
}
public function findBySimilaritySearch(string $pattern, int $firstResult,
int $maxResult, bool $simplify = false)
{
$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('p');
}
$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();
}
}
public function countBySimilaritySearch(string $pattern)
{
$qb = $this->createSimilarityQuery($pattern);
$this->addACLClauses($qb, 'sp');
$qb->select('COUNT(sp.id)');
return $qb->getQuery()->getSingleScalarResult();
}
private function createSearchQuery(
string $default = null,
string $firstname = null,
string $lastname = null,
?\DateTime $birthdate = null,
?\DateTime $birthdateBefore = null,
?\DateTime $birthdateAfter = null,
string $gender = null,
string $countryCode = null
): QueryBuilder {
if (!$this->security->getUser() instanceof User) {
throw new \RuntimeException("Search must be performed by a valid user");
}
$qb = $this->em->createQueryBuilder();
$qb->from(Person::class, 'p');
if (NULL !== $firstname) {
$qb->andWhere($qb->expr()->like('UNACCENT(LOWER(p.firstName))', ':firstname'))
->setParameter('firstname', '%'.$firstname.'%');
}
if (NULL !== $lastname) {
$qb->andWhere($qb->expr()->like('UNACCENT(LOWER(p.lastName))', ':lastname'))
->setParameter('lastname', '%'.$lastname.'%');
}
if (NULL !== $birthdate) {
$qb->andWhere($qb->expr()->eq('s.birthdate', ':birthdate'))
->setParameter('birthdate', $birthdate);
}
if (NULL !== $birthdateAfter) {
$qb->andWhere($qb->expr()->gt('p.birthdate', ':birthdateafter'))
->setParameter('birthdateafter', $birthdateAfter);
}
if (NULL !== $birthdateBefore) {
$qb->andWhere($qb->expr()->lt('p.birthdate', ':birthdatebefore'))
->setParameter('birthdatebefore', $birthdateBefore);
}
if (NULL !== $gender) {
$qb->andWhere($qb->expr()->eq('p.gender', ':gender'))
->setParameter('gender', $gender);
}
if (NULL !== $countryCode) {
try {
$country = $this->countryRepository->findOneBy(['countryCode' => $countryCode]);
} catch (NoResultException $ex) {
throw new ParsingException('The country code "'.$countryCode.'" '
. ', used in nationality, is unknow', 0, $ex);
} catch (NonUniqueResultException $e) {
throw $e;
}
$qb->andWhere($qb->expr()->eq('p.nationality', ':nationality'))
->setParameter('nationality', $country);
}
if (NULL !== $default) {
$grams = explode(' ', $default);
foreach($grams as $key => $gram) {
$qb->andWhere($qb->expr()
->like('p.fullnameCanonical', 'UNACCENT(LOWER(:default_'.$key.'))'))
->setParameter('default_'.$key, '%'.$gram.'%');
}
}
return $qb;
}
private function addACLClauses(QueryBuilder $qb, string $personAlias): void
{
// restrict center for security
$reachableCenters = $this->authorizationHelper
->getReachableCenters($this->security->getUser(), 'CHILL_PERSON_SEE');
$qb->andWhere(
$qb->expr()->orX(
$qb->expr()
->in($personAlias.'.center', ':centers'),
$qb->expr()
->isNull($personAlias.'.center')
)
);
$qb->setParameter('centers', $reachableCenters);
}
private function createSimilarityQuery($pattern): QueryBuilder
{
$qb = $this->em->createQueryBuilder();
$qb->from(Person::class, 'sp');
$grams = explode(' ', $pattern);
foreach($grams as $key => $gram) {
$qb->andWhere('SIMILARITY(sp.fullnameCanonical, UNACCENT(LOWER(:default_'.$key.')) ) >= 0.15')
->setParameter('default_'.$key, '%'.$gram.'%');
}
$qb->andWhere($qb->expr()
->notIn(
'sp.id',
$this->createSearchQuery($pattern)
->addSelect('p.id')
->getDQL()
)
);
return $qb;
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace Chill\PersonBundle\Repository;
use Chill\MainBundle\Search\ParsingException;
use Chill\PersonBundle\Entity\Person;
use Doctrine\ORM\NonUniqueResultException;
interface PersonACLAwareRepositoryInterface
{
/**
* @return array|Person[]
* @throws NonUniqueResultException
* @throws ParsingException
*/
public function findBySearchCriteria(
int $start,
int $limit,
bool $simplify = false,
string $default = null,
string $firstname = null,
string $lastname = null,
?\DateTime $birthdate = null,
?\DateTime $birthdateBefore = null,
?\DateTime $birthdateAfter = null,
string $gender = null,
string $countryCode = null
): array;
public function countBySearchCriteria(
string $default = null,
string $firstname = null,
string $lastname = null,
?\DateTime $birthdate = null,
?\DateTime $birthdateBefore = null,
?\DateTime $birthdateAfter = null,
string $gender = null,
string $countryCode = null
);
public function findBySimilaritySearch(
string $pattern,
int $firstResult,
int $maxResult,
bool $simplify = false
);
public function countBySimilaritySearch(string $pattern);
}

View File

@@ -23,9 +23,11 @@ use Chill\PersonBundle\Entity\Person;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ObjectRepository;
use UnexpectedValueException;
final class PersonRepository
final class PersonRepository implements ObjectRepository
{
private EntityRepository $repository;
@@ -44,6 +46,26 @@ final class PersonRepository
return $this->repository->findBy(['id' => $ids]);
}
public function findAll()
{
return $this->repository->findAll();
}
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null)
{
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
public function findOneBy(array $criteria)
{
return $this->repository->findOneBy($criteria);
}
public function getClassName()
{
return Person::class;
}
/**
* @param $centers
* @param $firstResult