Mapper.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Framework\Search\Request;
  7. use Magento\Framework\Exception\StateException;
  8. use Magento\Framework\Search\Request\Query\Filter;
  9. use Magento\Framework\Phrase;
  10. /**
  11. * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  12. * @api
  13. * @since 100.0.2
  14. */
  15. class Mapper
  16. {
  17. /**
  18. * @var QueryInterface
  19. */
  20. private $rootQuery;
  21. /**
  22. * @var array
  23. */
  24. private $queries;
  25. /**
  26. * @var array
  27. */
  28. private $filters;
  29. /**
  30. * @var string[]
  31. */
  32. private $mappedQueries;
  33. /**
  34. * @var string[]
  35. */
  36. private $mappedFilters;
  37. /**
  38. * @var array
  39. */
  40. private $aggregations;
  41. /**
  42. * @var \Magento\Framework\ObjectManagerInterface
  43. */
  44. private $objectManager;
  45. /**
  46. * @var string
  47. */
  48. private $rootQueryName;
  49. /**
  50. * @param \Magento\Framework\ObjectManagerInterface $objectManager
  51. * @param array $queries
  52. * @param string $rootQueryName
  53. * @param array $aggregations
  54. * @param array $filters
  55. * @throws \Exception
  56. * @throws \InvalidArgumentException
  57. * @throws StateException
  58. */
  59. public function __construct(
  60. \Magento\Framework\ObjectManagerInterface $objectManager,
  61. array $queries,
  62. $rootQueryName,
  63. array $aggregations = [],
  64. array $filters = []
  65. ) {
  66. $this->objectManager = $objectManager;
  67. $this->queries = $queries;
  68. $this->aggregations = $aggregations;
  69. $this->filters = $filters;
  70. $this->rootQueryName = $rootQueryName;
  71. }
  72. /**
  73. * Get Query Interface by name
  74. *
  75. * @return QueryInterface
  76. * @throws \Exception
  77. * @throws \InvalidArgumentException
  78. * @throws StateException
  79. */
  80. public function getRootQuery()
  81. {
  82. if (!$this->rootQuery) {
  83. $this->mappedQueries = [];
  84. $this->mappedFilters = [];
  85. $this->rootQuery = $this->mapQuery($this->rootQueryName);
  86. $this->validate();
  87. }
  88. return $this->rootQuery;
  89. }
  90. /**
  91. * Convert array to Query instance
  92. *
  93. * @param string $queryName
  94. * @return QueryInterface
  95. * @throws \Exception
  96. * @throws \InvalidArgumentException
  97. * @throws StateException
  98. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  99. */
  100. private function mapQuery($queryName)
  101. {
  102. if (!isset($this->queries[$queryName])) {
  103. throw new \Exception('Query ' . $queryName . ' does not exist');
  104. } elseif (in_array($queryName, $this->mappedQueries)) {
  105. throw new StateException(
  106. new Phrase('A cycle was found. The "%1" query is already used in the request hierarchy.', [$queryName])
  107. );
  108. }
  109. $this->mappedQueries[] = $queryName;
  110. $query = $this->queries[$queryName];
  111. switch ($query['type']) {
  112. case QueryInterface::TYPE_MATCH:
  113. $query = $this->objectManager->create(
  114. \Magento\Framework\Search\Request\Query\Match::class,
  115. [
  116. 'name' => $query['name'],
  117. 'value' => $query['value'],
  118. 'boost' => isset($query['boost']) ? $query['boost'] : 1,
  119. 'matches' => $query['match']
  120. ]
  121. );
  122. break;
  123. case QueryInterface::TYPE_FILTER:
  124. if (isset($query['queryReference'][0])) {
  125. $reference = $this->mapQuery($query['queryReference'][0]['ref']);
  126. $referenceType = Filter::REFERENCE_QUERY;
  127. } elseif (isset($query['filterReference'][0])) {
  128. $reference = $this->mapFilter($query['filterReference'][0]['ref']);
  129. $referenceType = Filter::REFERENCE_FILTER;
  130. } else {
  131. throw new \Exception('Reference is not provided');
  132. }
  133. $query = $this->objectManager->create(
  134. \Magento\Framework\Search\Request\Query\Filter::class,
  135. [
  136. 'name' => $query['name'],
  137. 'boost' => isset($query['boost']) ? $query['boost'] : 1,
  138. 'reference' => $reference,
  139. 'referenceType' => $referenceType
  140. ]
  141. );
  142. break;
  143. case QueryInterface::TYPE_BOOL:
  144. $aggregatedByType = $this->aggregateQueriesByType($query['queryReference']);
  145. $query = $this->objectManager->create(
  146. \Magento\Framework\Search\Request\Query\BoolExpression::class,
  147. array_merge(
  148. ['name' => $query['name'], 'boost' => isset($query['boost']) ? $query['boost'] : 1],
  149. $aggregatedByType
  150. )
  151. );
  152. break;
  153. default:
  154. throw new \InvalidArgumentException('Invalid query type');
  155. }
  156. return $query;
  157. }
  158. /**
  159. * Convert array to Filter instance
  160. *
  161. * @param string $filterName
  162. * @throws \Exception
  163. * @return FilterInterface
  164. * @throws \Exception
  165. * @throws \InvalidArgumentException
  166. * @throws StateException
  167. */
  168. private function mapFilter($filterName)
  169. {
  170. if (!isset($this->filters[$filterName])) {
  171. throw new \Exception('Filter ' . $filterName . ' does not exist');
  172. } elseif (in_array($filterName, $this->mappedFilters)) {
  173. throw new StateException(
  174. new Phrase(
  175. 'A cycle was found. The "%1" filter is already used in the request hierarchy.',
  176. [$filterName]
  177. )
  178. );
  179. }
  180. $this->mappedFilters[] = $filterName;
  181. $filter = $this->filters[$filterName];
  182. switch ($filter['type']) {
  183. case FilterInterface::TYPE_TERM:
  184. $filter = $this->objectManager->create(
  185. \Magento\Framework\Search\Request\Filter\Term::class,
  186. [
  187. 'name' => $filter['name'],
  188. 'field' => $filter['field'],
  189. 'value' => $filter['value']
  190. ]
  191. );
  192. break;
  193. case FilterInterface::TYPE_RANGE:
  194. $filter = $this->objectManager->create(
  195. \Magento\Framework\Search\Request\Filter\Range::class,
  196. [
  197. 'name' => $filter['name'],
  198. 'field' => $filter['field'],
  199. 'from' => isset($filter['from']) ? $filter['from'] : null,
  200. 'to' => isset($filter['to']) ? $filter['to'] : null
  201. ]
  202. );
  203. break;
  204. case FilterInterface::TYPE_WILDCARD:
  205. $filter = $this->objectManager->create(
  206. \Magento\Framework\Search\Request\Filter\Wildcard::class,
  207. [
  208. 'name' => $filter['name'],
  209. 'field' => $filter['field'],
  210. 'value' => $filter['value']
  211. ]
  212. );
  213. break;
  214. case FilterInterface::TYPE_BOOL:
  215. $aggregatedByType = $this->aggregateFiltersByType($filter['filterReference']);
  216. $filter = $this->objectManager->create(
  217. \Magento\Framework\Search\Request\Filter\BoolExpression::class,
  218. array_merge(
  219. ['name' => $filter['name']],
  220. $aggregatedByType
  221. )
  222. );
  223. break;
  224. default:
  225. throw new \InvalidArgumentException('Invalid filter type');
  226. }
  227. return $filter;
  228. }
  229. /**
  230. * Aggregate Filters by clause
  231. *
  232. * @param array $data
  233. * @return array
  234. */
  235. private function aggregateFiltersByType($data)
  236. {
  237. $list = [];
  238. foreach ($data as $value) {
  239. $list[$value['clause']][$value['ref']] = $this->mapFilter($value['ref']);
  240. }
  241. return $list;
  242. }
  243. /**
  244. * Aggregate Queries by clause
  245. *
  246. * @param array $data
  247. * @return array
  248. */
  249. private function aggregateQueriesByType($data)
  250. {
  251. $list = [];
  252. foreach ($data as $value) {
  253. $list[$value['clause']][$value['ref']] = $this->mapQuery($value['ref']);
  254. }
  255. return $list;
  256. }
  257. /**
  258. * @return void
  259. * @throws StateException
  260. */
  261. private function validate()
  262. {
  263. $this->validateQueries();
  264. $this->validateFilters();
  265. }
  266. /**
  267. * @return void
  268. * @throws StateException
  269. */
  270. private function validateQueries()
  271. {
  272. $this->validateNotUsed($this->queries, $this->mappedQueries, 'Query %1 is not used in request hierarchy');
  273. }
  274. /**
  275. * @param array $elements
  276. * @param string[] $mappedElements
  277. * @param string $errorMessage
  278. * @return void
  279. * @throws \Magento\Framework\Exception\StateException
  280. */
  281. private function validateNotUsed($elements, $mappedElements, $errorMessage)
  282. {
  283. $allElements = array_keys($elements);
  284. $notUsedElements = implode(', ', array_diff($allElements, $mappedElements));
  285. if (!empty($notUsedElements)) {
  286. throw new StateException(new Phrase($errorMessage, [$notUsedElements]));
  287. }
  288. }
  289. /**
  290. * @return void
  291. * @throws StateException
  292. */
  293. private function validateFilters()
  294. {
  295. $this->validateNotUsed($this->filters, $this->mappedFilters, 'Filter %1 is not used in request hierarchy');
  296. }
  297. /**
  298. * Build BucketInterface[] from array
  299. *
  300. * @return array
  301. * @throws StateException
  302. */
  303. public function getBuckets()
  304. {
  305. $buckets = [];
  306. foreach ($this->aggregations as $bucketData) {
  307. $arguments = [
  308. 'name' => $bucketData['name'],
  309. 'field' => $bucketData['field'],
  310. 'metrics' => $this->mapMetrics($bucketData),
  311. ];
  312. switch ($bucketData['type']) {
  313. case BucketInterface::TYPE_TERM:
  314. $bucket = $this->objectManager->create(
  315. \Magento\Framework\Search\Request\Aggregation\TermBucket::class,
  316. $arguments
  317. );
  318. break;
  319. case BucketInterface::TYPE_RANGE:
  320. $bucket = $this->objectManager->create(
  321. \Magento\Framework\Search\Request\Aggregation\RangeBucket::class,
  322. array_merge(
  323. $arguments,
  324. ['ranges' => $this->mapRanges($bucketData)]
  325. )
  326. );
  327. break;
  328. case BucketInterface::TYPE_DYNAMIC:
  329. $bucket = $this->objectManager->create(
  330. \Magento\Framework\Search\Request\Aggregation\DynamicBucket::class,
  331. array_merge(
  332. $arguments,
  333. ['method' => $bucketData['method']]
  334. )
  335. );
  336. break;
  337. default:
  338. throw new StateException(new Phrase('The bucket type is invalid. Verify and try again.'));
  339. break;
  340. }
  341. $buckets[] = $bucket;
  342. }
  343. return $buckets;
  344. }
  345. /**
  346. * Build Metric[] from array
  347. *
  348. * @param array $bucketData
  349. * @return array
  350. */
  351. private function mapMetrics(array $bucketData)
  352. {
  353. $metricObjects = [];
  354. if (isset($bucketData['metric'])) {
  355. $metrics = $bucketData['metric'];
  356. foreach ($metrics as $metric) {
  357. $metricObjects[] = $this->objectManager->create(
  358. \Magento\Framework\Search\Request\Aggregation\Metric::class,
  359. [
  360. 'type' => $metric['type']
  361. ]
  362. );
  363. }
  364. }
  365. return $metricObjects;
  366. }
  367. /**
  368. * Build Range[] from array
  369. *
  370. * @param array $bucketData
  371. * @return array
  372. */
  373. private function mapRanges(array $bucketData)
  374. {
  375. $rangeObjects = [];
  376. if (isset($bucketData['range'])) {
  377. $ranges = $bucketData['range'];
  378. foreach ($ranges as $range) {
  379. $rangeObjects[] = $this->objectManager->create(
  380. \Magento\Framework\Search\Request\Aggregation\Range::class,
  381. [
  382. 'from' => $range['from'],
  383. 'to' => $range['to']
  384. ]
  385. );
  386. }
  387. }
  388. return $rangeObjects;
  389. }
  390. }