PageRenderTime 54ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 0ms

/magento/module-elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php

https://bitbucket.org/sergiu-tot-fb/vendors
PHP | 353 lines | 227 code | 23 blank | 103 comment | 37 complexity | e18a84c3bb55fbdded0f56673a9b587c MD5 | raw file
Possible License(s): BSD-3-Clause, LGPL-2.1, MIT, Apache-2.0
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Elasticsearch\Model\Adapter\BatchDataMapper;
  7. use Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider;
  8. use Magento\Eav\Api\Data\AttributeInterface;
  9. use Magento\Elasticsearch\Model\Adapter\Document\Builder;
  10. use Magento\Elasticsearch\Model\Adapter\FieldMapperInterface;
  11. use Magento\Elasticsearch\Model\Adapter\BatchDataMapperInterface;
  12. use Magento\Elasticsearch\Model\Adapter\FieldType\Date as DateFieldType;
  13. use Magento\AdvancedSearch\Model\Adapter\DataMapper\AdditionalFieldsProviderInterface;
  14. /**
  15. * Map product index data to search engine metadata
  16. */
  17. class ProductDataMapper implements BatchDataMapperInterface
  18. {
  19. /**
  20. * @var Builder
  21. */
  22. private $builder;
  23. /**
  24. * @var FieldMapperInterface
  25. */
  26. private $fieldMapper;
  27. /**
  28. * @var DateFieldType
  29. */
  30. private $dateFieldType;
  31. /**
  32. * @var array
  33. */
  34. private $attributeData = [];
  35. /**
  36. * @var array
  37. */
  38. private $excludedAttributes;
  39. /**
  40. * @var AdditionalFieldsProviderInterface
  41. */
  42. private $additionalFieldsProvider;
  43. /**
  44. * @var DataProvider
  45. */
  46. private $dataProvider;
  47. /**
  48. * List of attributes which will be skipped during mapping
  49. *
  50. * @var string[]
  51. */
  52. private $defaultExcludedAttributes = [
  53. 'price',
  54. 'media_gallery',
  55. 'tier_price',
  56. 'quantity_and_stock_status',
  57. 'media_gallery',
  58. 'giftcard_amounts',
  59. ];
  60. /**
  61. * @var string[]
  62. */
  63. private $attributesExcludedFromMerge = [
  64. 'status',
  65. 'visibility',
  66. 'tax_class_id'
  67. ];
  68. /**
  69. * Construction for DocumentDataMapper
  70. *
  71. * @param Builder $builder
  72. * @param FieldMapperInterface $fieldMapper
  73. * @param DateFieldType $dateFieldType
  74. * @param AdditionalFieldsProviderInterface $additionalFieldsProvider
  75. * @param DataProvider $dataProvider
  76. * @param array $excludedAttributes
  77. */
  78. public function __construct(
  79. Builder $builder,
  80. FieldMapperInterface $fieldMapper,
  81. DateFieldType $dateFieldType,
  82. AdditionalFieldsProviderInterface $additionalFieldsProvider,
  83. DataProvider $dataProvider,
  84. array $excludedAttributes = []
  85. ) {
  86. $this->builder = $builder;
  87. $this->fieldMapper = $fieldMapper;
  88. $this->dateFieldType = $dateFieldType;
  89. $this->excludedAttributes = array_merge($this->defaultExcludedAttributes, $excludedAttributes);
  90. $this->additionalFieldsProvider = $additionalFieldsProvider;
  91. $this->dataProvider = $dataProvider;
  92. }
  93. /**
  94. * {@inheritdoc}
  95. */
  96. public function map(array $documentData, $storeId, array $context = [])
  97. {
  98. // reset attribute data for new store
  99. $this->attributeData = [];
  100. $documents = [];
  101. foreach ($documentData as $productId => $indexData) {
  102. $this->builder->addField('store_id', $storeId);
  103. $productIndexData = $this->convertToProductData($productId, $indexData, $storeId);
  104. foreach ($productIndexData as $attributeCode => $value) {
  105. // Prepare processing attribute info
  106. if (strpos($attributeCode, '_value') !== false) {
  107. $this->builder->addField($attributeCode, $value);
  108. continue;
  109. }
  110. if (in_array($attributeCode, $this->excludedAttributes, true)) {
  111. continue;
  112. }
  113. $this->builder->addField(
  114. $this->fieldMapper->getFieldName(
  115. $attributeCode,
  116. $context
  117. ),
  118. $value
  119. );
  120. }
  121. $documents[$productId] = $this->builder->build();
  122. }
  123. $productIds = array_keys($documentData);
  124. foreach ($this->additionalFieldsProvider->getFields($productIds, $storeId) as $productId => $fields) {
  125. $documents[$productId] = array_merge_recursive(
  126. $documents[$productId],
  127. $this->builder->addFields($fields)->build()
  128. );
  129. }
  130. return $documents;
  131. }
  132. /**
  133. * Convert raw data retrieved from source tables to human-readable format
  134. * E.g. [42 => [1 => 2]] will be converted to ['color' => '2', 'color_value' => 'red']
  135. *
  136. * @param int $productId
  137. * @param array $indexData
  138. * @param int $storeId
  139. * @return array
  140. */
  141. private function convertToProductData($productId, array $indexData, $storeId)
  142. {
  143. $productAttributes = [];
  144. foreach ($indexData as $attributeId => $attributeValue) {
  145. $attributeData = $this->getAttributeData($attributeId);
  146. if (!$attributeData) {
  147. continue;
  148. }
  149. $productAttributes = array_merge(
  150. $productAttributes,
  151. $this->convertAttribute(
  152. $productId,
  153. $attributeId,
  154. $attributeValue,
  155. $attributeData,
  156. $storeId
  157. )
  158. );
  159. }
  160. return $productAttributes;
  161. }
  162. /**
  163. * Convert data for attribute: 1) add new value {attribute_code}_value for select and multiselect searchable
  164. * attributes, that will contain actual value 2) add child products data to composite products
  165. *
  166. * @param int $productId
  167. * @param int $attributeId
  168. * @param mixed $attributeValue
  169. * @param array $attributeData
  170. * @param int $storeId
  171. * @return array
  172. */
  173. private function convertAttribute($productId, $attributeId, $attributeValue, array $attributeData, $storeId)
  174. {
  175. $productAttributes = [];
  176. $attributeCode = $attributeData[AttributeInterface::ATTRIBUTE_CODE];
  177. $attributeFrontendInput = $attributeData[AttributeInterface::FRONTEND_INPUT];
  178. if (is_array($attributeValue)) {
  179. if (!$attributeData['is_searchable']) {
  180. $value = $this->getValueForAttribute(
  181. $productId,
  182. $attributeCode,
  183. $attributeValue,
  184. $attributeData['is_searchable']
  185. );
  186. } else {
  187. if (($attributeFrontendInput == 'select' || $attributeFrontendInput == 'multiselect')
  188. && !in_array($attributeCode, $this->excludedAttributes)
  189. ) {
  190. $value = $this->getValueForAttribute(
  191. $productId,
  192. $attributeCode,
  193. $attributeValue,
  194. $attributeData['is_searchable']
  195. );
  196. $productAttributes[$attributeCode . '_value'] = $this->getValueForAttributeOptions(
  197. $attributeData,
  198. $attributeValue
  199. );
  200. } else {
  201. $value = implode(' ', $attributeValue);
  202. }
  203. }
  204. } else {
  205. $value = $attributeValue;
  206. }
  207. // cover case with "options"
  208. // see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::prepareProductIndex
  209. if ($value) {
  210. if ($attributeId === 'options') {
  211. $productAttributes[$attributeId] = $value;
  212. } else {
  213. if (isset($attributeData[AttributeInterface::OPTIONS][$value])) {
  214. $productAttributes[$attributeCode . '_value'] = $attributeData[AttributeInterface::OPTIONS][$value];
  215. }
  216. $productAttributes[$attributeCode] = $this->formatProductAttributeValue(
  217. $value,
  218. $attributeData,
  219. $storeId
  220. );
  221. }
  222. }
  223. return $productAttributes;
  224. }
  225. /**
  226. * Get product attribute data by attribute id
  227. *
  228. * @param int $attributeId
  229. * @return array
  230. */
  231. private function getAttributeData($attributeId)
  232. {
  233. if (!array_key_exists($attributeId, $this->attributeData)) {
  234. $attribute = $this->dataProvider->getSearchableAttribute($attributeId);
  235. if ($attribute) {
  236. $options = [];
  237. if ($attribute->getFrontendInput() === 'select' || $attribute->getFrontendInput() === 'multiselect') {
  238. foreach ($attribute->getOptions() as $option) {
  239. $options[$option->getValue()] = $option->getLabel();
  240. }
  241. }
  242. $this->attributeData[$attributeId] = [
  243. AttributeInterface::ATTRIBUTE_CODE => $attribute->getAttributeCode(),
  244. AttributeInterface::FRONTEND_INPUT => $attribute->getFrontendInput(),
  245. AttributeInterface::BACKEND_TYPE => $attribute->getBackendType(),
  246. AttributeInterface::OPTIONS => $options,
  247. 'is_searchable' => $attribute->getIsSearchable(),
  248. ];
  249. } else {
  250. $this->attributeData[$attributeId] = null;
  251. }
  252. }
  253. return $this->attributeData[$attributeId];
  254. }
  255. /**
  256. * Format product attribute value for search engine
  257. *
  258. * @param mixed $value
  259. * @param array $attributeData
  260. * @param string $storeId
  261. * @return string
  262. */
  263. private function formatProductAttributeValue($value, $attributeData, $storeId)
  264. {
  265. if ($attributeData[AttributeInterface::FRONTEND_INPUT] === 'date'
  266. || in_array($attributeData[AttributeInterface::BACKEND_TYPE], ['datetime', 'timestamp'])) {
  267. return $this->dateFieldType->formatDate($storeId, $value);
  268. } elseif ($attributeData[AttributeInterface::FRONTEND_INPUT] === 'multiselect') {
  269. return str_replace(',', ' ', $value);
  270. } else {
  271. return $value;
  272. }
  273. }
  274. /**
  275. * Return single value if value exists for the productId in array, otherwise return concatenated array values
  276. *
  277. * @param int $productId
  278. * @param string $attributeCode
  279. * @param array $attributeValue
  280. * @param bool $isSearchable
  281. * @return mixed
  282. */
  283. private function getValueForAttribute($productId, $attributeCode, array $attributeValue, $isSearchable)
  284. {
  285. if ((!$isSearchable || in_array($attributeCode, $this->attributesExcludedFromMerge))
  286. && isset($attributeValue[$productId])
  287. ) {
  288. $value = $attributeValue[$productId];
  289. } elseif (in_array($attributeCode, $this->attributesExcludedFromMerge) && !isset($attributeValue[$productId])) {
  290. $value = '';
  291. } else {
  292. $value = implode(' ', $attributeValue);
  293. }
  294. return $value;
  295. }
  296. /**
  297. * Concatenate select and multiselect attribute values
  298. *
  299. * @param array $attributeData
  300. * @param array $attributeValue
  301. * @return string
  302. */
  303. private function getValueForAttributeOptions(array $attributeData, array $attributeValue)
  304. {
  305. $result = null;
  306. $selectedValues = [];
  307. if ($attributeData[AttributeInterface::FRONTEND_INPUT] == 'select') {
  308. foreach ($attributeValue as $selectedValue) {
  309. if (isset($attributeData[AttributeInterface::OPTIONS][$selectedValue])) {
  310. $selectedValues[] = $attributeData[AttributeInterface::OPTIONS][$selectedValue];
  311. }
  312. }
  313. }
  314. if ($attributeData[AttributeInterface::FRONTEND_INPUT] == 'multiselect') {
  315. foreach ($attributeValue as $selectedAttributeValues) {
  316. $selectedAttributeValues = explode(',', $selectedAttributeValues);
  317. foreach ($selectedAttributeValues as $selectedValue) {
  318. if (isset($attributeData[AttributeInterface::OPTIONS][$selectedValue])) {
  319. $selectedValues[] = $attributeData[AttributeInterface::OPTIONS][$selectedValue];
  320. }
  321. }
  322. }
  323. }
  324. $selectedValues = array_unique($selectedValues);
  325. if (!empty($selectedValues)) {
  326. $result = implode(' ', $selectedValues);
  327. }
  328. return $result;
  329. }
  330. }