<?php
namespace Aqarmap\Bundle\SearchBundle\Controller\Api\V4;
use App\Exception\UnauthorizedHttpException;
use Aqarmap\Bundle\ListingBundle\Constant\ListingStatus;
use Aqarmap\Bundle\ListingBundle\Entity\Location;
use Aqarmap\Bundle\ListingBundle\Entity\Section;
use Aqarmap\Bundle\ListingBundle\Event\SearchTriggerEvent;
use Aqarmap\Bundle\ListingBundle\Message\Search;
use Aqarmap\Bundle\ListingBundle\Repository\ListingRepository;
use Aqarmap\Bundle\ListingBundle\Repository\LocationRepository;
use Aqarmap\Bundle\ListingBundle\Repository\PropertyTypeRepository;
use Aqarmap\Bundle\ListingBundle\Repository\SectionRepository;
use Aqarmap\Bundle\ListingBundle\Service\ListingManager;
use Aqarmap\Bundle\ListingBundle\Service\RelatedResultService;
use Aqarmap\Bundle\ListingBundle\Service\SectionService;
use Aqarmap\Bundle\MainBundle\Controller\Api\V4\BaseController;
use Aqarmap\Bundle\MainBundle\Model\Listing\V4\ListingDataMapper;
use Aqarmap\Bundle\MainBundle\Repository\CustomParagraphRepository;
use Aqarmap\Bundle\SearchBundle\CriteriaBuilders\BuilderDirector;
use Aqarmap\Bundle\SearchBundle\CriteriaMediator\Contracts\MediatorInterface;
use Aqarmap\Bundle\SearchBundle\Services\ListingFaqService;
use Aqarmap\Bundle\SearchBundle\Services\SEOListingSearchService;
use FOS\RestBundle\Controller\Annotations as Rest;
use FOS\RestBundle\View\View;
use JMS\Serializer\SerializationContext;
use JMS\Serializer\SerializerInterface;
use Nelmio\ApiDocBundle\Annotation\Operation;
use OpenApi\Annotations as OA;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
use Symfony\Component\Cache\Adapter\AdapterInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Messenger\MessageBusInterface;
class ListingSearchController extends BaseController
{
/**
* @var BuilderDirector
*/
private $builderDirector;
/**
* @var MediatorInterface
*/
private $mediator;
/**
* @var EventDispatcherInterface
*/
private $eventDispatcher;
/**
* @var SerializerInterface
*/
private $serializer;
/**
* @var RelatedResultService
*/
private $relatedResultService;
/**
* @var CustomParagraphRepository
*/
private $customParagraphRepository;
/**
* @var SEOListingSearchService
*/
private $seoListingSearchService;
/**
* @var LocationRepository
*/
private $locationRepository;
/**
* @var SectionRepository
*/
private $sectionRepository;
/**
* @var PropertyTypeRepository
*/
private $propertyTypeRepository;
/**
* @var ListingFaqService
*/
private $listingFaqService;
public function __construct(
BuilderDirector $builderDirector,
MediatorInterface $mediator,
EventDispatcherInterface $eventDispatcher,
SerializerInterface $serializer,
RelatedResultService $relatedResultService,
LocationRepository $locationRepository,
CustomParagraphRepository $customParagraphRepository,
SEOListingSearchService $seoListingSearchService,
SectionRepository $sectionRepository,
PropertyTypeRepository $propertyTypeRepository,
ListingFaqService $listingFaqService
) {
$this->builderDirector = $builderDirector;
$this->mediator = $mediator;
$this->eventDispatcher = $eventDispatcher;
$this->serializer = $serializer;
$this->relatedResultService = $relatedResultService;
$this->customParagraphRepository = $customParagraphRepository;
$this->seoListingSearchService = $seoListingSearchService;
$this->locationRepository = $locationRepository;
$this->sectionRepository = $sectionRepository;
$this->propertyTypeRepository = $propertyTypeRepository;
$this->listingFaqService = $listingFaqService;
}
/**
* Listings Search V4 ( Legacy code ).
*
* @Operation(
* tags={"Listing"},
* summary="Search",
*
* @OA\Parameter(
* name="propertyType",
* in="query",
* description="Property Type ID",
* required=false,
* ),
* @OA\Parameter(
* name="location",
* in="query",
* description="Location ID comma spereted",
* required=false,
* ),
* @OA\Parameter(
* name="section",
* in="query",
* description="Section ID",
* required=false,
* ),
* @OA\Parameter(
* name="bounds",
* in="query",
* description="Map bounds (example: 24.6275450,46.6363017,24.6977461,46.817232)",
* required=false,
* ),
* @OA\Parameter(
* name="minPrice",
* in="query",
* description="Minimum Prices",
* required=false,
* ),
* @OA\Parameter(
* name="maxPrice",
* in="query",
* description="Maximum Prices",
* required=false,
* ),
* @OA\Parameter(
* name="minArea",
* in="query",
* description="Minimum Area",
* required=false,
* ),
* @OA\Parameter(
* name="maxArea",
* in="query",
* description="Maximum Area",
* required=false,
* ),
* @OA\Parameter(
* name="minFloor",
* in="query",
* description="Minimum Floor",
* required=false,
* ),
* @OA\Parameter(
* name="minRoom",
* in="query",
* description="Minimum Room",
* required=false,
* ),
* @OA\Parameter(
* name="floor",
* in="query",
* description="Floor Number",
* required=false,
* ),
* @OA\Parameter(
* name="room",
* in="query",
* description="Number of Rooms",
* required=false,
* ),
* @OA\Parameter(
* name="baths",
* in="query",
* description="Number of Baths",
* required=false,
* ),
* @OA\Parameter(
* name="finishType",
* in="query",
* description="finish Type",
* required=false,
* ),
* @OA\Parameter(
* name="sellerRole",
* in="query",
* description="sellerRole",
* required=false,
* ),
* @OA\Parameter(
* name="paymentMethod",
* in="query",
* description="paymentMethod",
* required=false,
* ),
* @OA\Parameter(
* name="deliveryYear",
* in="query",
* description="deliveryYear",
* required=false,
* ),
* @OA\Parameter(
* name="bath",
* in="query",
* description="Number of Baths",
* required=false,
* ),
* @OA\Parameter(
* name="photos",
* in="query",
* description="Get only listings with photos",
* required=false,
* ),
* @OA\Parameter(
* name="isMortgage",
* in="query",
* description="Get only listings that support mortgage",
* required=false,
* ),
* @OA\Parameter(
* name="eligibleForMortgage",
* in="query",
* description="Get listings that has mortgage Percentage",
* required=false,
* ),
* @OA\Parameter(
* name="page",
* in="query",
* description="Page number, starting from 1.",
* required=false,
* ),
* @OA\Parameter(
* name="limit",
* in="query",
* description="Number of items per page.",
* required=false,
* ),
* @OA\Parameter(
* name="sort",
* in="query",
* description="Sort search results by price or area.",
* required=false,
* ),
* @OA\Parameter(
* name="direction",
* in="query",
* description="Ascending (A to Z, 0 to 9), Descending (Z to A, 9 to 0)",
* required=false,
* ),
* @OA\Parameter(
* name="keywordSearch",
* in="query",
* description="Search by keyword in listing's title, description and address",
* required=false,
* ),
* @OA\Parameter(
* name="unitOnly",
* in="query",
* description="Get compound units only",
* required=false,
* ),
*
* @OA\Response(
* response="500",
* description="Returned when something went wrong, for example if you entered non existing propertyType ID"
* )
* )
*
* @Rest\Get("/api/v4/listings/search-legacy", options={"i18n" = false}, name="legacy_aqarmap_api_listings_search_v4")
*
* @Rest\QueryParam(name="propertyType", requirements="\d+", default=null, description="Property Type ID")
* @Rest\QueryParam(name="location", requirements="\d+", default=null, description="Location ID comma spereted")
* @Rest\QueryParam(name="section", requirements="\d+", default=null, description="Section ID")
* @Rest\QueryParam(
* name="bounds", requirements="South-West Latitude,
* Longitude, North-East Latitude, Longitude", default=null,
* description="Map bounds (example: 24.6275450,46.6363017,24.6977461,46.817232)"
* )
* @Rest\QueryParam(name="minPrice", requirements="\d+", default=null, description="Minimum Prices")
* @Rest\QueryParam(name="maxPrice", requirements="\d+", default=null, description="Maximum Prices")
* @Rest\QueryParam(name="minArea", requirements="\d+", default=null, description="Minimum Area")
* @Rest\QueryParam(name="maxArea", requirements="\d+", default=null, description="Maximum Area")
* @Rest\QueryParam(name="minFloor", requirements="\d+", default=null, description="Minimum Floor")
* @Rest\QueryParam(name="minRoom", requirements="\d+", default=null, description="Minimum Room")
* @Rest\QueryParam(name="floor", requirements="\d+", default=null, description="Floor Number")
* @Rest\QueryParam(name="room", requirements="\d+", default=null, description="Number of Rooms")
* @Rest\QueryParam(name="baths", requirements="\d+", default=null, description="Number of Baths")
* @Rest\QueryParam(name="finishType", default=null, description="finish Type")
* @Rest\QueryParam(name="sellerRole", requirements="\d+", default=null, description="sellerRole")
* @Rest\QueryParam(name="paymentMethod", requirements="\d+", default=null, description="paymentMethod")
* @Rest\QueryParam(name="deliveryYear", requirements="\d+", default=null, description="deliveryYear")
* @Rest\QueryParam(name="bath", default=null, default=null, description="Number of Baths")
* @Rest\QueryParam(
* name="photos", requirements="(1)|(0)",
* nullable=true, strict=true, default="0",
* description="Get only listings with photos"
* )
* @Rest\QueryParam(
* name="isMortgage", requirements="(1)|(0)",
* nullable=true, strict=true, default="0",
* description="Get only listings that support mortgage"
* )
* @Rest\QueryParam(name="eligibleForMortgage", nullable=true, strict=true, description="Get listings that has mortgage Percentage")
* @Rest\QueryParam(
* name="page", requirements="\d+", nullable=true, default=1,
* description="Page number, starting from 1."
* )
* @Rest\QueryParam(
* name="limit", requirements="\d+", nullable=true,
* default=10, description="Number of items per page."
* )
* @Rest\QueryParam(
* name="sort", requirements="price|area", nullable=true,
* default=null, description="Sort search results by price or area."
* )
* @Rest\QueryParam(
* name="direction", requirements="asc|desc", nullable=true,
* default="asc", description="Ascending (A to Z, 0 to 9), Descending (Z to A, 9 to 0)"
* )
* @Rest\QueryParam(
* name="keywordSearch",
* description="Search by keyword in listing's title, description and address"
* )
* @Rest\QueryParam(
* name="unitOnly",
* description="Get compound units only",
* requirements="(1)|(0)",
* nullable=true,
* )
*
* @Rest\View()
*
* @Cache(
* expires="+2 hours", maxage="+2 hours", smaxage="+2 hours",
* public=true, vary={"Accept-Language", "X-Accept-Version", "Accept"}
* )
*/
public function getSearchListings(Request $request, AdapterInterface $cache): Response
{
$criteria = $this->builderDirector->build($request)->getResult();
$locations = explode(',', $request->query->get('location', ''));
$request->query->set('locations', $locations);
if (!empty($criteria['keywordSearch'])) {
$this->eventDispatcher->dispatch(new SearchTriggerEvent($request));
}
if ($this->getUser()) {
$this->dispatchMessage(new Search($request->query, $this->getUser()));
}
$criteria['status'] = ListingStatus::LIVE;
$cacheKey = sprintf('api_v4_listings_search_%s_%s', $request->getLocale(), md5(http_build_query($request->query->all())));
$cachedSerialize = $cache->getItem($cacheKey);
if (!$cachedSerialize->isHit() || empty($cachedSerialize->get())) {
$result = $this->mediator->start($criteria)->getResults();
$mapped = new ListingDataMapper();
$mapped->setMapListingAttributes(true);
$mapped->setMapListingMainPhoto(true);
$mapped->setMapListingPhotos(true);
$data = [
'default' => $this->makeMappedPaginatedBody($result['searchResults'], $mapped),
'related' => [],
];
$data = $this->serializer->serialize(
$data,
'json',
SerializationContext::create()
->setGroups('SearchV4')
->enableMaxDepthChecks()
);
$cachedSerialize->set($data);
$cachedSerialize->expiresAfter(3600 * 3);
$cache->save($cachedSerialize);
}
return new Response($cachedSerialize->get());
}
/**
* Related Listings Search V4.
*
* @Operation(
* tags={"Listing"},
* summary="Search",
*
* @OA\Parameter(
* name="propertyType",
* in="query",
* description="Property Type ID",
* required=false,
* ),
* @OA\Parameter(
* name="location",
* in="query",
* description="Location ID comma spereted",
* required=false,
* ),
* @OA\Parameter(
* name="section",
* in="query",
* description="Section ID",
* required=false,
* ),
* @OA\Parameter(
* name="page",
* in="query",
* description="Page number, starting from 1.",
* required=false,
* ),
* @OA\Parameter(
* name="limit",
* in="query",
* description="Number of items per page.",
* required=false,
* ),
* @OA\Parameter(
* name="sort",
* in="query",
* description="Sort search results by price or area.",
* required=false,
* ),
* @OA\Parameter(
* name="direction",
* in="query",
* description="Ascending (A to Z, 0 to 9), Descending (Z to A, 9 to 0)",
* required=false,
* ),
*
* @OA\Response(
* response="500",
* description="Returned when something went wrong, for example if you entered non existing propertyType ID"
* )
* )
*
* @Rest\Get("/api/v4/listings/search/related", options={"i18n" = false}, name="aqarmap_api_get_related_results_v4")
*
* @Rest\QueryParam(name="propertyType", requirements="\d+", default=null, description="Property Type ID")
* @Rest\QueryParam(name="location", requirements="\d+", default=null, description="Location ID comma spereted")
* @Rest\QueryParam(name="section", requirements="\d+", default=null, description="Section ID")
* @Rest\QueryParam(
* name="page", requirements="\d+", nullable=true, default=1,
* description="Page number, starting from 1."
* )
* @Rest\QueryParam(
* name="limit", requirements="\d+", nullable=true,
* default=10, description="Number of items per page."
* )
* @Rest\QueryParam(
* name="sort", requirements="price|area", nullable=true,
* default=null, description="Sort search results by price or area."
* )
* @Rest\QueryParam(
* name="direction", requirements="asc|desc", nullable=true,
* default="asc", description="Ascending (A to Z, 0 to 9), Descending (Z to A, 9 to 0)"
* )
*
* @Rest\View(serializerGroups={"DefaultV4", "SearchV4"})
*
* @return View
*
* @throws \Exception
*/
public function getRelatedSearchListings(Request $request)
{
$criteria = $this->builderDirector
->build($request)
->getResult();
$criteria['page'] = $request->query->get('page', 1);
$criteria['selectedLocation'] = current($criteria['location']);
return $this->paginatedRespond(
$this->relatedResultService->joinListingsWithPaginatedLocations($criteria)
);
}
/**
* @Rest\Get("/api/v4/listings", options={"i18n" = false}, name="aqarmap_api_get_listings")
* @Rest\Get("/api/v4/listings/search", options={"i18n" = false}, name="aqarmap_api_listings_search_v4")
*/
public function getListings(Request $request, ListingRepository $listingRepository, ListingManager $listingManager, AdapterInterface $cache, MessageBusInterface $messageBus)
{
/** @var UserInterface $user */
$user = $this->getUser();
$hasSearchScoringRole = $user && $user->hasRole('ROLE_SEARCH_SCORING');
if (!$hasSearchScoringRole && ($request->get('esdebug') || $request->get('scoredebug'))) {
$request->query->remove('esdebug');
$request->query->remove('scoredebug');
}
if ($request->get('location')) {
$request->query->set('locations', explode(',', $request->query->get('location', '')));
}
$cacheKey = sprintf('api_listings_search_v4_%s_%s', $request->getLocale(), md5(http_build_query($request->query->all())));
$cachedSerialize = $cache->getItem($cacheKey);
if (!$cachedSerialize->isHit() || empty($cachedSerialize->get())) {
$listingsElasticResponse = $listingManager->getListingsElasticResponse($request);
$listingsQueryBuilder = $listingRepository->getListingsByIds(array_column($listingsElasticResponse['items'], 'id'));
if ('aqarmap_api_listings_search_v4' === $request->attributes->get('_route')) {
$listingsElasticResponse['pagination']['totalPages'] = (float) $listingsElasticResponse['pagination']['totalPages'];
}
$listings = $listingsQueryBuilder->getQuery()->getResult();
$data = [
'default' => $this->mapPaginatedBody($listingsElasticResponse, $listings),
'related' => [],
];
$data = $this->serializer->serialize(
$data,
'json',
SerializationContext::create()
->setGroups('SearchV4')
->enableMaxDepthChecks()
);
$cachedSerialize->set($data);
$cachedSerialize->expiresAfter(3600 * 3);
$cache->save($cachedSerialize);
}
return new Response($cachedSerialize->get());
}
/**
* @Rest\Get("/api/v4/listings/debug", options={"i18n" = false}, name="aqarmap_api_get_listings_debug")
*/
public function getListingsEsDebug(Request $request, ListingManager $listingManager)
{
/** @var UserInterface $user */
$user = $this->getUser();
if (!($user && $user->hasRole('ROLE_SEARCH_SCORING'))) {
throw new UnauthorizedHttpException();
}
$request->query->set('esdebug', 1);
if ($request->get('location')) {
$request->query->set('locations', explode(',', $request->query->get('location', '')));
}
$listingsDebugElasticResponse = $listingManager->getListingsDebugElasticResponse($request);
if ('aqarmap_api_listings_search_v4' === $request->attributes->get('_route')) {
$listingsDebugElasticResponse['pagination']['totalPages'] = (float) $listingsDebugElasticResponse['pagination']['totalPages'];
}
$data = [
'default' => $this->mapPaginatedBody($listingsDebugElasticResponse, $listingsDebugElasticResponse['items']),
];
return new JsonResponse($data);
}
/**
* @Rest\Get("/api/v4/listings/search/ssr-data", options={"i18n" = false}, name="aqarmap_api_get_listings_search_ssr-data")
*/
public function getListingsSearchSSRData(Request $request, ListingManager $listingManager, AdapterInterface $cache, SectionService $sectionService)
{
$cacheKey = sprintf('api_listings_search_ssr_data_%s_%s', $request->getLocale(), md5(http_build_query($request->query->all())));
$cachedResponse = $cache->getItem($cacheKey);
if (!$cachedResponse->isHit() || empty($cachedResponse->get())) {
$customParagraph = $locationChildren = $slugResolver = $faqData = $locationParents = [];
if ($request->get('location')) {
$request->query->set('locations', explode(',', $request->query->get('location', '')));
}
$location = $request->get('locations') ? $request->get('locations')[0] : null;
$location = $this->locationRepository->findOneBy(['slug' => $location]);
$section = $this->sectionRepository->findOneBy(['slug' => $request->query->get('section')]);
$propertyType = $this->propertyTypeRepository->findOneBy(['slug' => $request->query->get('propertyType')]);
if ($location) {
$locationChildren = $listingManager->getSerializedLocationChildren($location);
$locationParents = $listingManager->getLocationParents($location, $request->getLocale());
}
if ($section && $propertyType && $location) {
$longTail = $this->seoListingSearchService->getSerializedLongTailData($section, $propertyType, $location) ?? [];
}
if ($section && $propertyType) {
$customParagraph = $listingManager->getSerializedCustomParagraph($section, $propertyType, $location) ?? [];
$slugResolver = $listingManager->getSerializedResolvedSlugs($section, $propertyType, (array) $request->query->get('locations', []));
$faqData = $listingManager->getFaqs($section, $propertyType, $location, $request->getLocale());
}
$response = [
'locationChildren' => !empty($locationChildren) ? json_decode($locationChildren, true) : [],
'longTail' => !empty($longTail) ? json_decode($longTail, true) : [],
'customParagraph' => !empty($customParagraph) ? json_decode($customParagraph, true) : [],
'slugResolver' => !empty($slugResolver) ? json_decode($slugResolver, true) : [],
'faqData' => $faqData,
'locationParents' => $locationParents,
'sections' => json_decode($sectionService->getSerializedSections(), true),
'propertyTypeChips' => $listingManager->getListingPropertyTypesChips($request, $location, $section, $propertyType),
];
$cachedResponse->set(json_encode($response));
$cachedResponse->expiresAfter(3600 * 3);
$cache->save($cachedResponse);
}
return new JsonResponse(json_decode($cachedResponse->get(), true));
}
/**
* @Rest\Get("/api/v4/listings/trigger-search", options={"i18n" = false}, name="aqarmap_api_trigger_search_listings_v4")
*/
public function triggerSearchListingsAction(Request $request, MessageBusInterface $messageBus)
{
$user = $this->getUser();
if ($request->get('location')) {
$request->query->set('locations', explode(',', $request->query->get('location', '')));
}
if ($request->get('keywordSearch')) {
$this->eventDispatcher->dispatch(new SearchTriggerEvent($request));
}
if ($user) {
$messageBus->dispatch(new Search($request->query, $user));
}
return new JsonResponse([
'statusCode' => Response::HTTP_OK,
'statusMessage' => 'Search triggered successfully!',
]);
}
private function mapPaginatedBody(array $result, array $listings): array
{
if (!$result['pagination']) {
return [
'statusCode' => $this->getStatusCode(),
'statusMessage' => $this->getStatusMessage(),
'paginate' => [],
'data' => [],
'errors' => $this->getErrors(),
];
}
return [
'statusCode' => $this->getStatusCode(),
'statusMessage' => $this->getStatusMessage(),
'paginate' => $result['pagination'],
'data' => $listings,
'errors' => $this->getErrors(),
];
}
}