RowCustomizer.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\BundleImportExport\Model\Export;
  7. use Magento\Catalog\Model\ResourceModel\Product\Collection;
  8. use Magento\CatalogImportExport\Model\Export\RowCustomizerInterface;
  9. use Magento\CatalogImportExport\Model\Import\Product as ImportProductModel;
  10. use Magento\Bundle\Model\ResourceModel\Selection\Collection as SelectionCollection;
  11. use Magento\ImportExport\Model\Import as ImportModel;
  12. use Magento\Catalog\Model\Product\Type\AbstractType;
  13. use Magento\Store\Model\StoreManagerInterface;
  14. /**
  15. * Class RowCustomizer
  16. */
  17. class RowCustomizer implements RowCustomizerInterface
  18. {
  19. const BUNDLE_PRICE_TYPE_COL = 'bundle_price_type';
  20. const BUNDLE_SKU_TYPE_COL = 'bundle_sku_type';
  21. const BUNDLE_PRICE_VIEW_COL = 'bundle_price_view';
  22. const BUNDLE_WEIGHT_TYPE_COL = 'bundle_weight_type';
  23. const BUNDLE_VALUES_COL = 'bundle_values';
  24. const VALUE_FIXED = 'fixed';
  25. const VALUE_DYNAMIC = 'dynamic';
  26. const VALUE_PERCENT = 'percent';
  27. const VALUE_PRICE_RANGE = 'Price range';
  28. const VALUE_AS_LOW_AS = 'As low as';
  29. /**
  30. * Mapping for bundle types
  31. *
  32. * @var array
  33. */
  34. protected $typeMapping = [
  35. '0' => self::VALUE_DYNAMIC,
  36. '1' => self::VALUE_FIXED
  37. ];
  38. /**
  39. * Mapping for price views
  40. *
  41. * @var array
  42. */
  43. protected $priceViewMapping = [
  44. '0' => self::VALUE_PRICE_RANGE,
  45. '1' => self::VALUE_AS_LOW_AS
  46. ];
  47. /**
  48. * Mapping for price types
  49. *
  50. * @var array
  51. */
  52. protected $priceTypeMapping = [
  53. '0' => self::VALUE_FIXED,
  54. '1' => self::VALUE_PERCENT
  55. ];
  56. /**
  57. * Bundle product columns
  58. *
  59. * @var array
  60. */
  61. protected $bundleColumns = [
  62. self::BUNDLE_PRICE_TYPE_COL,
  63. self::BUNDLE_SKU_TYPE_COL,
  64. self::BUNDLE_PRICE_VIEW_COL,
  65. self::BUNDLE_WEIGHT_TYPE_COL,
  66. self::BUNDLE_VALUES_COL
  67. ];
  68. /**
  69. * Product's bundle data
  70. *
  71. * @var array
  72. */
  73. protected $bundleData = [];
  74. /**
  75. * Column name for shipment_type attribute
  76. *
  77. * @var string
  78. */
  79. private $shipmentTypeColumn = 'bundle_shipment_type';
  80. /**
  81. * Mapping for shipment type
  82. *
  83. * @var array
  84. */
  85. private $shipmentTypeMapping = [
  86. AbstractType::SHIPMENT_TOGETHER => 'together',
  87. AbstractType::SHIPMENT_SEPARATELY => 'separately',
  88. ];
  89. /**
  90. * @var \Magento\Bundle\Model\ResourceModel\Option\Collection[]
  91. */
  92. private $optionCollections = [];
  93. /**
  94. * @var array
  95. */
  96. private $storeIdToCode = [];
  97. /**
  98. * @var string
  99. */
  100. private $optionCollectionCacheKey = '_cache_instance_options_collection';
  101. /**
  102. * @var StoreManagerInterface
  103. */
  104. private $storeManager;
  105. /**
  106. * @param StoreManagerInterface $storeManager
  107. */
  108. public function __construct(StoreManagerInterface $storeManager)
  109. {
  110. $this->storeManager = $storeManager;
  111. }
  112. /**
  113. * Retrieve list of bundle specific columns
  114. * @return array
  115. */
  116. private function getBundleColumns()
  117. {
  118. return array_merge($this->bundleColumns, [$this->shipmentTypeColumn]);
  119. }
  120. /**
  121. * Prepare data for export
  122. *
  123. * @param \Magento\Catalog\Model\ResourceModel\Product\Collection $collection
  124. * @param int[] $productIds
  125. * @return $this
  126. */
  127. public function prepareData($collection, $productIds)
  128. {
  129. $productCollection = clone $collection;
  130. $productCollection->addAttributeToFilter(
  131. 'entity_id',
  132. ['in' => $productIds]
  133. )->addAttributeToFilter(
  134. 'type_id',
  135. ['eq' => \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE]
  136. );
  137. return $this->populateBundleData($productCollection);
  138. }
  139. /**
  140. * Set headers columns
  141. *
  142. * @param array $columns
  143. * @return array
  144. */
  145. public function addHeaderColumns($columns)
  146. {
  147. $columns = array_merge($columns, $this->getBundleColumns());
  148. return $columns;
  149. }
  150. /**
  151. * Add data for export
  152. *
  153. * @param array $dataRow
  154. * @param int $productId
  155. * @return array
  156. */
  157. public function addData($dataRow, $productId)
  158. {
  159. if (!empty($this->bundleData[$productId])) {
  160. $dataRow = array_merge($this->cleanNotBundleAdditionalAttributes($dataRow), $this->bundleData[$productId]);
  161. }
  162. return $dataRow;
  163. }
  164. /**
  165. * Calculate the largest links block
  166. *
  167. * @param array $additionalRowsCount
  168. * @param int $productId
  169. * @return array
  170. * @SuppressWarnings(PHPMD.UnusedFormalParameter)
  171. */
  172. public function getAdditionalRowsCount($additionalRowsCount, $productId)
  173. {
  174. return $additionalRowsCount;
  175. }
  176. /**
  177. * Populate bundle product data
  178. *
  179. * @param \Magento\Catalog\Model\ResourceModel\Product\Collection $collection
  180. * @return $this
  181. */
  182. protected function populateBundleData($collection)
  183. {
  184. foreach ($collection as $product) {
  185. $id = $product->getEntityId();
  186. $this->bundleData[$id][self::BUNDLE_PRICE_TYPE_COL] = $this->getTypeValue($product->getPriceType());
  187. $this->bundleData[$id][$this->shipmentTypeColumn] = $this->getShipmentTypeValue(
  188. $product->getShipmentType()
  189. );
  190. $this->bundleData[$id][self::BUNDLE_SKU_TYPE_COL] = $this->getTypeValue($product->getSkuType());
  191. $this->bundleData[$id][self::BUNDLE_PRICE_VIEW_COL] = $this->getPriceViewValue($product->getPriceView());
  192. $this->bundleData[$id][self::BUNDLE_WEIGHT_TYPE_COL] = $this->getTypeValue($product->getWeightType());
  193. $this->bundleData[$id][self::BUNDLE_VALUES_COL] = $this->getFormattedBundleOptionValues($product);
  194. }
  195. return $this;
  196. }
  197. /**
  198. * Retrieve formatted bundle options
  199. *
  200. * @param \Magento\Catalog\Model\Product $product
  201. * @return string
  202. */
  203. protected function getFormattedBundleOptionValues(\Magento\Catalog\Model\Product $product): string
  204. {
  205. $optionCollections = $this->getProductOptionCollection($product);
  206. $bundleData = '';
  207. $optionTitles = $this->getBundleOptionTitles($product);
  208. foreach ($optionCollections->getItems() as $option) {
  209. $optionValues = $this->getFormattedOptionValues($option, $optionTitles);
  210. $bundleData .= $this->getFormattedBundleSelections(
  211. $optionValues,
  212. $product->getTypeInstance()
  213. ->getSelectionsCollection([$option->getId()], $product)
  214. ->setOrder('position', Collection::SORT_ORDER_ASC)
  215. );
  216. }
  217. return rtrim($bundleData, ImportProductModel::PSEUDO_MULTI_LINE_SEPARATOR);
  218. }
  219. /**
  220. * Retrieve formatted bundle selections
  221. *
  222. * @param string $optionValues
  223. * @param SelectionCollection $selections
  224. * @return string
  225. */
  226. protected function getFormattedBundleSelections($optionValues, SelectionCollection $selections)
  227. {
  228. $bundleData = '';
  229. $selections->addAttributeToSort('position');
  230. foreach ($selections as $selection) {
  231. $selectionData = [
  232. 'sku' => $selection->getSku(),
  233. 'price' => $selection->getSelectionPriceValue(),
  234. 'default' => $selection->getIsDefault(),
  235. 'default_qty' => $selection->getSelectionQty(),
  236. 'price_type' => $this->getPriceTypeValue($selection->getSelectionPriceType()),
  237. 'can_change_qty' => $selection->getSelectionCanChangeQty(),
  238. ];
  239. $bundleData .= $optionValues
  240. . ImportModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR
  241. . implode(
  242. ImportModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR,
  243. array_map(
  244. function ($value, $key) {
  245. return $key . ImportProductModel::PAIR_NAME_VALUE_SEPARATOR . $value;
  246. },
  247. $selectionData,
  248. array_keys($selectionData)
  249. )
  250. )
  251. . ImportProductModel::PSEUDO_MULTI_LINE_SEPARATOR;
  252. }
  253. return $bundleData;
  254. }
  255. /**
  256. * Retrieve option value of bundle product
  257. *
  258. * @param \Magento\Bundle\Model\Option $option
  259. * @param string[] $optionTitles
  260. * @return string
  261. */
  262. protected function getFormattedOptionValues(
  263. \Magento\Bundle\Model\Option $option,
  264. array $optionTitles = []
  265. ): string {
  266. $names = implode(ImportModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR, array_map(
  267. function ($title, $storeName) {
  268. return $storeName . ImportProductModel::PAIR_NAME_VALUE_SEPARATOR . $title;
  269. },
  270. $optionTitles[$option->getOptionId()],
  271. array_keys($optionTitles[$option->getOptionId()])
  272. ));
  273. return $names . ImportModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR
  274. . 'type' . ImportProductModel::PAIR_NAME_VALUE_SEPARATOR
  275. . $option->getType() . ImportModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR
  276. . 'required' . ImportProductModel::PAIR_NAME_VALUE_SEPARATOR
  277. . $option->getRequired();
  278. }
  279. /**
  280. * Retrieve bundle type value by code
  281. *
  282. * @param string $type
  283. * @return string
  284. */
  285. protected function getTypeValue($type)
  286. {
  287. return $this->typeMapping[$type] ?? self::VALUE_DYNAMIC;
  288. }
  289. /**
  290. * Retrieve bundle price view value by code
  291. *
  292. * @param string $type
  293. * @return string
  294. */
  295. protected function getPriceViewValue($type)
  296. {
  297. return $this->priceViewMapping[$type] ?? self::VALUE_PRICE_RANGE;
  298. }
  299. /**
  300. * Retrieve bundle price type value by code
  301. *
  302. * @param string $type
  303. * @return string
  304. */
  305. protected function getPriceTypeValue($type)
  306. {
  307. return $this->priceTypeMapping[$type] ?? null;
  308. }
  309. /**
  310. * Retrieve bundle shipment type value by code
  311. *
  312. * @param string $type
  313. * @return string
  314. */
  315. private function getShipmentTypeValue($type)
  316. {
  317. return $this->shipmentTypeMapping[$type] ?? null;
  318. }
  319. /**
  320. * Remove bundle specified additional attributes as now they are stored in specified columns
  321. *
  322. * @param array $dataRow
  323. * @return array
  324. */
  325. protected function cleanNotBundleAdditionalAttributes($dataRow)
  326. {
  327. if (!empty($dataRow['additional_attributes'])) {
  328. $additionalAttributes = $this->parseAdditionalAttributes($dataRow['additional_attributes']);
  329. $dataRow['additional_attributes'] = $this->getNotBundleAttributes($additionalAttributes);
  330. }
  331. return $dataRow;
  332. }
  333. /**
  334. * Retrieve not bundle additional attributes
  335. *
  336. * @param array $additionalAttributes
  337. * @return string
  338. */
  339. protected function getNotBundleAttributes($additionalAttributes)
  340. {
  341. $filteredAttributes = [];
  342. foreach ($additionalAttributes as $code => $value) {
  343. if (!in_array('bundle_' . $code, $this->getBundleColumns())) {
  344. $filteredAttributes[] = $code . ImportProductModel::PAIR_NAME_VALUE_SEPARATOR . $value;
  345. }
  346. }
  347. return implode(ImportModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR, $filteredAttributes);
  348. }
  349. /**
  350. * Retrieves additional attributes as array code=>value.
  351. *
  352. * @param string $additionalAttributes
  353. * @return array
  354. */
  355. private function parseAdditionalAttributes($additionalAttributes)
  356. {
  357. $attributeNameValuePairs = explode(ImportModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR, $additionalAttributes);
  358. $preparedAttributes = [];
  359. $code = '';
  360. foreach ($attributeNameValuePairs as $attributeData) {
  361. //process case when attribute has ImportModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR inside its value
  362. if (strpos($attributeData, ImportProductModel::PAIR_NAME_VALUE_SEPARATOR) === false) {
  363. if (!$code) {
  364. continue;
  365. }
  366. $preparedAttributes[$code] .= ImportModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR . $attributeData;
  367. continue;
  368. }
  369. list($code, $value) = explode(ImportProductModel::PAIR_NAME_VALUE_SEPARATOR, $attributeData, 2);
  370. $preparedAttributes[$code] = $value;
  371. }
  372. return $preparedAttributes;
  373. }
  374. /**
  375. * Get product options titles.
  376. *
  377. * Values for all store views (default) should be specified with 'name' key.
  378. * If user want to specify value or change existing for non default store views it should be specified with
  379. * 'name_' prefix and needed store view suffix.
  380. *
  381. * For example:
  382. * - 'name=All store views name' for all store views
  383. * - 'name_specific_store=Specific store name' for store view with 'specific_store' store code
  384. *
  385. * @param \Magento\Catalog\Model\Product $product
  386. * @return array
  387. */
  388. private function getBundleOptionTitles(\Magento\Catalog\Model\Product $product): array
  389. {
  390. $optionCollections = $this->getProductOptionCollection($product);
  391. $optionsTitles = [];
  392. /** @var \Magento\Bundle\Model\Option $option */
  393. foreach ($optionCollections->getItems() as $option) {
  394. $optionsTitles[$option->getId()]['name'] = $option->getTitle();
  395. }
  396. $storeIds = $product->getStoreIds();
  397. if (count($storeIds) > 1) {
  398. foreach ($storeIds as $storeId) {
  399. $optionCollections = $this->getProductOptionCollection($product, (int)$storeId);
  400. /** @var \Magento\Bundle\Model\Option $option */
  401. foreach ($optionCollections->getItems() as $option) {
  402. $optionTitle = $option->getTitle();
  403. if ($optionsTitles[$option->getId()]['name'] != $optionTitle) {
  404. $optionsTitles[$option->getId()]['name_' . $this->getStoreCodeById((int)$storeId)] =
  405. $optionTitle;
  406. }
  407. }
  408. }
  409. }
  410. return $optionsTitles;
  411. }
  412. /**
  413. * Get product options collection by provided product model.
  414. *
  415. * Set given store id to the product if it was defined (default store id will be set if was not).
  416. *
  417. * @param \Magento\Catalog\Model\Product $product $product
  418. * @param int $storeId
  419. * @return \Magento\Bundle\Model\ResourceModel\Option\Collection
  420. */
  421. private function getProductOptionCollection(
  422. \Magento\Catalog\Model\Product $product,
  423. int $storeId = \Magento\Store\Model\Store::DEFAULT_STORE_ID
  424. ): \Magento\Bundle\Model\ResourceModel\Option\Collection {
  425. $productSku = $product->getSku();
  426. if (!isset($this->optionCollections[$productSku][$storeId])) {
  427. $product->unsetData($this->optionCollectionCacheKey);
  428. $product->setStoreId($storeId);
  429. $this->optionCollections[$productSku][$storeId] = $product->getTypeInstance()
  430. ->getOptionsCollection($product)
  431. ->setOrder('position', Collection::SORT_ORDER_ASC);
  432. }
  433. return $this->optionCollections[$productSku][$storeId];
  434. }
  435. /**
  436. * Retrieve store code by it's ID.
  437. *
  438. * Collect store id in $storeIdToCode[] private variable if it was not initialized earlier.
  439. *
  440. * @param int $storeId
  441. * @return string
  442. */
  443. private function getStoreCodeById(int $storeId): string
  444. {
  445. if (!isset($this->storeIdToCode[$storeId])) {
  446. $this->storeIdToCode[$storeId] = $this->storeManager->getStore($storeId)->getCode();
  447. }
  448. return $this->storeIdToCode[$storeId];
  449. }
  450. }