mirror of
				https://gitlab.com/Chill-Projet/chill-bundles.git
				synced 2025-10-31 01:08:26 +00:00 
			
		
		
		
	Merge branch 'features/add-search-api-for-users' into 'master'
extend search api to users See merge request Chill-Projet/chill-bundles!121
This commit is contained in:
		| @@ -22,7 +22,9 @@ | ||||
|  | ||||
| namespace Chill\MainBundle\Controller; | ||||
|  | ||||
| use Chill\MainBundle\Search\SearchApiNoQueryException; | ||||
| use Chill\MainBundle\Serializer\Model\Collection; | ||||
| use GuzzleHttp\Psr7\Response; | ||||
| use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | ||||
| use Symfony\Component\HttpFoundation\Request; | ||||
| use Chill\MainBundle\Search\UnknowSearchDomainException; | ||||
| @@ -33,6 +35,7 @@ use Symfony\Component\Form\Extension\Core\Type\SubmitType; | ||||
| use Symfony\Component\Form\Extension\Core\Type\FormType; | ||||
| use Symfony\Component\HttpFoundation\JsonResponse; | ||||
| use Chill\MainBundle\Search\SearchProvider; | ||||
| use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; | ||||
| use Symfony\Contracts\Translation\TranslatorInterface; | ||||
| use Chill\MainBundle\Pagination\PaginatorFactory; | ||||
| use Chill\MainBundle\Search\SearchApi; | ||||
| @@ -46,15 +49,15 @@ use Symfony\Component\HttpFoundation\Exception\BadRequestException; | ||||
| class SearchController extends AbstractController | ||||
| { | ||||
|     protected SearchProvider $searchProvider; | ||||
|      | ||||
|  | ||||
|     protected TranslatorInterface $translator; | ||||
|      | ||||
|  | ||||
|     protected PaginatorFactory $paginatorFactory; | ||||
|  | ||||
|     protected SearchApi $searchApi; | ||||
|      | ||||
|  | ||||
|     function __construct( | ||||
|         SearchProvider $searchProvider,  | ||||
|         SearchProvider $searchProvider, | ||||
|         TranslatorInterface $translator, | ||||
|         PaginatorFactory $paginatorFactory, | ||||
|         SearchApi $searchApi | ||||
| @@ -65,14 +68,14 @@ class SearchController extends AbstractController | ||||
|         $this->searchApi = $searchApi; | ||||
|     } | ||||
|  | ||||
|      | ||||
|  | ||||
|     public function searchAction(Request $request, $_format) | ||||
|     { | ||||
|         $pattern = $request->query->get('q', ''); | ||||
|          | ||||
|  | ||||
|         if ($pattern === ''){ | ||||
|             switch($_format) { | ||||
|                 case 'html':  | ||||
|                 case 'html': | ||||
|                     return $this->render('@ChillMain/Search/error.html.twig', | ||||
|                           array( | ||||
|                              'message' => $this->translator->trans("Your search is empty. " | ||||
| @@ -86,16 +89,16 @@ class SearchController extends AbstractController | ||||
|                     ]); | ||||
|             } | ||||
|         } | ||||
|          | ||||
|  | ||||
|         $name = $request->query->get('name', NULL); | ||||
|          | ||||
|  | ||||
|         try { | ||||
|             if ($name === NULL) { | ||||
|                 if ($_format === 'json') { | ||||
|                     return new JsonResponse('Currently, we still do not aggregate results ' | ||||
|                         . 'from different providers', JsonResponse::HTTP_BAD_REQUEST); | ||||
|                 } | ||||
|                  | ||||
|  | ||||
|                 // no specific search selected. Rendering result in "preview" mode | ||||
|                 $results = $this->searchProvider | ||||
|                     ->getSearchResults( | ||||
| @@ -119,7 +122,7 @@ class SearchController extends AbstractController | ||||
|                                 ), | ||||
|                             $_format | ||||
|                             )]; | ||||
|                  | ||||
|  | ||||
|                 if ($_format === 'json') { | ||||
|                     return new JsonResponse(\reset($results)); | ||||
|                 } | ||||
| @@ -141,8 +144,8 @@ class SearchController extends AbstractController | ||||
|                      'pattern' => $pattern | ||||
|                   )); | ||||
|         } | ||||
|          | ||||
|      | ||||
|  | ||||
|  | ||||
|         return $this->render('@ChillMain/Search/list.html.twig', | ||||
|               array('results' => $results, 'pattern' => $pattern) | ||||
|               ); | ||||
| @@ -159,29 +162,33 @@ class SearchController extends AbstractController | ||||
|                 ." one type"); | ||||
|         } | ||||
|  | ||||
|         $collection = $this->searchApi->getResults($query, $types, []); | ||||
|         try { | ||||
|             $collection = $this->searchApi->getResults($query, $types, []); | ||||
|         } catch (SearchApiNoQueryException $e) { | ||||
|             throw new BadRequestHttpException($e->getMessage(), $e); | ||||
|         } | ||||
|  | ||||
|         return $this->json($collection); | ||||
|         return $this->json($collection, \Symfony\Component\HttpFoundation\Response::HTTP_OK, [], [ "groups" =>  ["read"]]); | ||||
|     } | ||||
|      | ||||
|  | ||||
|     public function advancedSearchListAction(Request $request) | ||||
|     { | ||||
|         /* @var $variable Chill\MainBundle\Search\SearchProvider */ | ||||
|         $searchProvider = $this->searchProvider; | ||||
|         $advancedSearchProviders = $searchProvider | ||||
|             ->getHasAdvancedFormSearchServices(); | ||||
|          | ||||
|  | ||||
|         if(\count($advancedSearchProviders) === 1) { | ||||
|             \reset($advancedSearchProviders); | ||||
|              | ||||
|  | ||||
|             return $this->redirectToRoute('chill_main_advanced_search', [ | ||||
|                 'name' => \key($advancedSearchProviders) | ||||
|             ]); | ||||
|         } | ||||
|          | ||||
|  | ||||
|         return $this->render('@ChillMain/Search/choose_list.html.twig'); | ||||
|     } | ||||
|      | ||||
|  | ||||
|     public function advancedSearchAction($name, Request $request) | ||||
|     { | ||||
|         try { | ||||
| @@ -190,22 +197,22 @@ class SearchController extends AbstractController | ||||
|             /* @var $variable Chill\MainBundle\Search\HasAdvancedSearchFormInterface */ | ||||
|             $search = $this->searchProvider | ||||
|                         ->getHasAdvancedFormByName($name); | ||||
|          | ||||
|  | ||||
|         } catch (\Chill\MainBundle\Search\UnknowSearchNameException $e) { | ||||
|             throw $this->createNotFoundException("no advanced search for " | ||||
|                 . "$name"); | ||||
|         } | ||||
|          | ||||
|  | ||||
|         if ($request->query->has('q')) { | ||||
|             $data = $search->convertTermsToFormData($searchProvider->parse( | ||||
|                 $request->query->get('q'))); | ||||
|         } | ||||
|          | ||||
|  | ||||
|         $form = $this->createAdvancedSearchForm($name, $data ?? []); | ||||
|          | ||||
|  | ||||
|         if ($request->isMethod(Request::METHOD_POST)) { | ||||
|             $form->handleRequest($request); | ||||
|              | ||||
|  | ||||
|             if ($form->isValid()) { | ||||
|                 $pattern = $this->searchProvider | ||||
|                     ->getHasAdvancedFormByName($name) | ||||
| @@ -215,8 +222,8 @@ class SearchController extends AbstractController | ||||
|                     'q' => $pattern, 'name' => $name | ||||
|                 ]); | ||||
|             } | ||||
|         }  | ||||
|          | ||||
|         } | ||||
|  | ||||
|         return $this->render('@ChillMain/Search/advanced_search.html.twig', | ||||
|             [ | ||||
|                 'form' => $form->createView(), | ||||
| @@ -224,15 +231,15 @@ class SearchController extends AbstractController | ||||
|                 'title' => $search->getAdvancedSearchTitle() | ||||
|             ]); | ||||
|     } | ||||
|      | ||||
|  | ||||
|     protected function createAdvancedSearchForm($name, array $data = []) | ||||
|     { | ||||
|         $builder = $this | ||||
|             ->get('form.factory') | ||||
|             ->createNamedBuilder( | ||||
|                 null, | ||||
|                 FormType::class,  | ||||
|                 $data,  | ||||
|                 FormType::class, | ||||
|                 $data, | ||||
|                 [ 'method' => Request::METHOD_POST ] | ||||
|             ); | ||||
|  | ||||
| @@ -240,12 +247,12 @@ class SearchController extends AbstractController | ||||
|             ->getHasAdvancedFormByName($name) | ||||
|             ->buildForm($builder) | ||||
|             ; | ||||
|          | ||||
|  | ||||
|         $builder->add('submit', SubmitType::class, [ | ||||
|             'label' => 'Search' | ||||
|         ]); | ||||
|          | ||||
|  | ||||
|         return $builder->getForm(); | ||||
|     } | ||||
|      | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,59 @@ | ||||
| <?php | ||||
|  | ||||
| namespace Chill\MainBundle\Search\Entity; | ||||
|  | ||||
| use Chill\MainBundle\Repository\UserRepository; | ||||
| use Chill\MainBundle\Search\SearchApiInterface; | ||||
| use Chill\MainBundle\Search\SearchApiQuery; | ||||
|  | ||||
| class SearchUserApiProvider implements SearchApiInterface | ||||
| { | ||||
|     private UserRepository $userRepository; | ||||
|  | ||||
|     /** | ||||
|      * @param UserRepository $userRepository | ||||
|      */ | ||||
|     public function __construct(UserRepository $userRepository) | ||||
|     { | ||||
|         $this->userRepository = $userRepository; | ||||
|     } | ||||
|  | ||||
|     public function provideQuery(string $pattern, array $parameters): SearchApiQuery | ||||
|     { | ||||
|         $query = new SearchApiQuery(); | ||||
|         $query | ||||
|             ->setSelectKey("user") | ||||
|             ->setSelectJsonbMetadata("jsonb_build_object('id', u.id)") | ||||
|             ->setSelectPertinence("GREATEST(SIMILARITY(LOWER(UNACCENT(?)), u.usernamecanonical), | ||||
|                 SIMILARITY(LOWER(UNACCENT(?)), u.emailcanonical))", [ $pattern, $pattern ]) | ||||
|             ->setFromClause("users AS u") | ||||
|             ->setWhereClause("SIMILARITY(LOWER(UNACCENT(?)), u.usernamecanonical) > 0.15 | ||||
|                 OR | ||||
|                 SIMILARITY(LOWER(UNACCENT(?)), u.emailcanonical) > 0.15 | ||||
|             ", [ $pattern, $pattern ]); | ||||
|  | ||||
|         return $query; | ||||
|     } | ||||
|  | ||||
|     public function supportsTypes(string $pattern, array $types, array $parameters): bool | ||||
|     { | ||||
|         return \in_array('user', $types); | ||||
|     } | ||||
|  | ||||
|     public function prepare(array $metadatas): void | ||||
|     { | ||||
|         $ids = \array_map(fn($m) => $m['id'], $metadatas); | ||||
|  | ||||
|         $this->userRepository->findBy([ 'id' => $ids ]); | ||||
|     } | ||||
|  | ||||
|     public function supportsResult(string $key, array $metadatas): bool | ||||
|     { | ||||
|         return $key === 'user'; | ||||
|     } | ||||
|  | ||||
|     public function getResult(string $key, array $metadata, float $pertinence) | ||||
|     { | ||||
|         return $this->userRepository->find($metadata['id']); | ||||
|     } | ||||
| } | ||||
| @@ -2,6 +2,7 @@ | ||||
|  | ||||
| namespace Chill\MainBundle\Search; | ||||
|  | ||||
| use Chill\MainBundle\Search\Entity\SearchUserApiProvider; | ||||
| use Chill\MainBundle\Serializer\Model\Collection; | ||||
| use Chill\MainBundle\Pagination\PaginatorFactory; | ||||
| use Chill\PersonBundle\Search\SearchPersonApiProvider; | ||||
| @@ -25,12 +26,14 @@ class SearchApi | ||||
|         EntityManagerInterface $em, | ||||
|         SearchPersonApiProvider $searchPerson, | ||||
|         ThirdPartyApiSearch $thirdPartyApiSearch, | ||||
|         SearchUserApiProvider $searchUser, | ||||
|         PaginatorFactory $paginator | ||||
|     ) | ||||
|     { | ||||
|         $this->em = $em; | ||||
|         $this->providers[] = $searchPerson; | ||||
|         $this->providers[] = $thirdPartyApiSearch; | ||||
|         $this->providers[] = $searchUser; | ||||
|         $this->paginator = $paginator; | ||||
|     } | ||||
|  | ||||
| @@ -41,6 +44,10 @@ class SearchApi | ||||
|     { | ||||
|         $queries = $this->findQueries($pattern, $types, $parameters); | ||||
|  | ||||
|         if (0 === count($queries)) { | ||||
|             throw new SearchApiNoQueryException($pattern, $types, $parameters); | ||||
|         } | ||||
|  | ||||
|         $total = $this->countItems($queries, $types, $parameters); | ||||
|         $paginator = $this->paginator->create($total); | ||||
|  | ||||
| @@ -49,9 +56,7 @@ class SearchApi | ||||
|         $this->prepareProviders($rawResults); | ||||
|         $results = $this->buildResults($rawResults); | ||||
|  | ||||
|         $collection = new Collection($results, $paginator); | ||||
|  | ||||
|         return $collection; | ||||
|         return new Collection($results, $paginator); | ||||
|     } | ||||
|  | ||||
|     private function findQueries($pattern, array $types, array $parameters): array | ||||
| @@ -77,7 +82,7 @@ class SearchApi | ||||
|         $rsmCount->addScalarResult('count', 'count'); | ||||
|         $countNq = $this->em->createNativeQuery($countQuery, $rsmCount); | ||||
|         $countNq->setParameters($parameters); | ||||
|          | ||||
|  | ||||
|         return $countNq->getSingleScalarResult(); | ||||
|     } | ||||
|  | ||||
| @@ -130,7 +135,7 @@ class SearchApi | ||||
|  | ||||
|         $nq = $this->em->createNativeQuery($union, $rsm); | ||||
|         $nq->setParameters($parameters); | ||||
|          | ||||
|  | ||||
|         return $nq->getResult(); | ||||
|     } | ||||
|  | ||||
| @@ -142,7 +147,7 @@ class SearchApi | ||||
|                 if ($p->supportsResult($r['key'], $r['metadata'])) { | ||||
|                     $metadatas[$k][] = $r['metadata']; | ||||
|                     break; | ||||
|                 }  | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @@ -161,7 +166,7 @@ class SearchApi | ||||
|                             $p->getResult($r['key'], $r['metadata'], $r['pertinence']) | ||||
|                         ); | ||||
|                     break; | ||||
|                 }  | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,23 @@ | ||||
| <?php | ||||
|  | ||||
| namespace Chill\MainBundle\Search; | ||||
|  | ||||
| use Throwable; | ||||
|  | ||||
| class SearchApiNoQueryException extends \RuntimeException | ||||
| { | ||||
|     private string $pattern; | ||||
|     private array $types; | ||||
|     private array $parameters; | ||||
|  | ||||
|     public function __construct(string $pattern = "", array $types = [], array $parameters = [], $code = 0, Throwable $previous = null) | ||||
|     { | ||||
|         $typesStr = \implode(", ", $types); | ||||
|         $message = "No query for this search: pattern : {$pattern}, types: {$typesStr}"; | ||||
|         $this->pattern = $pattern; | ||||
|         $this->types = $types; | ||||
|         $this->parameters = $parameters; | ||||
|  | ||||
|         parent::__construct($message, $code, $previous); | ||||
|     } | ||||
| } | ||||
| @@ -127,8 +127,6 @@ paths: | ||||
|         - person | ||||
|         - thirdparty | ||||
|       description: > | ||||
|         **Warning**: This is currently a stub (not really implemented | ||||
|  | ||||
|         The search is performed across multiple entities. The entities must be listed into | ||||
|         `type` parameters. | ||||
|  | ||||
| @@ -152,6 +150,7 @@ paths: | ||||
|               enum: | ||||
|                 - person | ||||
|                 - thirdparty | ||||
|                 - user | ||||
|       responses: | ||||
|         200: | ||||
|           description: "OK" | ||||
|   | ||||
| @@ -7,3 +7,8 @@ services: | ||||
|     Chill\MainBundle\Search\SearchApi: | ||||
|         autowire: true | ||||
|         autoconfigure: true | ||||
|  | ||||
|     Chill\MainBundle\Search\Entity\: | ||||
|         autowire: true | ||||
|         autoconfigure: true | ||||
|         resource: '../../Search/Entity' | ||||
|   | ||||
		Reference in New Issue
	
	Block a user