src/Aqarmap/Bundle/SearchBundle/Controller/Api/SearchResultsController.php line 23

  1. <?php
  2. namespace Aqarmap\Bundle\SearchBundle\Controller\Api;
  3. use Aqarmap\Bundle\ListingBundle\Builder\BuilderInterface;
  4. use Aqarmap\Bundle\ListingBundle\Entity\Location;
  5. use Aqarmap\Bundle\ListingBundle\Entity\PropertyType;
  6. use Aqarmap\Bundle\ListingBundle\Entity\Section;
  7. use Aqarmap\Bundle\ListingBundle\Repository\LocationRepository;
  8. use Aqarmap\Bundle\ListingBundle\Repository\PropertyTypeRepository;
  9. use Aqarmap\Bundle\ListingBundle\Repository\SectionRepository;
  10. use Aqarmap\Bundle\ListingBundle\Service\ListingManager;
  11. use Aqarmap\Bundle\MainBundle\Constant\CustomParagraphPlaceTypes;
  12. use Aqarmap\Bundle\MainBundle\Controller\Api\BaseController;
  13. use Aqarmap\Bundle\MainBundle\Repository\CustomParagraphRepository;
  14. use Aqarmap\Bundle\MainBundle\Service\MenuBuilderService;
  15. use Aqarmap\Bundle\SearchBundle\Services\SEOListingSearchService;
  16. use FOS\RestBundle\Controller\Annotations as Rest;
  17. use OpenApi\Attributes as OA;
  18. use Symfony\Bridge\Doctrine\Attribute\MapEntity;
  19. use Symfony\Component\HttpFoundation\Request;
  20. use Symfony\Component\HttpKernel\Attribute\Cache;
  21. class SearchResultsController extends BaseController
  22. {
  23. #[Rest\Get('/api/v2/search/slug-resolver', name: 'aqarmap_api_search_slug_resolver', options: ['i18n' => false])]
  24. #[OA\Get(summary: 'Resolve search page slugs', description: 'Resolves section, property type, and location slugs into their corresponding search page entities.', tags: ['ListingsSearch'])]
  25. #[OA\Parameter(name: 'section', description: 'Section slug.', in: 'query', required: false, schema: new OA\Schema(type: 'string'))]
  26. #[OA\Parameter(name: 'propertyType', description: 'Property Type slug.', in: 'query', required: false, schema: new OA\Schema(type: 'string'))]
  27. #[OA\Parameter(name: 'locations', description: 'Locations slugs. Can be provided as an array.', in: 'query', required: false, schema: new OA\Schema(type: 'array', items: new OA\Items(type: 'string')))]
  28. #[Rest\View(serializerGroups: ['SlugResolver'])]
  29. #[Rest\QueryParam(name: 'section', default: null, description: 'Section slug')]
  30. #[Rest\QueryParam(name: 'propertyType', default: null, description: 'Property Type slug')]
  31. #[Rest\QueryParam(name: 'locations', default: null, description: 'Locations slugs. As array.')]
  32. #[Cache(expires: '+7 days', maxage: '+7 days', smaxage: '+7 days', public: true, vary: ['Accept-Language', 'X-Accept-Version', 'Accept'])]
  33. #[OA\Response(response: 200, description: 'Resolved section, property type, and location entities for the provided slugs.')]
  34. public function resolveSearchPageSlugs(Request $request, SectionRepository $sectionRepo, PropertyTypeRepository $propertyTypeRepo, LocationRepository $locationRepo): array
  35. {
  36. if ($request->get('location')) {
  37. $request->query->set('locations', explode(',', $request->query->get('location', '')));
  38. }
  39. $locationsSlugs = $request->query->all('locations');
  40. $data = [
  41. 'section' => $sectionRepo->findOneBy(['slug' => $request->query->get('section')]),
  42. 'propertyType' => $propertyTypeRepo->findOneBy(['slug' => $request->query->get('propertyType')]),
  43. 'locations' => [],
  44. ];
  45. if (!empty($locationsSlugs) && $this->validLocationSlugArray($locationsSlugs)) {
  46. $data['locations'] = $locationRepo->getLocationsBySlugs($locationsSlugs);
  47. }
  48. return $data;
  49. }
  50. /**
  51. * Legazy.
  52. */
  53. #[Rest\Get('/api/v2/search/properties-chips-legazy', options: ['i18n' => false], name: 'aqarmap_api_properties_chips_legazy')]
  54. #[Rest\View]
  55. #[Cache(expires: '+1 days', maxage: '+1 days', smaxage: '+1 days', public: true, vary: ['Accept-Language', 'X-Accept-Version', 'Accept'])]
  56. public function getPropertiesChipsLegazy(
  57. Request $request,
  58. #[MapEntity(id: 'location')]
  59. ?Location $location,
  60. #[MapEntity(id: 'propertyType')]
  61. ?PropertyType $propertyType,
  62. #[MapEntity(id: 'section')]
  63. ?Section $section,
  64. MenuBuilderService $menuBuilderService,
  65. ) {
  66. $locale = $request->getLocale();
  67. $data = [];
  68. if ($section && $propertyType) {
  69. $criteria = [
  70. 'location' => $location,
  71. 'propertyType' => $propertyType,
  72. 'section' => $section,
  73. 'locale' => $locale,
  74. ];
  75. $data = $menuBuilderService->getSearchResultPropertyChips($criteria);
  76. }
  77. return $data;
  78. }
  79. #[Rest\Get('/api/v2/search/properties-chips', options: ['i18n' => false], name: 'aqarmap_api_properties_chips')]
  80. #[OA\Get(summary: 'Get search property chips', description: 'Returns property type chips and result counts for the active search filters.', tags: ['ListingsSearch'])]
  81. #[Rest\View]
  82. #[Cache(expires: '+1 days', maxage: '+1 days', smaxage: '+1 days', public: true, vary: ['Accept-Language', 'X-Accept-Version', 'Accept'])]
  83. #[OA\Response(response: 200, description: 'Property type chips ordered by matching listings count.')]
  84. public function getPropertiesChips(Request $request, ListingManager $listingManager, PropertyTypeRepository $propertyTypeRepository)
  85. {
  86. $result = [];
  87. $types = $listingManager->getPropertiesChipsElasticResponse($request);
  88. $typesArray = [];
  89. foreach ($types as $id => $doc_count) {
  90. $typesArray[$id] = $doc_count;
  91. }
  92. arsort($typesArray);
  93. $propertyTypes = $propertyTypeRepository->findPropertyTypes(array_keys($typesArray));
  94. foreach ($propertyTypes as $propertyType) {
  95. $id = $propertyType->getId();
  96. $result[] = [
  97. 'id' => $id,
  98. 'title' => $propertyType->getTitle(),
  99. 'slug' => $propertyType->getSlug(),
  100. 'searchResultCount' => $typesArray[$id] ?? 0,
  101. 'level' => $propertyType->getLevel(),
  102. ];
  103. }
  104. return $result;
  105. }
  106. #[Rest\Get('/api/v2/search/{section}/{propertyType}/{location}/page-metadata', options: ['i18n' => false], name: 'aqarmap_api_search_page_metadata')]
  107. #[Rest\View(serializerGroups: ['Default'])]
  108. public function getSeoData(
  109. Section $section,
  110. PropertyType $propertyType,
  111. Location $location,
  112. CustomParagraphRepository $customParagraphRepository,
  113. SEOListingSearchService $seoListingSearchService,
  114. ) {
  115. $data['customParagraph'] = $customParagraphRepository->get([
  116. 'location' => $location->getId(),
  117. 'propertyType' => $propertyType->getId(),
  118. 'section' => $section->getId(),
  119. 'place' => CustomParagraphPlaceTypes::SEARCH_RESULTS,
  120. ])->getQuery()->setMaxResults(1)->getOneOrNullResult();
  121. $data['longTail'] = $seoListingSearchService->retrieveLongTail($location, $section, $propertyType);
  122. return [
  123. 'data' => $data,
  124. ];
  125. }
  126. #[Rest\Get('/api/v2/custom-paragraph', options: ['i18n' => false], name: 'aqarmap_api_custom_paragraph')]
  127. #[Rest\View(serializerGroups: ['Default'])]
  128. public function getCustomParagraph(
  129. Request $request,
  130. #[MapEntity(id: 'location')]
  131. ?Location $location,
  132. #[MapEntity(id: 'section')]
  133. ?Section $section,
  134. #[MapEntity(id: 'propertyType')]
  135. ?PropertyType $propertyType,
  136. CustomParagraphRepository $customParagraphRepository,
  137. ) {
  138. return $customParagraphRepository->get([
  139. 'location' => $location ? $location->getId() : null,
  140. 'propertyType' => $propertyType ? $propertyType->getId() : null,
  141. 'section' => $section->getId(),
  142. 'place' => $request->query->get('place') ?: CustomParagraphPlaceTypes::SEARCH_RESULTS,
  143. ])->getQuery()->setMaxResults(1)->getOneOrNullResult();
  144. }
  145. #[Rest\Get('/api/v2/location/{location}/listing', options: ['i18n' => false], name: 'aqarmap_api_location_listing_v2')]
  146. #[Rest\QueryParam(name: 'id', default: null, description: 'Location id')]
  147. #[Rest\View(serializerGroups: ['locationListingV2'])]
  148. public function getLocationListing(Location $location)
  149. {
  150. return $location;
  151. }
  152. private function validLocationSlugArray($array): bool
  153. {
  154. return \count($array) > 0 && array_reduce($array, fn ($carry, $item) => $carry && \is_string($item) && '' !== $item, true);
  155. }
  156. #[Rest\Get('/api/v4/search/ssr-data', name: 'aqarmap_search_results_ssr_data', options: ['i18n' => false, 'expose' => false])]
  157. #[OA\Get(summary: 'Get search SSR metadata', description: 'Builds SEO metadata and supporting SSR fields for search pages based on section, property type, and location slugs.', tags: ['ListingsSearch'])]
  158. #[OA\Parameter(name: 'section', description: 'Section slug.', in: 'query', required: false, schema: new OA\Schema(type: 'string'))]
  159. #[OA\Parameter(name: 'propertyType', description: 'Property Type slug.', in: 'query', required: false, schema: new OA\Schema(type: 'string'))]
  160. #[OA\Parameter(name: 'locations', description: 'Array of Locations slugs.', in: 'query', required: false, schema: new OA\Schema(type: 'array', items: new OA\Items(type: 'string')))]
  161. #[Rest\QueryParam(name: 'section', default: null, description: 'Section slug')]
  162. #[Rest\QueryParam(name: 'propertyType', default: null, description: 'Property Type slug')]
  163. #[Rest\QueryParam(name: 'locations', default: null, description: 'Array of Locations slugs.')]
  164. #[Rest\View(serializerGroups: ['SlugResolver'])]
  165. #[OA\Response(response: 200, description: 'SSR metadata payload for the search page.')]
  166. #[OA\Response(response: 400, description: 'Returned when section, property type, or locations are missing from the request.')]
  167. public function singlePageSSRData(Request $request, SectionRepository $sectionRepository, PropertyTypeRepository $propertyTypeRepository, LocationRepository $locationRepository, BuilderInterface $searchDescriptionBuilder): array
  168. {
  169. $sectionSlug = $request->get('section');
  170. if (!$sectionSlug) {
  171. throw new \InvalidArgumentException('Section is required');
  172. }
  173. $section = $sectionRepository->findOneBy(['slug' => $sectionSlug]);
  174. $propertyTypeSlug = $request->get('propertyType');
  175. if (!$propertyTypeSlug) {
  176. throw new \InvalidArgumentException('propertyType is required');
  177. }
  178. $propertyType = $propertyTypeRepository->findOneBy(['slug' => $propertyTypeSlug]);
  179. $locationsSlugs = $request->query->all('locations');
  180. if (!$locationsSlugs) {
  181. throw new \InvalidArgumentException('locations is required');
  182. }
  183. $locations = $locationRepository->getLocationsBySlugs($locationsSlugs);
  184. // Page Description
  185. $searchDescriptionBuilder
  186. ->setSection($section)
  187. ->setPropertyType($propertyType)
  188. ->setLocation(reset($locations))
  189. ;
  190. return [
  191. 'section' => $section,
  192. 'propertyType' => $propertyType,
  193. 'locations' => $locations,
  194. 'pageTitle' => '',
  195. 'metaDescription' => $searchDescriptionBuilder->format()->get(),
  196. 'metaKeywords' => '',
  197. 'canonicalUrl' => '',
  198. 'breadcrumbs' => [
  199. [
  200. 'title' => '',
  201. 'url' => '',
  202. ],
  203. ],
  204. 'propertyTypesChips' => [
  205. [
  206. 'title' => '',
  207. 'url' => '',
  208. ],
  209. ],
  210. ];
  211. }
  212. }