* * 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 . */ namespace Chill\PersonBundle\Search; use Chill\MainBundle\Search\AbstractSearch; use Doctrine\ORM\EntityManagerInterface; 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; class PersonSearch extends AbstractSearch implements ContainerAwareInterface, HasAdvancedSearchFormInterface { use ContainerAwareTrait; /** * * @var EntityManagerInterface */ private $em; /** * * @var \Chill\MainBundle\Entity\User */ private $user; /** * * @var AuthorizationHelper */ private $helper; /** * * @var PaginatorFactory */ protected $paginatorFactory; const NAME = "person_regular"; public function __construct( EntityManagerInterface $em, TokenStorageInterface $tokenStorage, AuthorizationHelper $helper, PaginatorFactory $paginatorFactory) { $this->em = $em; $this->user = $tokenStorage->getToken()->getUser(); $this->helper = $helper; $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'); } } /* * (non-PHPdoc) * @see \Chill\MainBundle\Search\SearchInterface::getOrder() */ public function getOrder() { return 100; } /* * (non-PHPdoc) * @see \Chill\MainBundle\Search\SearchInterface::isActiveByDefault() */ public function isActiveByDefault() { return true; } public function supports($domain, $format) { return 'person' === $domain; } /* * (non-PHPdoc) * @see \Chill\MainBundle\Search\SearchInterface::renderResult() */ 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') { return $this->container->get('templating')->render('ChillPersonBundle:Person:list.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']), 'total' => $total, 'start' => $start, 'search_name' => self::NAME, 'preview' => $options[SearchInterface::SEARCH_PREVIEW_OPTION], 'paginator' => $paginator )); } elseif ($format === 'json') { return [ 'results' => $this->search($terms, $start, $limit, \array_merge($options, [ 'simplify' => true ])), 'pagination' => [ 'more' => $paginator->hasNextPage() ] ]; } } /** * * @param string $pattern * @param int $start * @param int $limit * @param array $options * @return Person[] */ protected function search(array $terms, $start, $limit, array $options = array()) { $qb = $this->createQuery($terms, 'search'); if ($options['simplify'] ?? false) { $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 ($options['simplify'] ?? false) { return $qb->getQuery()->getResult(Query::HYDRATE_ARRAY); } else { return $qb->getQuery()->getResult(); } } protected function count(array $terms) { $qb = $this->createQuery($terms); $qb->select('COUNT(p.id)'); 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.'%'); } } //restraint center for security $reachableCenters = $this->helper->getReachableCenters($this->user, new Role('CHILL_PERSON_SEE')); $qb->andWhere($qb->expr() ->in('p.center', ':centers')) ->setParameter('centers', $reachableCenters) ; $this->_cacheQuery[$cacheKey] = $qb; return clone $qb; } public function buildForm(FormBuilderInterface $builder) { $builder ->add('_default', TextType::class, [ 'label' => 'First name or Last name', 'required' => false ]) ->add('firstname', TextType::class, [ 'label' => 'First name', 'required' => false ]) ->add('lastname', TextType::class, [ 'label' => 'Last name', 'required' => false ]) ->add('birthdate-after', ChillDateType::class, [ 'label' => 'Birthdate after', 'required' => false ]) ->add('birthdate', ChillDateType::class, [ 'label' => 'Birthdate', 'required' => false ]) ->add('birthdate-before', ChillDateType::class, [ 'label' => 'Birthdate before', 'required' => false ]) ->add('gender', ChoiceType::class, [ 'choices' => [ 'Man' => Person::MALE_GENDER, 'Woman' => Person::FEMALE_GENDER ], 'label' => 'Gender', 'required' => false ]) ; } public function convertFormDataToQuery(array $data) { $string = '@person '; $string .= empty($data['_default']) ? '' : $data['_default'].' '; foreach(['firstname', 'lastname', 'gender'] as $key) { $string .= empty($data[$key]) ? '' : $key.':'. // add quote if contains spaces (strpos($data[$key], ' ') !== false ? '"'.$data[$key].'"': $data[$key]) .' '; } foreach (['birthdate', 'birthdate-before', 'birthdate-after'] as $key) { $string .= empty($data[$key]) ? '' : $key.':'.$data[$key]->format('Y-m-d').' ' ; } return $string; } public function convertTermsToFormData(array $terms) { foreach(['firstname', 'lastname', 'gender', '_default'] as $key) { $data[$key] = $terms[$key] ?? null; } // parse dates 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 for $key is " . 'not parsable', 0, $ex); } } $data[$key] = $date ?? null; } return $data; } public function getAdvancedSearchTitle() { return 'Search within persons'; } }