From 6bc83edfe9267eca4bc941e8ad5451a621baa6ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Fri, 10 Sep 2021 17:44:01 +0200 Subject: [PATCH] Refactor PersonSearch and create PersonACLAwareRepository The search api delegates the query to a person acl aware "repository" (although this does not implements ObjectRepository interface). --- .../ChillMainBundle/ChillMainBundle.php | 1 + .../Search/SearchInterface.php | 43 ++- .../Repository/PersonACLAwareRepository.php | 272 +++++++++++++++++ .../PersonACLAwareRepositoryInterface.php | 50 +++ .../Repository/PersonRepository.php | 24 +- .../ChillPersonBundle/Search/PersonSearch.php | 285 ++++++------------ .../Search/SimilarityPersonSearch.php | 212 ++----------- .../ChillPersonBundle/config/services.yaml | 1 - .../config/services/repository.yaml | 5 + .../config/services/search.yaml | 20 +- 10 files changed, 497 insertions(+), 416 deletions(-) create mode 100644 src/Bundle/ChillPersonBundle/Repository/PersonACLAwareRepository.php create mode 100644 src/Bundle/ChillPersonBundle/Repository/PersonACLAwareRepositoryInterface.php diff --git a/src/Bundle/ChillMainBundle/ChillMainBundle.php b/src/Bundle/ChillMainBundle/ChillMainBundle.php index d059443b5..d2ae5f1b5 100644 --- a/src/Bundle/ChillMainBundle/ChillMainBundle.php +++ b/src/Bundle/ChillMainBundle/ChillMainBundle.php @@ -3,6 +3,7 @@ namespace Chill\MainBundle; use Chill\MainBundle\Routing\LocalMenuBuilderInterface; +use Chill\MainBundle\Search\SearchInterface; use Chill\MainBundle\Security\Resolver\CenterResolverInterface; use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\DependencyInjection\ContainerBuilder; diff --git a/src/Bundle/ChillMainBundle/Search/SearchInterface.php b/src/Bundle/ChillMainBundle/Search/SearchInterface.php index 708835b47..958ce9ddd 100644 --- a/src/Bundle/ChillMainBundle/Search/SearchInterface.php +++ b/src/Bundle/ChillMainBundle/Search/SearchInterface.php @@ -23,45 +23,44 @@ namespace Chill\MainBundle\Search; /** * This interface must be implemented on services which provide search results. - * + * * @todo : write doc and add a link to documentation - * + * * @author Julien Fastré * */ interface SearchInterface { - const SEARCH_PREVIEW_OPTION = '_search_preview'; - + /** * Request parameters contained inside the `add_q` parameters */ const REQUEST_QUERY_PARAMETERS = '_search_parameters'; - + /** * Supplementary parameters to the query string */ const REQUEST_QUERY_KEY_ADD_PARAMETERS = 'add_q'; - /** + /** * return the result in a html string. The string will be inclued (as raw) * into a global view. - * + * * The global view may be : * {% for result as resultsFromDifferentSearchInterface %} * {{ result|raw }} * {% endfor %} - * + * * **available options** : - * - SEARCH_PREVIEW_OPTION (boolean) : if renderResult should return a "preview" of + * - SEARCH_PREVIEW_OPTION (boolean) : if renderResult should return a "preview" of * the results. In this case, a subset of results should be returned, and, * if the query return more results, a button "see all results" should be * displayed at the end of the list. - * - * **Interaction between `start` and `limit` and pagination : you should - * take only the given parameters into account; the results from pagination - * should be ignored. (Most of the time, it should be the same). + * + * **Interaction between `start` and `limit` and pagination : you should + * take only the given parameters into account; the results from pagination + * should be ignored. (Most of the time, it should be the same). * * @param array $terms the string to search * @param int $start the first result (for pagination) @@ -72,10 +71,10 @@ interface SearchInterface */ public function renderResult(array $terms, $start=0, $limit=50, array $options = array(), $format = 'html'); - /** + /** * we may desactive the search interface by default. in this case, - * the search will be launch and rendered only with "advanced search" - * + * the search will be launch and rendered only with "advanced search" + * * this may be activated/desactived from bundle definition in config.yml * * @return bool @@ -84,18 +83,18 @@ interface SearchInterface /** * the order in which the results will appears in the global view - * + * * (this may be eventually defined in config.yml) - * - * @return int + * + * @return int */ public function getOrder(); - + /** * indicate if the implementation supports the given domain - * + * * @return boolean */ public function supports($domain, $format); - + } diff --git a/src/Bundle/ChillPersonBundle/Repository/PersonACLAwareRepository.php b/src/Bundle/ChillPersonBundle/Repository/PersonACLAwareRepository.php new file mode 100644 index 000000000..f466260ca --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Repository/PersonACLAwareRepository.php @@ -0,0 +1,272 @@ +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; + } +} diff --git a/src/Bundle/ChillPersonBundle/Repository/PersonACLAwareRepositoryInterface.php b/src/Bundle/ChillPersonBundle/Repository/PersonACLAwareRepositoryInterface.php new file mode 100644 index 000000000..8e83da03d --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Repository/PersonACLAwareRepositoryInterface.php @@ -0,0 +1,50 @@ +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 diff --git a/src/Bundle/ChillPersonBundle/Search/PersonSearch.php b/src/Bundle/ChillPersonBundle/Search/PersonSearch.php index 231636fff..f62b90006 100644 --- a/src/Bundle/ChillPersonBundle/Search/PersonSearch.php +++ b/src/Bundle/ChillPersonBundle/Search/PersonSearch.php @@ -20,71 +20,39 @@ namespace Chill\PersonBundle\Search; use Chill\MainBundle\Search\AbstractSearch; -use Doctrine\ORM\EntityManagerInterface; +use Chill\PersonBundle\Repository\PersonACLAwareRepositoryInterface; use Chill\PersonBundle\Entity\Person; use Chill\MainBundle\Search\SearchInterface; -use Symfony\Component\DependencyInjection\ContainerAwareInterface; -use Symfony\Component\DependencyInjection\ContainerAwareTrait; use Chill\MainBundle\Search\ParsingException; -use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; -use Chill\MainBundle\Security\Authorization\AuthorizationHelper; -use Symfony\Component\Security\Core\Role\Role; use Chill\MainBundle\Pagination\PaginatorFactory; use Symfony\Component\Form\Extension\Core\Type\TextType; use Chill\MainBundle\Form\Type\ChillDateType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\FormBuilderInterface; use Chill\MainBundle\Search\HasAdvancedSearchFormInterface; -use Doctrine\ORM\Query; +use Symfony\Component\Templating\EngineInterface; -class PersonSearch extends AbstractSearch implements ContainerAwareInterface, - HasAdvancedSearchFormInterface +class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterface { - use ContainerAwareTrait; - - /** - * - * @var EntityManagerInterface - */ - private $em; - - /** - * - * @var \Chill\MainBundle\Entity\User - */ - private $user; - - /** - * - * @var AuthorizationHelper - */ - private $helper; - - /** - * - * @var PaginatorFactory - */ - protected $paginatorFactory; + protected EngineInterface $templating; + protected PaginatorFactory $paginatorFactory; + protected PersonACLAwareRepositoryInterface $personACLAwareRepository; const NAME = "person_regular"; + private const POSSIBLE_KEYS = [ + '_default', 'firstname', 'lastname', 'birthdate', 'birthdate-before', + 'birthdate-after', 'gender', 'nationality' + ]; public function __construct( - EntityManagerInterface $em, - TokenStorageInterface $tokenStorage, - AuthorizationHelper $helper, - PaginatorFactory $paginatorFactory) - { - $this->em = $em; - $this->user = $tokenStorage->getToken()->getUser(); - $this->helper = $helper; + EngineInterface $templating, + PaginatorFactory $paginatorFactory, + PersonACLAwareRepositoryInterface $personACLAwareRepository + ) { + $this->templating = $templating; $this->paginatorFactory = $paginatorFactory; - - // throw an error if user is not a valid user - if (!$this->user instanceof \Chill\MainBundle\Entity\User) { - throw new \LogicException('The user provided must be an instance' - . ' of Chill\MainBundle\Entity\User'); - } + $this->personACLAwareRepository = $personACLAwareRepository; } /* @@ -120,12 +88,14 @@ class PersonSearch extends AbstractSearch implements ContainerAwareInterface, $paginator = $this->paginatorFactory->create($total); if ($format === 'html') { - return $this->container->get('templating')->render('@ChillPerson/Person/list_with_period.html.twig', + return $this->templating->render('@ChillPerson/Person/list_with_period.html.twig', array( 'persons' => $this->search($terms, $start, $limit, $options), - 'pattern' => $this->recomposePattern($terms, array('nationality', - 'firstname', 'lastname', 'birthdate', 'gender', - 'birthdate-before','birthdate-after'), $terms['_domain']), + 'pattern' => $this->recomposePattern( + $terms, + \array_filter(self::POSSIBLE_KEYS, fn($item) => $item !== '_default'), + $terms['_domain'] + ), 'total' => $total, 'start' => $start, 'search_name' => self::NAME, @@ -152,158 +122,81 @@ class PersonSearch extends AbstractSearch implements ContainerAwareInterface, */ protected function search(array $terms, $start, $limit, array $options = array()) { - $qb = $this->createQuery($terms, 'search'); + [ + '_default' => $default, + 'firstname' => $firstname, + 'lastname' => $lastname, + 'birthdate' => $birthdate, + 'birthdate-before' => $birthdateBefore, + 'birthdate-after' => $birthdateAfter, + 'gender' => $gender, + 'nationality' => $countryCode, - if ($options['simplify'] ?? false) { - $qb->select( - 'p.id', - $qb->expr()->concat( - 'p.firstName', - $qb->expr()->literal(' '), - 'p.lastName' - ).'AS text' - ); - } else { - $qb->select('p'); + ] = $terms + \array_fill_keys(self::POSSIBLE_KEYS, null); + + foreach (['birthdateBefore', 'birthdateAfter', 'birthdate'] as $v) { + if (NULL !== ${$v}) { + try { + ${$v} = new \DateTime(${$v}); + } catch (\Exception $e) { + throw new ParsingException('The date is ' + . 'not parsable', 0, $e); + } + } } - $qb - ->setMaxResults($limit) - ->setFirstResult($start); - - //order by firstname, lastname - - $qb - ->orderBy('p.firstName') - ->addOrderBy('p.lastName'); - - if ($options['simplify'] ?? false) { - return $qb->getQuery()->getResult(Query::HYDRATE_ARRAY); - } else { - return $qb->getQuery()->getResult(); - } + return $this->personACLAwareRepository + ->findBySearchCriteria( + $start, + $limit, + $options['simplify'] ?? false, + $default, + $firstname, + $lastname, + $birthdate, + $birthdateBefore, + $birthdateAfter, + $gender, + $countryCode, + ); } - protected function count(array $terms) + protected function count(array $terms): int { - $qb = $this->createQuery($terms); + [ + '_default' => $default, + 'firstname' => $firstname, + 'lastname' => $lastname, + 'birthdate' => $birthdate, + 'birthdate-before' => $birthdateBefore, + 'birthdate-after' => $birthdateAfter, + 'gender' => $gender, + 'nationality' => $countryCode, - $qb->select('COUNT(p.id)'); + ] = $terms + \array_fill_keys(self::POSSIBLE_KEYS, null); - return $qb->getQuery()->getSingleScalarResult(); - } - - - private $_cacheQuery = array(); - - /** - * - * @param array $terms - * @return \Doctrine\ORM\QueryBuilder - */ - public function createQuery(array $terms) - { - //get from cache - $cacheKey = md5(serialize($terms)); - if (array_key_exists($cacheKey, $this->_cacheQuery)) { - return clone $this->_cacheQuery[$cacheKey]; - } - - $qb = $this->em->createQueryBuilder(); - - $qb->from('ChillPersonBundle:Person', 'p'); - - if (array_key_exists('firstname', $terms)) { - $qb->andWhere($qb->expr()->like('UNACCENT(LOWER(p.firstName))', ':firstname')) - ->setParameter('firstname', '%'.$terms['firstname'].'%'); - } - - if (array_key_exists('lastname', $terms)) { - $qb->andWhere($qb->expr()->like('UNACCENT(LOWER(p.lastName))', ':lastname')) - ->setParameter('lastname', '%'.$terms['lastname'].'%'); - } - - foreach (['birthdate', 'birthdate-before', 'birthdate-after'] as $key) - if (array_key_exists($key, $terms)) { - try { - $date = new \DateTime($terms[$key]); - } catch (\Exception $ex) { - throw new ParsingException('The date is ' - . 'not parsable', 0, $ex); - } - - switch($key) { - case 'birthdate': - $qb->andWhere($qb->expr()->eq('p.birthdate', ':birthdate')) - ->setParameter('birthdate', $date); - break; - case 'birthdate-before': - $qb->andWhere($qb->expr()->lt('p.birthdate', ':birthdatebefore')) - ->setParameter('birthdatebefore', $date); - break; - case 'birthdate-after': - $qb->andWhere($qb->expr()->gt('p.birthdate', ':birthdateafter')) - ->setParameter('birthdateafter', $date); - break; - default: - throw new \LogicException("this case $key should not exists"); - } - - } - - if (array_key_exists('gender', $terms)) { - if (!in_array($terms['gender'], array(Person::MALE_GENDER, Person::FEMALE_GENDER))) { - throw new ParsingException('The gender ' - .$terms['gender'].' is not accepted. Should be "'.Person::MALE_GENDER - .'" or "'.Person::FEMALE_GENDER.'"'); - } - - $qb->andWhere($qb->expr()->eq('p.gender', ':gender')) - ->setParameter('gender', $terms['gender']); - } - - if (array_key_exists('nationality', $terms)) { - try { - $country = $this->em->createQuery('SELECT c FROM ' - . 'ChillMainBundle:Country c WHERE ' - . 'LOWER(c.countryCode) LIKE :code') - ->setParameter('code', $terms['nationality']) - ->getSingleResult(); - } catch (\Doctrine\ORM\NoResultException $ex) { - throw new ParsingException('The country code "'.$terms['nationality'].'" ' - . ', used in nationality, is unknow', 0, $ex); - } - - $qb->andWhere($qb->expr()->eq('p.nationality', ':nationality')) - ->setParameter('nationality', $country); - } - - if ($terms['_default'] !== '') { - $grams = explode(' ', $terms['_default']); - - foreach($grams as $key => $gram) { - $qb->andWhere($qb->expr() - ->like('p.fullnameCanonical', 'UNACCENT(LOWER(:default_'.$key.'))')) - ->setParameter('default_'.$key, '%'.$gram.'%'); + foreach (['birthdateBefore', 'birthdateAfter', 'birthdate'] as $v) { + if (NULL !== ${$v}) { + try { + ${$v} = new \DateTime(${$v}); + } catch (\Exception $e) { + throw new ParsingException('The date is ' + . 'not parsable', 0, $e); + } } } - //restraint center for security - $reachableCenters = $this->helper->getReachableCenters($this->user, - new Role('CHILL_PERSON_SEE')); - $qb->andWhere( - $qb->expr()->orX( - $qb->expr() - ->in('p.center', ':centers'), - $qb->expr() - ->isNull('p.center') - ) - ); - $qb->setParameter('centers', $reachableCenters); - - $this->_cacheQuery[$cacheKey] = $qb; - - return clone $qb; + return $this->personACLAwareRepository + ->countBySearchCriteria( + $default, + $firstname, + $lastname, + $birthdate, + $birthdateBefore, + $birthdateAfter, + $gender, + $countryCode, + ); } public function buildForm(FormBuilderInterface $builder) @@ -396,4 +289,10 @@ class PersonSearch extends AbstractSearch implements ContainerAwareInterface, return 'Search within persons'; } + public static function getAlias(): string + { + return self::NAME; + } + + } diff --git a/src/Bundle/ChillPersonBundle/Search/SimilarityPersonSearch.php b/src/Bundle/ChillPersonBundle/Search/SimilarityPersonSearch.php index ea2c9a5c7..f1ea52c8e 100644 --- a/src/Bundle/ChillPersonBundle/Search/SimilarityPersonSearch.php +++ b/src/Bundle/ChillPersonBundle/Search/SimilarityPersonSearch.php @@ -2,90 +2,32 @@ namespace Chill\PersonBundle\Search; - use Chill\MainBundle\Pagination\PaginatorFactory; use Chill\MainBundle\Search\AbstractSearch; use Chill\MainBundle\Search\SearchInterface; -use Chill\MainBundle\Security\Authorization\AuthorizationHelper; -use Doctrine\ORM\EntityManagerInterface; -use Symfony\Component\DependencyInjection\ContainerAwareInterface; -use Symfony\Component\DependencyInjection\ContainerAwareTrait; -use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; -use Symfony\Component\Security\Core\Role\Role; +use Chill\PersonBundle\Repository\PersonACLAwareRepositoryInterface; +use Symfony\Component\Templating\EngineInterface; -/** - * Class SimilarityPersonSearch - * - * @package Chill\PersonBundle\Search - */ class SimilarityPersonSearch extends AbstractSearch { - - use ContainerAwareTrait; - - /** - * - * @var EntityManagerInterface - */ - private $em; - - /** - * - * @var \Chill\MainBundle\Entity\User - */ - private $user; - - /** - * - * @var AuthorizationHelper - */ - private $helper; - - /** - * - * @var PaginatorFactory - */ - protected $paginatorFactory; - + protected PaginatorFactory $paginatorFactory; + + private PersonACLAwareRepositoryInterface $personACLAwareRepository; + + private EngineInterface $templating; + const NAME = "person_similarity"; - - /** - * - * @var PersonSearch - */ - private $personSearch; - - - /** - * SimilarityPersonSearch constructor. - * - * @param EntityManagerInterface $em - * @param TokenStorageInterface $tokenStorage - * @param AuthorizationHelper $helper - * @param PaginatorFactory $paginatorFactory - * @param PersonSearch $personSearch - */ + public function __construct( - EntityManagerInterface $em, - TokenStorageInterface $tokenStorage, - AuthorizationHelper $helper, PaginatorFactory $paginatorFactory, - PersonSearch $personSearch) - { - $this->em = $em; - $this->user = $tokenStorage->getToken()->getUser(); - $this->helper = $helper; + PersonACLAwareRepositoryInterface $personACLAwareRepository, + EngineInterface $templating + ) { $this->paginatorFactory = $paginatorFactory; - $this->personSearch = $personSearch; - - // throw an error if user is not a valid user - if (!$this->user instanceof \Chill\MainBundle\Entity\User) { - throw new \LogicException('The user provided must be an instance' - . ' of Chill\MainBundle\Entity\User'); - } + $this->personACLAwareRepository = $personACLAwareRepository; + $this->templating = $templating; } - + /* * (non-PHPdoc) * @see \Chill\MainBundle\Search\SearchInterface::getOrder() @@ -94,7 +36,7 @@ class SimilarityPersonSearch extends AbstractSearch { return 200; } - + /* * (non-PHPdoc) * @see \Chill\MainBundle\Search\SearchInterface::isActiveByDefault() @@ -103,30 +45,27 @@ class SimilarityPersonSearch extends AbstractSearch { return true; } - + public function supports($domain, $format) { return 'person' === $domain; } - + /** * @param array $terms * @param int $start * @param int $limit * @param array $options * @param string $format - * @return array */ public function renderResult(array $terms, $start = 0, $limit = 50, array $options = array(), $format = 'html') { $total = $this->count($terms); $paginator = $this->paginatorFactory->create($total); - - if ($format === 'html') - { - if ($total !== 0) - { - return $this->container->get('templating')->render('ChillPersonBundle:Person:list.html.twig', + + if ($format === 'html') { + if ($total !== 0) { + return $this->templating->render('ChillPersonBundle:Person:list.html.twig', array( 'persons' => $this->search($terms, $start, $limit, $options), 'pattern' => $this->recomposePattern($terms, array('nationality', @@ -143,9 +82,8 @@ class SimilarityPersonSearch extends AbstractSearch else { return null; } - - } elseif ($format === 'json') - { + + } elseif ($format === 'json') { return [ 'results' => $this->search($terms, $start, $limit, \array_merge($options, [ 'simplify' => true ])), 'pagination' => [ @@ -154,8 +92,7 @@ class SimilarityPersonSearch extends AbstractSearch ]; } } - - + /** * * @param string $pattern @@ -166,101 +103,12 @@ class SimilarityPersonSearch extends AbstractSearch */ protected function search(array $terms, $start, $limit, array $options = array()) { - $qb = $this->createQuery($terms, 'search'); - - - if ($options['simplify'] ?? false) { - $qb->select( - 'sp.id', - $qb->expr()->concat( - 'sp.firstName', - $qb->expr()->literal(' '), - 'sp.lastName' - ).'AS text' - ); - } else { - $qb->select('sp'); - } - - $qb - ->setMaxResults($limit) - ->setFirstResult($start); - - //order by firstname, lastname - - $qb - ->orderBy('sp.firstName') - ->addOrderBy('sp.lastName'); - - if ($options['simplify'] ?? false) { - return $qb->getQuery()->getResult(Query::HYDRATE_ARRAY); - } else { - return $qb->getQuery()->getResult(); - } + return $this->personACLAwareRepository + ->findBySimilaritySearch($terms['_default']); } - - + protected function count(array $terms) { - $qb = $this->createQuery($terms); - - - $qb->select('COUNT(sp.id)'); - - return $qb->getQuery()->getSingleScalarResult(); + return $this->personACLAwareRepository->countBySimilaritySearch($terms['_default']); } - - - private $_cacheQuery = array(); - - /** - * - * @param array $terms - * @return \Doctrine\ORM\QueryBuilder - */ - protected function createQuery(array $terms) - { - //get from cache - $cacheKey = md5(serialize($terms)); - if (array_key_exists($cacheKey, $this->_cacheQuery)) { - return clone $this->_cacheQuery[$cacheKey]; - } - - $qb = $this->em->createQueryBuilder(); - - $qb ->select('sp') - ->from('ChillPersonBundle:Person', 'sp'); - - if ($terms['_default'] !== '') { - $grams = explode(' ', $terms['_default']); - - 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->personSearch - ->createQuery($terms) - ->addSelect('p.id') - ->getDQL() - ) - ); - } - - //restraint center for security - $reachableCenters = $this->helper->getReachableCenters($this->user, - new Role('CHILL_PERSON_SEE')); - $qb->andWhere($qb->expr() - ->in('sp.center', ':centers')) - ->setParameter('centers', $reachableCenters) - ; - - $this->_cacheQuery[$cacheKey] = $qb; - - return clone $qb; - } - -} \ No newline at end of file +} diff --git a/src/Bundle/ChillPersonBundle/config/services.yaml b/src/Bundle/ChillPersonBundle/config/services.yaml index 43e042e6b..6133068a4 100644 --- a/src/Bundle/ChillPersonBundle/config/services.yaml +++ b/src/Bundle/ChillPersonBundle/config/services.yaml @@ -70,7 +70,6 @@ services: autowire: true autoconfigure: true resource: '../Repository/' - tags: ['doctrine.repository_service'] Chill\PersonBundle\Controller\: autowire: true diff --git a/src/Bundle/ChillPersonBundle/config/services/repository.yaml b/src/Bundle/ChillPersonBundle/config/services/repository.yaml index 72ca27e24..fa76ccd7c 100644 --- a/src/Bundle/ChillPersonBundle/config/services/repository.yaml +++ b/src/Bundle/ChillPersonBundle/config/services/repository.yaml @@ -1,6 +1,11 @@ services: + # note: the services.yaml file define some autoloading + chill.person.repository.person: class: Chill\PersonBundle\Repository\PersonRepository autowire: true autoconfigure: true Chill\PersonBundle\Repository\PersonRepository: '@chill.person.repository.person' + + Chill\PersonBundle\Repository\PersonACLAwareRepositoryInterface: '@Chill\PersonBundle\Repository\PersonACLAwareRepository' + diff --git a/src/Bundle/ChillPersonBundle/config/services/search.yaml b/src/Bundle/ChillPersonBundle/config/services/search.yaml index 284773196..6a7cd4496 100644 --- a/src/Bundle/ChillPersonBundle/config/services/search.yaml +++ b/src/Bundle/ChillPersonBundle/config/services/search.yaml @@ -1,25 +1,11 @@ services: - chill.person.search_person: - class: Chill\PersonBundle\Search\PersonSearch - arguments: - - "@doctrine.orm.entity_manager" - - "@security.token_storage" - - "@chill.main.security.authorization.helper" - - "@chill_main.paginator_factory" - calls: - - ['setContainer', ["@service_container"]] + Chill\PersonBundle\Search\PersonSearch: + autowire: true tags: - { name: chill.search, alias: 'person_regular' } Chill\PersonBundle\Search\SimilarityPersonSearch: - arguments: - - "@doctrine.orm.entity_manager" - - "@security.token_storage" - - "@chill.main.security.authorization.helper" - - "@chill_main.paginator_factory" - - '@chill.person.search_person' - calls: - - ['setContainer', ["@service_container"]] + autowire: true tags: - { name: chill.search, alias: 'person_similarity' }