src/Aqarmap/Bundle/SearchBundle/Services/CompoundSearchService.php line 121

  1. <?php
  2. namespace Aqarmap\Bundle\SearchBundle\Services;
  3. use Aqarmap\Bundle\ListingBundle\Constant\CompoundFeedback;
  4. use Aqarmap\Bundle\ListingBundle\Constant\ListingCategories;
  5. use Aqarmap\Bundle\ListingBundle\Constant\ListingSections;
  6. use Aqarmap\Bundle\ListingBundle\Constant\ListingStatus;
  7. use Aqarmap\Bundle\ListingBundle\Entity\CompoundLocation;
  8. use Aqarmap\Bundle\MainBundle\Constant\CustomParagraphPlaceTypes;
  9. use Aqarmap\Bundle\MainBundle\Entity\CustomParagraph;
  10. use Aqarmap\Bundle\MainBundle\Repository\CustomParagraphRepository;
  11. use Aqarmap\Bundle\SearchBundle\Repositories\ListingSearchRepository;
  12. use Doctrine\ORM\NonUniqueResultException;
  13. use Elastica\Aggregation\Terms;
  14. use Elastica\Aggregation\ValueCount;
  15. use Symfony\Component\Form\FormInterface;
  16. use Symfony\Component\HttpFoundation\Request;
  17. class CompoundSearchService extends BaseSearchService
  18. {
  19. /** @var ListingSearchRepository */
  20. protected $repository;
  21. public const TOTAL_LISTINGS_PER_PAGE = 30;
  22. public const CUSTOM_PARAGRAPH_MAX_RESULTS = 1;
  23. public const CRITERIA_DATA_TYPE = [
  24. 'compoundLocations' => 'is_numeric',
  25. 'propertyTypes' => 'is_numeric',
  26. 'priceLevels' => 'is_numeric',
  27. 'compoundStatus' => 'is_numeric',
  28. 'paymentMethods' => 'is_numeric',
  29. 'keywords' => 'is_numeric',
  30. 'finishTypes' => 'is_string',
  31. 'deliveryYears' => 'is_numeric',
  32. 'developerExperience' => 'is_string',
  33. ];
  34. public const DELIVERY_YEAR_FIELD = 'childrenAttributesList.year-built.keyword';
  35. public function setSort($sortBy = '_score', $order = 'asc'): void
  36. {
  37. $this->repository->setSort($sortBy);
  38. $this->repository->setOrder($order);
  39. }
  40. /**
  41. * Deep Search, Search inside sub (Locations & Property Types).
  42. */
  43. public function enableDeepSearch(array &$criteria): void
  44. {
  45. try {
  46. $this->enableCompoundLocationDeepSearch($criteria);
  47. } catch (\Exception) {
  48. }
  49. }
  50. /**
  51. * Enable Compound Location Deep Search.
  52. */
  53. public function enableCompoundLocationDeepSearch(array &$criteria): void
  54. {
  55. if (isset($criteria['compoundLocations'])) {
  56. $criteria['compoundLocations'] = \is_array($criteria['compoundLocations']) ? $criteria['compoundLocations'] : [$criteria['compoundLocations']];
  57. $locations = [];
  58. foreach ($criteria['compoundLocations'] as $location) {
  59. if (!\is_object($location)) {
  60. $location = $this->entityManager->getReference(CompoundLocation::class, $location);
  61. }
  62. $locations = array_merge($locations, $this->entityManager->getRepository(CompoundLocation::class)->getCompoundLocationChildren($location));
  63. }
  64. array_walk($locations, function(&$location): void {
  65. $location = $location->getId();
  66. });
  67. $criteria['compoundLocations'] = array_unique($locations);
  68. }
  69. }
  70. /**
  71. * Enable property type Deep Search.
  72. */
  73. public function enablePropertytypeDeepSearch(array &$criteria): void
  74. {
  75. if (isset($criteria['propertyTypes'])) {
  76. $criteria['propertyTypes'] = \is_array($criteria['propertyTypes']) ? $criteria['propertyTypes'] : [$criteria['propertyTypes']];
  77. $propertyTypes = [];
  78. foreach ($criteria['propertyTypes'] as $propertyType) {
  79. if (!\is_object($propertyType)) {
  80. $propertyType = $this->entityManager->getReference(\Aqarmap\Bundle\ListingBundle\Entity\PropertyType::class, $propertyType);
  81. }
  82. $propertyTypes = array_merge($propertyTypes, $this->entityManager->getRepository(\Aqarmap\Bundle\ListingBundle\Entity\PropertyType::class)->getPropertyTypeChildren($propertyType));
  83. }
  84. array_walk($propertyTypes, function(&$propertyType): void {
  85. $propertyType = $propertyType->getId();
  86. });
  87. $criteria['propertyTypes'] = array_unique($propertyTypes);
  88. }
  89. }
  90. /**
  91. * Handles criteria & find compounds results.
  92. *
  93. * @return \Knp\Component\Pager\Pagination\PaginationInterface|mixed
  94. */
  95. public function getCompounds(array $criteria, $page)
  96. {
  97. $criteria = array_merge($criteria, [
  98. 'category' => ListingCategories::PROJECTS,
  99. 'nullParent' => null,
  100. 'status' => ListingStatus::LIVE,
  101. ]);
  102. $this->handleCriteriaToQuery($criteria);
  103. return $this->getResults($page, $criteria);
  104. }
  105. /**
  106. * Compounds query.
  107. *
  108. * @param int $page
  109. *
  110. * @return \Knp\Component\Pager\Pagination\PaginationInterface|mixed
  111. */
  112. public function getResults($page, array $criteria)
  113. {
  114. $limit = (int) ($criteria['limit'] ?? self::TOTAL_LISTINGS_PER_PAGE) ?: self::TOTAL_LISTINGS_PER_PAGE;
  115. $resultsQuery = $this->repository->getResultQuery();
  116. // Adding Default Sorting By Featuring
  117. $resultsQuery->addSort([$criteria['sort'][0] ?? 'featured' => ['order' => $criteria['direction'] ?? 'desc']]);
  118. $results = $this->finder->createPaginatorAdapter($resultsQuery);
  119. try {
  120. $pagination = $this->paginator->paginate($results, (int) $page ?: 1, $limit);
  121. } catch (\Exception) {
  122. $pagination = $this->paginator->paginate([], (int) $page ?: 1, $limit);
  123. }
  124. return $pagination;
  125. }
  126. /**
  127. * Handles criteria and find compounds count.
  128. *
  129. * @return array
  130. *
  131. * @throws \Exception
  132. */
  133. public function getResultsCount(Request $request)
  134. {
  135. $fields = explode(',', $request->query->get('aggregation'));
  136. $request->query->remove('aggregation');
  137. $criteria = $this->compoundService->stringCriteriaToInteger($request->query->all());
  138. $criteria = $this->handleCriteriaDataType($criteria);
  139. $criteria = array_merge($criteria, [
  140. 'category' => ListingCategories::PROJECTS,
  141. 'nullParent' => null,
  142. 'status' => ListingStatus::LIVE,
  143. ]);
  144. $compoundsCount = [];
  145. $oldCriteria = [];
  146. foreach ($fields as $field) {
  147. $clonedField = $field;
  148. $aggregationType = $this->getAggregationType($clonedField);
  149. $this->handleRelationField($field);
  150. $this->excludeAggregationFromCriteria($clonedField, $criteria, $oldCriteria);
  151. $counts = $this->findCompoundsCountByField($field, $criteria, $aggregationType);
  152. if (self::DELIVERY_YEAR_FIELD == $field) {
  153. $compoundsCount[$clonedField] = $this->handleCountResults($counts, $aggregationType, true);
  154. } else {
  155. $compoundsCount[$clonedField] = $this->handleCountResults($counts, $aggregationType);
  156. }
  157. $criteria = array_merge($criteria, $oldCriteria);
  158. }
  159. $compoundsCount['totalHits'] = $this->findCompoundsTotalCount($criteria);
  160. return $compoundsCount;
  161. }
  162. public function isNoIndexMetaTag(array $request): bool
  163. {
  164. return isset($request['priceLevels']) || isset($request['compoundStatus'])
  165. || isset($request['paymentMethods']) || isset($request['finishTypes'])
  166. || isset($request['compoundLocations']) || isset($request['propertyTypes'])
  167. || isset($request['keywords']) ? true : false;
  168. }
  169. /**
  170. * Compounds count query.
  171. *
  172. * @return array|mixed
  173. */
  174. protected function findCompoundsCountByField(string $field, array $criteria, $aggregationType = CompoundFeedback::AGGERGATION_BUCKET_COUNT_TYPE)
  175. {
  176. $this->handleCriteriaToQuery($criteria);
  177. $resultsQuery = $this->repository->getResultQuery();
  178. if (CompoundFeedback::AGGERGATION_SINGLE_COUNT_TYPE == $aggregationType) {
  179. $aggregation = new ValueCount('fields', $field);
  180. } else {
  181. $aggregation = new Terms('fields');
  182. $aggregation->setField($field);
  183. $aggregation->setSize(CompoundFeedback::AGGERGATION_SIZE);
  184. }
  185. $resultsQuery->addAggregation($aggregation);
  186. return $this
  187. ->finder
  188. ->createPaginatorAdapter($resultsQuery)
  189. ->getAggregations();
  190. }
  191. /**
  192. * Compounds total count query.
  193. *
  194. * @return int
  195. */
  196. protected function findCompoundsTotalCount(array $criteria)
  197. {
  198. $this->handleCriteriaToQuery($criteria);
  199. $resultsQuery = $this->repository->getResultQuery();
  200. return $this
  201. ->finder
  202. ->createPaginatorAdapter($resultsQuery)
  203. ->getTotalHits();
  204. }
  205. /**
  206. * Excludes aggregation field from criteria.
  207. */
  208. protected function excludeAggregationFromCriteria(string $field, array &$criteria, array &$oldCriteria): void
  209. {
  210. $aggregationField = $field.'s';
  211. if (\array_key_exists($field, $criteria)) {
  212. $oldCriteria[$field] = $criteria[$field];
  213. unset($criteria[$field]);
  214. } elseif (\array_key_exists($aggregationField, $criteria)) {
  215. $oldCriteria[$aggregationField] = $criteria[$aggregationField];
  216. unset($criteria[$aggregationField]);
  217. }
  218. }
  219. /**
  220. * Adds 'id' to relation field.
  221. */
  222. protected function handleRelationField(&$field): void
  223. {
  224. switch ($field) {
  225. case 'compoundLocation':
  226. $field .= '.id';
  227. break;
  228. case 'propertyType':
  229. $field = 'childrenPropertyType.id';
  230. break;
  231. case 'finishType':
  232. $field = 'childrenAttributesList.finish-type.keyword';
  233. break;
  234. case 'deliveryYears':
  235. $field = self::DELIVERY_YEAR_FIELD;
  236. break;
  237. case 'hasDelivered':
  238. $field = 'compoundField.numberOfProjectsDelivered';
  239. break;
  240. case 'hasInhabited':
  241. $field = 'compoundField.numberOfProjectsInhabited';
  242. break;
  243. default:
  244. break;
  245. }
  246. }
  247. /**
  248. * Handles criteria data types.
  249. */
  250. protected function handleCriteriaDataType(array $criteria)
  251. {
  252. // TODO implement better valiadtion
  253. $acceptedCriteria = self::CRITERIA_DATA_TYPE;
  254. foreach ($criteria as $key => $value) {
  255. if (\array_key_exists($key, $acceptedCriteria)) {
  256. $criteria[$key] = array_filter($value, $acceptedCriteria[$key]);
  257. if (empty($criteria[$key])) {
  258. unset($criteria[$key]);
  259. }
  260. }
  261. }
  262. return $criteria;
  263. }
  264. /**
  265. * Converts criteria value to string.
  266. */
  267. protected function handleStringCriteria(&$criteria): void
  268. {
  269. if (\array_key_exists('keywords', $criteria)) {
  270. $criteria['keywords'] = \is_array($criteria['keywords']) ? implode(',', $criteria['keywords']) : $criteria['keywords'];
  271. }
  272. }
  273. /**
  274. * handle count results.
  275. */
  276. protected function handleCountResults(array $aggregations, ?string $aggregationType = null, $isDeliveryYear = false)
  277. {
  278. if (CompoundFeedback::AGGERGATION_SINGLE_COUNT_TYPE == $aggregationType) {
  279. return $aggregations['fields']['value'];
  280. }
  281. return $this->handleAggregationResults($aggregations, $isDeliveryYear);
  282. }
  283. /**
  284. * Mapping aggregations results.
  285. *
  286. * @return array
  287. */
  288. protected function handleAggregationResults(array $aggregations, $isDeliveryYear = false)
  289. {
  290. $result = [];
  291. if (\array_key_exists('fields', $aggregations) && \array_key_exists('buckets', $aggregations['fields'])) {
  292. foreach ($aggregations['fields']['buckets'] as $aggregation) {
  293. if ($isDeliveryYear) {
  294. $result[(int) $aggregation['key'] - date('Y')] = $aggregation['doc_count'];
  295. } else {
  296. $result[$aggregation['key']] = $aggregation['doc_count'];
  297. }
  298. }
  299. }
  300. return $result;
  301. }
  302. /**
  303. * Sets Repository.
  304. *
  305. * @throws \Exception
  306. */
  307. protected function setRepository(): void
  308. {
  309. $this->repository = new ListingSearchRepository($this->clientService->getClient(), $this->setting);
  310. }
  311. /**
  312. * Add Location to checked Compound Locations.
  313. *
  314. * @param array $checkedLocations
  315. *
  316. * @return array
  317. */
  318. public function addCheckedLocations($checkedLocations, CompoundLocation $location)
  319. {
  320. if (!\in_array($location->getId(), $checkedLocations)) {
  321. array_unshift($checkedLocations, $location->getId());
  322. }
  323. return $checkedLocations;
  324. }
  325. /**
  326. * Add Location to Compound Locations criteria.
  327. *
  328. * @return array
  329. */
  330. public function addLocationsToCriteria(array $criteria, CompoundLocation $location)
  331. {
  332. $locationId = $location->getId();
  333. if (!isset($criteria['compoundLocations'])) {
  334. $criteria['compoundLocations'] = [$locationId];
  335. } elseif (!\in_array($locationId, $criteria['compoundLocations'])) {
  336. array_unshift($criteria['compoundLocations'], $locationId);
  337. }
  338. return $criteria;
  339. }
  340. /**
  341. * remove Current Location from Criteria.
  342. *
  343. * @return string
  344. */
  345. public function removeCurrentLocation(array $criteria)
  346. {
  347. $locationsIds = $criteria['compoundLocations'];
  348. array_shift($locationsIds);
  349. return implode(',', $locationsIds);
  350. }
  351. /**
  352. * Generate the Parameters of No Slug in Url of Compound Search for RedirectUrl.
  353. *
  354. * @return array
  355. */
  356. public function getRedirectUrlParams(array $criteria)
  357. {
  358. $location = $this->entityManager
  359. ->getRepository(CompoundLocation::class)
  360. ->findOneById($criteria['compoundLocations'][0]);
  361. if ($location) {
  362. $params['location'] = $location->getSlug();
  363. }
  364. $params['compoundLocations'] = null;
  365. if (\count($criteria['compoundLocations']) > 1) {
  366. $params['compoundLocations'] = $this->removeCurrentLocation($criteria);
  367. }
  368. return $params;
  369. }
  370. /**
  371. * Sets compound search data.
  372. */
  373. public function setCompoundSearchFormData(FormInterface $form, array $criteria)
  374. {
  375. $compoundService = $this->compoundService;
  376. $form->get('priceLevel')->setData($compoundService->getParameter($criteria, 'priceLevels'));
  377. $form->get('compoundStatus')->setData($compoundService->getParameter($criteria, 'compoundStatus'));
  378. $form->get('paymentMethod')->setData($compoundService->getParameter($criteria, 'paymentMethods'));
  379. $form->get('finishType')->setData($compoundService->getParameter($criteria, 'finishType'));
  380. $form->get('keyword')->setData($compoundService->getParameter($criteria, 'keywords'));
  381. $form->get('finishType')->setData($compoundService->getParameter($criteria, 'finishTypes'));
  382. $form->get('deliveryYears')->setData($compoundService->getParameter($criteria, 'deliveryYears'));
  383. return $form;
  384. }
  385. private function getAggregationType(string $field): string
  386. {
  387. if (\in_array($field, CompoundFeedback::SINGLE_AGGREGATION_FIELDS)) {
  388. return CompoundFeedback::AGGERGATION_SINGLE_COUNT_TYPE;
  389. }
  390. return CompoundFeedback::AGGERGATION_BUCKET_COUNT_TYPE;
  391. }
  392. /**
  393. * @throws \Exception
  394. */
  395. public function getCustomParagraph(array $criteria): ?CustomParagraph
  396. {
  397. $customParagraphs = null;
  398. $selectedCustomParagraphId = $criteria['selectedParagraphId'] ?? null;
  399. $criteria = $this->getCriteriaCustomParagraph($criteria);
  400. $customParagraphRepository = $this->getCustomParagraphRepository();
  401. $this->getTranslatableContainer()->setTranslationFallback(false);
  402. if ($selectedCustomParagraphId) {
  403. $criteria['id'] = $selectedCustomParagraphId;
  404. $customParagraphs = $this->getSelectedCustomParagraph($customParagraphRepository, $criteria);
  405. }
  406. if (!$customParagraphs) {
  407. $customParagraphs = $customParagraphRepository
  408. ->get(array_merge($criteria, ['published' => true]))
  409. ->getQuery()
  410. ->getResult();
  411. }
  412. if (!empty($customParagraphs)) {
  413. $customParagraphs = $customParagraphs[array_rand($customParagraphs)];
  414. } else {
  415. $customParagraphs = null;
  416. }
  417. return $customParagraphs;
  418. }
  419. private function getCriteriaCustomParagraph($criteria): array
  420. {
  421. return [
  422. 'place' => CustomParagraphPlaceTypes::COMPOUND_PLANNER,
  423. 'section' => ListingSections::PROJECTS,
  424. 'propertyType' => $criteria['propertyTypes'] ?? null,
  425. 'compoundLocations' => $criteria['selectedCompoundLocation'] ?? null,
  426. ];
  427. }
  428. private function getCustomParagraphRepository(): CustomParagraphRepository
  429. {
  430. /* @var CustomParagraphRepository $customParagraphRepository */
  431. return $this->entityManager->getRepository(CustomParagraph::class);
  432. }
  433. private function getSelectedCustomParagraph($customParagraphRepository, $criteria)
  434. {
  435. $result = null;
  436. try {
  437. $result = $customParagraphRepository
  438. ->get($criteria)
  439. ->getQuery()
  440. ->setMaxResults(self::CUSTOM_PARAGRAPH_MAX_RESULTS)
  441. ->getOneOrNullResult();
  442. } catch (NonUniqueResultException) {
  443. $result = null;
  444. }
  445. return $result;
  446. }
  447. /**
  448. * @return \Gedmo\Translatable\TranslatableListener|object|null
  449. *
  450. * @throws \Exception
  451. */
  452. private function getTranslatableContainer()
  453. {
  454. return $this->translatableListener;
  455. }
  456. }