src/Aqarmap/Bundle/SearchBundle/Controller/Api/V4/ListingSearchController.php line 511

  1. use OpenApi\Attributes as OA;
  2. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  3. use Symfony\Component\HttpFoundation\JsonResponse;
  4. use Symfony\Component\HttpFoundation\Request;
  5. use Symfony\Component\HttpFoundation\Response;
  6. use Symfony\Component\HttpKernel\Attribute\Cache;
  7. use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
  8. use Symfony\Component\Messenger\MessageBusInterface;
  9. use Symfony\Contracts\Cache\CacheInterface;
  10. use Symfony\Contracts\Cache\ItemInterface;
  11. use Symfony\Contracts\HttpClient\HttpClientInterface;
  12. class ListingSearchController extends BaseController
  13. {
  14. public function __construct(private readonly BuilderDirector $builderDirector, private readonly MediatorInterface $mediator, private readonly EventDispatcherInterface $eventDispatcher, private readonly SerializerInterface $serializer, RelatedResultService $relatedResultService, private readonly LocationRepository $locationRepository, CustomParagraphRepository $customParagraphRepository, private readonly SEOListingSearchService $seoListingSearchService, private readonly SectionRepository $sectionRepository, private readonly PropertyTypeRepository $propertyTypeRepository, ListingFaqService $listingFaqService, private readonly MessageBusInterface $messageBus)
  15. {
  16. }
  17. /**
  18. * Listings Search V4 ( Legacy code ).
  19. */
  20. #[Rest\Get('/api/v4/listings/search-legacy', name: 'legacy_aqarmap_api_listings_search_v4', options: ['i18n' => false])]
  21. #[Rest\QueryParam(name: 'propertyType', requirements: '\d+', default: null, description: 'Property Type ID')]
  22. #[Rest\QueryParam(name: 'location', requirements: '\d+', default: null, description: 'Location ID comma spereted')]
  23. #[Rest\QueryParam(name: 'section', requirements: '\d+', default: null, description: 'Section ID')]
  24. #[Rest\QueryParam(name: 'bounds', requirements: 'South-West Latitude ,
  25. Longitude , North-East Latitude , Longitude')] // default=null,
  26. #[Rest\QueryParam(name: 'minPrice', requirements: '\d+', default: null, description: 'Minimum Prices')]
  27. #[Rest\QueryParam(name: 'maxPrice', requirements: '\d+', default: null, description: 'Maximum Prices')]
  28. #[Rest\QueryParam(name: 'minArea', requirements: '\d+', default: null, description: 'Minimum Area')]
  29. #[Rest\QueryParam(name: 'maxArea', requirements: '\d+', default: null, description: 'Maximum Area')]
  30. #[Rest\QueryParam(name: 'minFloor', requirements: '\d+', default: null, description: 'Minimum Floor')]
  31. #[Rest\QueryParam(name: 'minRoom', requirements: '\d+', default: null, description: 'Minimum Room')]
  32. #[Rest\QueryParam(name: 'floor', requirements: '\d+', default: null, description: 'Floor Number')]
  33. #[Rest\QueryParam(name: 'room', requirements: '\d+', default: null, description: 'Number of Rooms')]
  34. #[Rest\QueryParam(name: 'baths', requirements: '\d+', default: null, description: 'Number of Baths')]
  35. #[Rest\QueryParam(name: 'finishType', default: null, description: 'finish Type')]
  36. #[Rest\QueryParam(name: 'sellerRole', requirements: '\d+', default: null, description: 'sellerRole')]
  37. #[Rest\QueryParam(name: 'paymentMethod', requirements: '\d+', default: null, description: 'paymentMethod')]
  38. #[Rest\QueryParam(name: 'deliveryYear', requirements: '\d+', default: null, description: 'deliveryYear')]
  39. #[Rest\QueryParam(name: 'bath', default: null, description: 'Number of Baths')]
  40. #[Rest\QueryParam(name: 'photos', requirements: '(1)|(0)', default: '0', description: 'Get only listings with photos', strict: true, nullable: true)]
  41. #[Rest\QueryParam(name: 'isMortgage', requirements: '(1)|(0)', default: '0', description: 'Get only listings that support mortgage', strict: true, nullable: true)]
  42. #[Rest\QueryParam(name: 'eligibleForMortgage', description: 'Get listings that has mortgage Percentage', strict: true, nullable: true)]
  43. #[Rest\QueryParam(name: 'page', requirements: '\d+', default: 1, description: 'Page number, starting from 1.', nullable: true)]
  44. #[Rest\QueryParam(name: 'limit', requirements: '\d+', default: 10, description: 'Number of items per page.', nullable: true)]
  45. #[Rest\QueryParam(name: 'sort', requirements: 'price|area', default: null, description: 'Sort search results by price or area.', nullable: true)]
  46. #[Rest\QueryParam(name: 'direction', requirements: 'asc|desc', default: 'asc', description: 'Ascending (A to Z, 0 to 9), Descending (Z to A, 9 to 0)', nullable: true)]
  47. #[Rest\QueryParam(name: 'keywordSearch', description: "Search by keyword in listing's title, description and address")]
  48. #[Rest\QueryParam(name: 'unitOnly', requirements: '(1)|(0)', description: 'Get compound units only', nullable: true)]
  49. #[Rest\View]
  50. #[Cache(expires: '+2 hours', maxage: '+2 hours', smaxage: '+2 hours', public: true, vary: ['Accept-Language', 'X-Accept-Version', 'Accept'])]
  51. #[OA\Parameter(name: 'propertyType', description: 'Property Type ID', in: 'query', required: false)]
  52. #[OA\Parameter(name: 'location', description: 'Location ID comma spereted', in: 'query', required: false)]
  53. #[OA\Parameter(name: 'section', description: 'Section ID', in: 'query', required: false)]
  54. #[OA\Parameter(name: 'bounds', description: 'Map bounds (example: 24.6275450,46.6363017,24.6977461,46.817232)', in: 'query', required: false)]
  55. #[OA\Parameter(name: 'minPrice', description: 'Minimum Prices', in: 'query', required: false)]
  56. #[OA\Parameter(name: 'maxPrice', description: 'Maximum Prices', in: 'query', required: false)]
  57. #[OA\Parameter(name: 'minArea', description: 'Minimum Area', in: 'query', required: false)]
  58. #[OA\Parameter(name: 'maxArea', description: 'Maximum Area', in: 'query', required: false)]
  59. #[OA\Parameter(name: 'minFloor', description: 'Minimum Floor', in: 'query', required: false)]
  60. #[OA\Parameter(name: 'minRoom', description: 'Minimum Room', in: 'query', required: false)]
  61. #[OA\Parameter(name: 'floor', description: 'Floor Number', in: 'query', required: false)]
  62. #[OA\Parameter(name: 'room', description: 'Number of Rooms', in: 'query', required: false)]
  63. #[OA\Parameter(name: 'baths', description: 'Number of Baths', in: 'query', required: false)]
  64. #[OA\Parameter(name: 'finishType', description: 'finish Type', in: 'query', required: false)]
  65. #[OA\Parameter(name: 'sellerRole', description: 'sellerRole', in: 'query', required: false)]
  66. #[OA\Parameter(name: 'paymentMethod', description: 'paymentMethod', in: 'query', required: false)]
  67. #[OA\Parameter(name: 'deliveryYear', description: 'deliveryYear', in: 'query', required: false)]
  68. #[OA\Parameter(name: 'bath', description: 'Number of Baths', in: 'query', required: false)]
  69. #[OA\Parameter(name: 'photos', description: 'Get only listings with photos', in: 'query', required: false)]
  70. #[OA\Parameter(name: 'isMortgage', description: 'Get only listings that support mortgage', in: 'query', required: false)]
  71. #[OA\Parameter(name: 'eligibleForMortgage', description: 'Get listings that has mortgage Percentage', in: 'query', required: false)]
  72. #[OA\Parameter(name: 'page', description: 'Page number, starting from 1.', in: 'query', required: false)]
  73. #[OA\Parameter(name: 'limit', description: 'Number of items per page.', in: 'query', required: false)]
  74. #[OA\Parameter(name: 'sort', description: 'Sort search results by price or area.', in: 'query', required: false)]
  75. #[OA\Parameter(name: 'direction', description: 'Ascending (A to Z, 0 to 9), Descending (Z to A, 9 to 0)', in: 'query', required: false)]
  76. #[OA\Parameter(name: 'keywordSearch', description: "Search by keyword in listing's title, description and address", in: 'query', required: false)]
  77. #[OA\Parameter(name: 'unitOnly', description: 'Get compound units only', in: 'query', required: false)]
  78. #[OA\Response(response: 500, description: 'Returned when something went wrong, for example if you entered non existing propertyType ID')]
  79. public function getSearchListings(Request $request, CacheInterface $cache): Response
  80. {
  81. $criteria = $this->builderDirector->build($request)->getResult();
  82. $locations = explode(',', $request->query->get('location', ''));
  83. $request->query->set('locations', $locations);
  84. if (!empty($criteria['keywordSearch'])) {
  85. $this->eventDispatcher->dispatch(new SearchTriggerEvent($request));
  86. }
  87. if ($this->getUser() instanceof User) {
  88. $this->messageBus->dispatch(new Search($request->query, $this->getUser()));
  89. }
  90. $criteria['status'] = ListingStatus::LIVE;
  91. $cacheKey = sprintf('api_v4_listings_search_%s_%s', $request->getLocale(), md5(http_build_query($request->query->all())));
  92. $cachedSerialize = $cache->getItem($cacheKey);
  93. if (!$cachedSerialize->isHit() || empty($cachedSerialize->get())) {
  94. $result = $this->mediator->start($criteria)->getResults();
  95. $mapped = new ListingDataMapper();
  96. $mapped->setMapListingAttributes(true);
  97. $mapped->setMapListingMainPhoto(true);
  98. $mapped->setMapListingPhotos(true);
  99. $data = [
  100. 'default' => $this->makeMappedPaginatedBody($result['searchResults'], $mapped),
  101. 'related' => [],
  102. ];
  103. $data = $this->serializer->serialize(
  104. $data,
  105. 'json',
  106. SerializationContext::create()
  107. ->setGroups('SearchV4')
  108. ->enableMaxDepthChecks()
  109. );
  110. $cachedSerialize->set($data);
  111. $cachedSerialize->expiresAfter(3600 * 3);
  112. $cache->save($cachedSerialize);
  113. }
  114. return new Response($cachedSerialize->get());
  115. }
  116. /**
  117. * Related Listings Search V4.
  118. */
  119. #[Rest\Get('/api/v4/listings/search/related', name: 'aqarmap_api_get_related_results_v4', options: ['i18n' => false])]
  120. #[Rest\QueryParam(name: 'propertyType', requirements: '\d+', default: null, description: 'Property Type ID')]
  121. #[Rest\QueryParam(name: 'location', requirements: '\d+', default: null, description: 'Location ID comma spereted')]
  122. #[Rest\QueryParam(name: 'section', requirements: '\d+', default: null, description: 'Section ID')]
  123. #[Rest\QueryParam(name: 'page', requirements: '\d+', default: 1, description: 'Page number, starting from 1.', nullable: true)]
  124. #[Rest\QueryParam(name: 'limit', requirements: '\d+', default: 10, description: 'Number of items per page.', nullable: true)]
  125. #[Rest\QueryParam(name: 'sort', requirements: 'price|area', default: null, description: 'Sort search results by price or area.', nullable: true)]
  126. #[Rest\QueryParam(name: 'direction', requirements: 'asc|desc', default: 'asc', description: 'Ascending (A to Z, 0 to 9), Descending (Z to A, 9 to 0)', nullable: true)]
  127. #[Rest\View(serializerGroups: ['DefaultV4', 'SearchV4'])]
  128. #[OA\Parameter(name: 'propertyType', description: 'Property Type ID', in: 'query', required: false)]
  129. #[OA\Parameter(name: 'location', description: 'Location ID comma seperated', in: 'query', required: false)]
  130. #[OA\Parameter(name: 'section', description: 'Section ID', in: 'query', required: false)]
  131. #[OA\Parameter(name: 'page', description: 'Page number, starting from 1.', in: 'query', required: false)]
  132. #[OA\Parameter(name: 'limit', description: 'Number of items per page.', in: 'query', required: false)]
  133. #[OA\Parameter(name: 'sort', description: 'Sort search results by price or area.', in: 'query', required: false)]
  134. #[OA\Parameter(name: 'direction', description: 'Ascending (A to Z, 0 to 9), Descending (Z to A, 9 to 0)', in: 'query', required: false)]
  135. #[OA\Response(response: 500, description: 'Returned when something went wrong, for example if you entered non existing propertyType ID')]
  136. public function getRelatedSearchListings(Request $request, HttpClientInterface $searchClient, SerializerInterface $jmsSerializer, ListingRepository $listingRepository): JsonResponse
  137. {
  138. $response = $searchClient->request('GET', '/api/listing/nearest', [
  139. 'query' => $request->query->all(),
  140. 'headers' => [
  141. 'Accept-Language' => $request->getPreferredLanguage(),
  142. ],
  143. ]);
  144. if (200 === $response->getStatusCode()) {
  145. $data = $response->toArray();
  146. if (isset($data['data']) && is_array($data['data'])) {
  147. foreach ($data['data'] as &$item) {
  148. if (isset($item['listings']) && is_array($item['listings'])) {
  149. $listings = $listingRepository->getListingsByIds(array_column($item['listings'], 'id'))->getQuery()->getResult();
  150. $context = SerializationContext::create()
  151. ->setGroups(['DefaultV4', 'SearchV4']);
  152. $item['listings'] = json_decode(
  153. $jmsSerializer->serialize($listings, 'json', $context),
  154. true
  155. );
  156. }
  157. }
  158. }
  159. return $this->json($data);
  160. }
  161. return $this->json([
  162. 'error' => 'Failed to fetch nearest search listings',
  163. ], $response->getStatusCode());
  164. }
  165. #[Rest\Get('/api/v4/listings', name: 'aqarmap_api_get_listings', options: ['i18n' => false])]
  166. #[Rest\Get('/api/v4/listings/search', name: 'aqarmap_api_listings_search_v4', options: ['i18n' => false])]
  167. #[Rest\QueryParam(name: 'personalizedSearch', requirements: '(1)|(0)', default: '0', description: 'Enable personalized search', strict: true, nullable: true)]
  168. #[OA\Parameter(name: 'personalizedSearch', description: 'Enable personalized search (requires active subscription)', in: 'query', required: false)]
  169. public function getListings(Request $request, ListingRepository $listingRepository, ListingManager $listingManager, CacheInterface $cache, MessageBusInterface $messageBus)
  170. {
  171. /** @var UserInterface $user */
  172. $user = $this->getUser();
  173. $hasSearchScoringRole = $user && $user->hasRole('ROLE_SEARCH_SCORING');
  174. if (!$hasSearchScoringRole && ($request->get('esdebug') || $request->get('scoredebug'))) {
  175. $request->query->remove('esdebug');
  176. $request->query->remove('scoredebug');
  177. }
  178. if ($request->get('location')) {
  179. $request->query->set('locations', explode(',', $request->query->get('location', '')));
  180. }
  181. $personalizedSearch = $request->query->getBoolean('personalizedSearch', false);
  182. if ($personalizedSearch) {
  183. if (!$user instanceof User) {
  184. throw new UnauthorizedHttpException('Personalized search requires an active session. Please log in.');
  185. }
  186. if (!$user->hasSubscriptionPlan()) {
  187. throw new AccessDeniedHttpException('Active subscription required for personalized search.');
  188. }
  189. $request->query->set('personalizedSearch', true);
  190. $request->query->set('personalizedForUser', $user->getId());
  191. }
  192. if ($personalizedSearch) {
  193. $data = $this->getSerializedListingSearchResults($request, $listingRepository, $listingManager);
  194. } else {
  195. $cacheKey = sprintf('api_listings_search_v4_%s_%s', $request->getLocale(), md5(http_build_query($request->query->all())));
  196. $data = $cache->get(
  197. $cacheKey,
  198. function(ItemInterface $item) use ($request, $listingRepository, $listingManager) {
  199. $item->expiresAfter(3600 * 3);
  200. return $this->getSerializedListingSearchResults($request, $listingRepository, $listingManager);
  201. }
  202. );
  203. }
  204. return new Response($data);
  205. }
  206. private function getSerializedListingSearchResults(Request $request, ListingRepository $listingRepository, ListingManager $listingManager): string
  207. {
  208. $listingsElasticResponse = $listingManager->getListingsElasticResponse($request);
  209. $listingsQueryBuilder = $listingRepository->getListingsByIds(array_column($listingsElasticResponse['items'], 'id'));
  210. if ('aqarmap_api_listings_search_v4' === $request->attributes->get('_route')) {
  211. $listingsElasticResponse['pagination']['totalPages'] = (float) $listingsElasticResponse['pagination']['totalPages'];
  212. }
  213. $listings = $listingsQueryBuilder->getQuery()->getResult();
  214. $data = [
  215. 'default' => $this->mapPaginatedBody($listingsElasticResponse, $listings),
  216. 'related' => [],
  217. ];
  218. return $this->serializer->serialize(
  219. $data,
  220. 'json',
  221. SerializationContext::create()
  222. ->setGroups('SearchV4')
  223. ->enableMaxDepthChecks()
  224. );
  225. }
  226. #[Rest\Get('/api/v4/listings/debug', options: ['i18n' => false], name: 'aqarmap_api_get_listings_debug')]
  227. public function getListingsEsDebug(Request $request, ListingManager $listingManager)
  228. {
  229. /** @var UserInterface $user */
  230. $user = $this->getUser();
  231. if (!($user && $user->hasRole('ROLE_SEARCH_SCORING'))) {
  232. throw new UnauthorizedHttpException();
  233. }
  234. $request->query->set('esdebug', 1);
  235. if ($request->get('location')) {
  236. $request->query->set('locations', explode(',', $request->query->get('location', '')));
  237. }
  238. $listingsDebugElasticResponse = $listingManager->getListingsDebugElasticResponse($request);
  239. if ('aqarmap_api_listings_search_v4' === $request->attributes->get('_route')) {
  240. $listingsDebugElasticResponse['pagination']['totalPages'] = (float) $listingsDebugElasticResponse['pagination']['totalPages'];
  241. }
  242. $data = [
  243. 'default' => $this->mapPaginatedBody($listingsDebugElasticResponse, $listingsDebugElasticResponse['items']),
  244. ];
  245. return new JsonResponse($data);
  246. }
  247. #[Rest\Get('/api/v4/listings/search/ssr-data', name: 'aqarmap_api_get_listings_search_ssr-data', options: ['i18n' => false])]
  248. #[OA\Get(
  249. summary: 'Get listing search SSR data',
  250. description: 'Provides the pre-rendered content required for the listing search SSR page such as location hierarchy, SEO metadata and UI chips.',
  251. tags: ['Listings', 'Search']
  252. )]
  253. #[OA\Parameter(name: 'location', in: 'query', required: false, description: 'Slug of the primary location to load contextual data for.', schema: new OA\Schema(type: 'string'))]
  254. #[OA\Parameter(
  255. name: 'locations[]',
  256. in: 'query',
  257. required: false,
  258. description: 'Optional list of location slugs used to resolve additional data.',
  259. schema: new OA\Schema(type: 'array', items: new OA\Items(type: 'string'))
  260. )]
  261. #[OA\Parameter(name: 'section', in: 'query', required: false, description: 'Section slug to scope the returned data.', schema: new OA\Schema(type: 'string'))]
  262. #[OA\Parameter(name: 'propertyType', in: 'query', required: false, description: 'Property type slug to scope the returned data.', schema: new OA\Schema(type: 'string'))]
  263. #[OA\Parameter(name: 'byOwnerOnly', in: 'query', required: false, description: 'Whether to limit content to owner only listings.', schema: new OA\Schema(type: 'boolean'))]
  264. #[OA\Response(
  265. response: 200,
  266. description: 'Successful response containing SSR data required by the listing search page.',
  267. content: new OA\JsonContent(
  268. type: 'object',
  269. properties: [
  270. new OA\Property(
  271. property: 'locationChildren',
  272. description: 'Hierarchy of child locations related to the requested location.',
  273. type: 'array',
  274. items: new OA\Items(type: 'object')
  275. ),
  276. new OA\Property(
  277. property: 'longTail',
  278. description: 'SEO long-tail content blocks generated for the section, property type and location.',
  279. type: 'array',
  280. items: new OA\Items(type: 'object')
  281. ),
  282. new OA\Property(
  283. property: 'customParagraph',
  284. description: 'Custom paragraph content tailored to the current search context.',
  285. type: 'array',
  286. items: new OA\Items(type: 'object')
  287. ),
  288. new OA\Property(
  289. property: 'slugResolver',
  290. description: 'Resolved slugs mapping for the provided filters.',
  291. type: 'array',
  292. items: new OA\Items(type: 'object')
  293. ),
  294. new OA\Property(
  295. property: 'faqData',
  296. description: 'Frequently asked questions related to the current search.',
  297. type: 'array',
  298. items: new OA\Items(type: 'object')
  299. ),
  300. new OA\Property(
  301. property: 'locationParents',
  302. description: 'Ordered list of parent locations for breadcrumb generation.',
  303. type: 'array',
  304. items: new OA\Items(type: 'object')
  305. ),
  306. new OA\Property(
  307. property: 'sections',
  308. description: 'Available sections that can be displayed in the SSR page.',
  309. type: 'array',
  310. items: new OA\Items(type: 'object')
  311. ),
  312. new OA\Property(
  313. property: 'propertyTypeChips',
  314. description: 'Property type chips rendered in the search interface.',
  315. type: 'array',
  316. items: new OA\Items(type: 'object')
  317. ),
  318. ]
  319. )
  320. )]
  321. #[OA\Response(response: 500, description: 'Returned when SSR data could not be generated or cached.')]
  322. public function getListingsSearchSSRData(Request $request, ListingManager $listingManager, CacheInterface $cache, SectionService $sectionService)
  323. {
  324. $cacheKey = sprintf('api_listings_search_ssr_data_%s_%s', $request->getLocale(), md5(http_build_query($request->query->all())));
  325. $cachedResponse = $cache->getItem($cacheKey);
  326. if (!$cachedResponse->isHit() || empty($cachedResponse->get())) {
  327. $customParagraph = $locationChildren = $slugResolver = $faqData = $locationParents = [];
  328. if ($request->get('location')) {
  329. $request->query->set('locations', explode(',', $request->query->get('location', '')));
  330. }
  331. $location = $request->query->all('locations') ? $request->query->all('locations')[0] : null;
  332. $location = $this->locationRepository->findOneBy(['slug' => $location]);
  333. $section = $this->sectionRepository->findOneBy(['slug' => $request->query->get('section')]);
  334. $propertyType = $this->propertyTypeRepository->findOneBy(['slug' => $request->query->get('propertyType')]);
  335. if ($location) {
  336. $locationChildren = $listingManager->getSerializedLocationChildren($location);
  337. $locationParents = $listingManager->getLocationParents($location, $request->getLocale());
  338. }
  339. if ($section && $propertyType && $location) {
  340. $longTail = $this->seoListingSearchService->getSerializedLongTailData($section, $propertyType, $location) ?? [];
  341. }
  342. if ($section && $propertyType) {
  343. $byOwnerOnly = filter_var($request->get('byOwnerOnly'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? false;
  344. $customParagraph = $listingManager->getSerializedCustomParagraph($section, $propertyType, $location, $byOwnerOnly) ?? [];
  345. $slugResolver = $listingManager->getSerializedResolvedSlugs($section, $propertyType, $request->query->all('locations'));
  346. $faqData = $listingManager->getFaqs($section, $propertyType, $location, $request->getLocale());
  347. }
  348. $response = [
  349. 'locationChildren' => !empty($locationChildren) ? json_decode((string) $locationChildren, true) : [],
  350. 'longTail' => !empty($longTail) ? json_decode((string) $longTail, true) : [],
  351. 'customParagraph' => !empty($customParagraph) ? json_decode((string) $customParagraph, true) : [],
  352. 'slugResolver' => !empty($slugResolver) ? json_decode((string) $slugResolver, true) : [],
  353. 'faqData' => $faqData,
  354. 'locationParents' => $locationParents,
  355. 'sections' => json_decode((string) $sectionService->getSerializedSections(), true),
  356. 'propertyTypeChips' => $listingManager->getListingPropertyTypesChips($request, $location, $section, $propertyType),
  357. ];
  358. $cachedResponse->set(json_encode($response));
  359. $cachedResponse->expiresAfter(3600 * 3);
  360. $cache->save($cachedResponse);
  361. }
  362. return new JsonResponse(json_decode((string) $cachedResponse->get(), true));
  363. }
  364. #[Rest\Get('/api/v4/listings/trigger-search', options: ['i18n' => false], name: 'aqarmap_api_trigger_search_listings_v4')]
  365. public function triggerSearchListings(Request $request, MessageBusInterface $messageBus)
  366. {
  367. $user = $this->getUser();
  368. if ($request->get('location')) {
  369. $request->query->set('locations', explode(',', $request->query->get('location', '')));
  370. }
  371. if ($request->get('keywordSearch')) {
  372. $this->eventDispatcher->dispatch(new SearchTriggerEvent($request));
  373. }
  374. if ($user) {
  375. $messageBus->dispatch(new Search($request->query, $user));
  376. }
  377. return new JsonResponse([
  378. 'statusCode' => Response::HTTP_OK,
  379. 'statusMessage' => 'Search triggered successfully!',
  380. ]);
  381. }
  382. private function mapPaginatedBody(array $result, array $listings): array
  383. {
  384. if (!$result['pagination']) {
  385. return [
  386. 'statusCode' => $this->getStatusCode(),
  387. 'statusMessage' => $this->getStatusMessage(),
  388. 'paginate' => [],
  389. 'data' => [],
  390. 'errors' => $this->getErrors(),
  391. ];
  392. }
  393. return [
  394. 'statusCode' => $this->getStatusCode(),
  395. 'statusMessage' => $this->getStatusMessage(),
  396. 'paginate' => $result['pagination'],
  397. 'data' => $listings,
  398. 'errors' => $this->getErrors(),
  399. ];
  400. }
  401. }