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

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