<?php
namespace Aqarmap\Bundle\SearchBundle\Services\ElasticListingSearch;
use Aqarmap\Bundle\ListingBundle\Constant\ListingCategories;
use Aqarmap\Bundle\ListingBundle\Entity\Listing;
use Aqarmap\Bundle\ListingBundle\Entity\ListingSearchScoreSetting;
use Aqarmap\Bundle\ListingBundle\Entity\Location;
use Aqarmap\Bundle\ListingBundle\Entity\PropertyType;
use Aqarmap\Bundle\ListingBundle\Service\ListingManager;
use Aqarmap\Bundle\MainBundle\Service\Setting;
use Aqarmap\Bundle\SearchBundle\Repositories\ListingSearchRepository;
use Aqarmap\Bundle\SearchBundle\Services\ElasticClientService;
use Doctrine\ORM\EntityManagerInterface;
use FOS\ElasticaBundle\Finder\TransformedFinder;
use FOS\ElasticaBundle\HybridResult;
use Knp\Component\Pager\Paginator;
use Knp\Component\Pager\PaginatorInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Csrf\TokenStorage\TokenStorageInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
abstract class AbstractElasticListingSearch
{
public const TOTAL_LISTINGS_PER_PAGE = 30;
/**
* @var ListingSearchRepository
*/
protected $repository;
/**
* @var ElasticClientService
*/
protected $elasticClientService;
/**
* @var EntityManagerInterface
*/
protected $entityManager;
/**
* @var Paginator
*/
protected $paginator;
/**
* @var TransformedFinder
*/
protected $finder;
/**
* @var Session
*/
protected $session;
/**
* @var RouterInterface
*/
protected $router;
/**
* @var Setting
*/
protected $setting;
/**
* @var ListingManager
*/
protected $listingManager;
/**
* @var Translator
*/
protected $translator;
/**
* @var array
*/
private $parentsToChildrenLocationsMap = [];
/**
* @var RequestStack
*/
private $requestStack;
public function __construct(
SessionInterface $session,
EntityManagerInterface $entityManager,
PaginatorInterface $paginator,
ElasticClientService $elasticClientService,
TransformedFinder $finder,
RequestStack $requestStack,
RouterInterface $router,
Setting $setting,
TokenStorageInterface $tokenStorage,
ListingManager $listingManager,
TranslatorInterface $translator
) {
$this->session = $session;
$this->entityManager = $entityManager;
$this->paginator = $paginator;
$this->elasticClientService = $elasticClientService;
$this->finder = $finder;
$this->requestStack = $requestStack;
$this->router = $router;
$this->setting = $setting;
$this->listingManager = $listingManager;
$this->translator = $translator;
$this->setRepository();
$this->setRequest($requestStack);
}
final public function search(array $criteria)
{
$criteria = $this->buildCriteria($criteria);
$this->enableDeepSearch($criteria);
$this->handleCriteriaToQuery($criteria);
return $this->getResults((int) $criteria['page'], (int) $criteria['limit']);
}
/**
* Get the search criteria locations mapped to their children.
*
* @return array
*/
public function getParentsToChildrenLocationsMap()
{
return $this->parentsToChildrenLocationsMap;
}
abstract protected function getResults(int $page = 1, int $limit = 20);
protected function setSort($sortBy = '_score', $order = 'asc'): void
{
$this->repository->setSort($sortBy);
$this->repository->setOrder($order);
}
/**
* Deep Search, Search inside sub (Locations & Property Types).
*/
protected function enableDeepSearch(array &$criteria): void
{
$this->enableLocationDeepSearch($criteria);
$this->enablePropertyTypeDeepSearch($criteria);
}
/**
* @return array
*/
protected function getScoreSettings()
{
$settingsRaw = $this->entityManager->getRepository(ListingSearchScoreSetting::class)->findAll();
$settings = [];
foreach ($settingsRaw as $setting) {
$settings[$setting->getName()] = $setting->getValue();
}
return $settings;
}
protected function setRepository(): void
{
$this->repository = new ListingSearchRepository($this->elasticClientService->getClient(), $this->setting);
}
/**
* call handler for each criteria type.
*
* @param array $criteria
*/
protected function handleCriteriaToQuery($criteria): void
{
foreach ($criteria as $key => $value) {
$keyUp = ucfirst($key);
if (method_exists($this->repository, 'handle'.$keyUp)) {
\call_user_func([$this->repository, 'handle'.$keyUp], $value);
}
}
}
protected function getQuery()
{
return $this->repository->getResultQuery();
}
/**
* Set array of listing Scoring.
*
* @param array $listings
*/
protected function setListingsScoring($listings = []): array
{
$items = [];
foreach ($listings as $listing) {
$items[] = $this->setListingScoring($listing);
}
return $items;
}
/**
* Set Listing Scoring.
*
* @return Listing
*/
private function setListingScoring(HybridResult &$listing)
{
$score = $listing->getResult()->getScore();
$listing = $listing->getTransformed();
$listing->setElasticSearchScore($score);
return $listing;
}
private function setRequest(RequestStack $requestStack): self
{
if (!$requestStack->getCurrentRequest()) {
$requestStack->push(new Request());
}
return $this;
}
/**
* @param array $criteria
*
* @return array
*/
private function buildCriteria($criteria)
{
$limit = (int) $this->setting->getSetting('features', 'listings_count_per_search_page') ?: self::TOTAL_LISTINGS_PER_PAGE;
$criteria['limit'] ??= $limit;
$criteria['page'] = $this->requestStack->getCurrentRequest()->query->get('page', 1);
$criteria['excludedCategory'] = !$this->setting->getSetting('features', 'listing_search_scrapped') ?
[ListingCategories::SCRAPPED] :
[];
return $criteria;
}
/**
* Enable Location Deep Search.
*/
private function enableLocationDeepSearch(array &$criteria): void
{
if (isset($criteria['location'])) {
$locations = [];
$criteria['location'] = !\is_array($criteria['location']) ? [$criteria['location']] : $criteria['location'];
foreach ($criteria['location'] as $location) {
if (!$location instanceof Location) {
$location = $this->entityManager->getReference('AqarmapListingBundle:Location', $location);
}
$childrenLocations = $this
->entityManager
->getRepository(Location::class)
->getLocationChildrenIds($location);
$this->parentsToChildrenLocationsMap[$location->getId()] = $childrenLocations;
$locations = array_merge(
$locations,
$childrenLocations
);
}
$criteria['location'] = $locations;
}
}
/**
* Enable PropertyType Deep Search.
*/
private function enablePropertyTypeDeepSearch(array &$criteria): void
{
if (isset($criteria['propertyType'])) {
if (!\is_object($criteria['propertyType'])) {
$criteria['propertyType'] = $this->entityManager->getRepository(PropertyType::class)->findOneBy([
'id' => (int) $criteria['propertyType'],
]);
}
$criteria['propertyType'] = $this
->entityManager
->getRepository(PropertyType::class)
->getPropertyTypeChildrenIds($criteria['propertyType']);
}
}
}