diff --git a/vendor/magento/module-catalog-import-export/Model/Import/Product/Option.php b/vendor/magento/module-catalog-import-export/Model/Import/Product/Option.php index 7757a2ba5eb7..aef07a2109a6 100644 --- a/vendor/magento/module-catalog-import-export/Model/Import/Product/Option.php +++ b/vendor/magento/module-catalog-import-export/Model/Import/Product/Option.php @@ -7,13 +7,24 @@ namespace Magento\CatalogImportExport\Model\Import\Product; use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\ProductFactory; +use Magento\Catalog\Model\ResourceModel\Product\Option\CollectionFactory; use Magento\Catalog\Model\ResourceModel\Product\Option\Value\Collection as ProductOptionValueCollection; use Magento\Catalog\Model\ResourceModel\Product\Option\Value\CollectionFactory as ProductOptionValueCollectionFactory; use Magento\CatalogImportExport\Model\Import\Product; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Model\ResourceModel\Db\TransactionManagerInterface; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; use Magento\ImportExport\Model\Import; use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; +use Magento\ImportExport\Model\ResourceModel\CollectionByPagesIteratorFactory; +use Magento\ImportExport\Model\ResourceModel\Helper; +use Magento\ImportExport\Model\ResourceModel\Import\Data; use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; /** * Entity class which provide possibility to import product custom options @@ -341,26 +352,40 @@ class Option extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity private $optionTypeTitles; /** + * @var TransactionManagerInterface|null + */ + private $transactionManager; + + /** + * Contains mapping between new assigned option ID and ID in DB + * + * @var array + */ + private $optionNewIdExistingIdMap = []; + + /** + * Contains mapping between new assigned option_type ID and ID in DB + * * @var array */ - private $lastOptionTitle; + private $optionTypeNewIdExistingIdMap = []; /** - * @param \Magento\ImportExport\Model\ResourceModel\Import\Data $importData + * @param Data $importData * @param ResourceConnection $resource - * @param \Magento\ImportExport\Model\ResourceModel\Helper $resourceHelper - * @param \Magento\Store\Model\StoreManagerInterface $_storeManager - * @param \Magento\Catalog\Model\ProductFactory $productFactory - * @param \Magento\Catalog\Model\ResourceModel\Product\Option\CollectionFactory $optionColFactory - * @param \Magento\ImportExport\Model\ResourceModel\CollectionByPagesIteratorFactory $colIteratorFactory + * @param Helper $resourceHelper + * @param StoreManagerInterface $_storeManager + * @param ProductFactory $productFactory + * @param CollectionFactory $optionColFactory + * @param CollectionByPagesIteratorFactory $colIteratorFactory * @param \Magento\Catalog\Helper\Data $catalogData - * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig - * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $dateTime + * @param ScopeConfigInterface $scopeConfig + * @param TimezoneInterface $dateTime * @param ProcessingErrorAggregatorInterface $errorAggregator * @param array $data - * @param ProductOptionValueCollectionFactory $productOptionValueCollectionFactory - * @throws \Magento\Framework\Exception\LocalizedException - * + * @param ProductOptionValueCollectionFactory|null $productOptionValueCollectionFactory + * @param TransactionManagerInterface|null $transactionManager + * @throws LocalizedException * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -376,7 +401,8 @@ public function __construct( \Magento\Framework\Stdlib\DateTime\TimezoneInterface $dateTime, ProcessingErrorAggregatorInterface $errorAggregator, array $data = [], - ProductOptionValueCollectionFactory $productOptionValueCollectionFactory = null + ProductOptionValueCollectionFactory $productOptionValueCollectionFactory = null, + ?TransactionManagerInterface $transactionManager = null ) { $this->_resource = $resource; $this->_catalogData = $catalogData; @@ -388,7 +414,9 @@ public function __construct( $this->_scopeConfig = $scopeConfig; $this->dateTime = $dateTime; $this->productOptionValueCollectionFactory = $productOptionValueCollectionFactory - ?: \Magento\Framework\App\ObjectManager::getInstance()->get(ProductOptionValueCollectionFactory::class); + ?: ObjectManager::getInstance()->get(ProductOptionValueCollectionFactory::class); + $this->transactionManager = $transactionManager + ?: ObjectManager::getInstance()->get(TransactionManagerInterface::class); if (isset($data['connection'])) { $this->_connection = $data['connection']; @@ -805,10 +833,12 @@ protected function _findExistingOptionId(array $newOptionData, array $newOptionT ksort($newOptionTitles); $existingOptions = $this->getOldCustomOptions()[$productId]; foreach ($existingOptions as $optionId => $optionData) { - if ($optionData['type'] == $newOptionData['type'] - && $optionData['titles'][Store::DEFAULT_STORE_ID] == $newOptionTitles[Store::DEFAULT_STORE_ID] - ) { - return $optionId; + if ($optionData['type'] == $newOptionData['type']) { + foreach ($newOptionTitles as $storeId => $title) { + if (isset($optionData['titles'][$storeId]) && $optionData['titles'][$storeId] === $title) { + return $optionId; + } + } } } } @@ -1256,13 +1286,16 @@ private function addFileOptions($result, $optionRow) protected function _importData() { $this->_initProductsSku(); - $nextOptionId = $this->_resourceHelper->getNextAutoincrement($this->_tables['catalog_product_option']); - $nextValueId = $this->_resourceHelper->getNextAutoincrement( + $nextOptionId = (int) $this->_resourceHelper->getNextAutoincrement($this->_tables['catalog_product_option']); + $nextValueId = (int) $this->_resourceHelper->getNextAutoincrement( $this->_tables['catalog_product_option_type_value'] ); $prevOptionId = 0; $optionId = null; $valueId = null; + $this->optionNewIdExistingIdMap = []; + $this->optionTypeNewIdExistingIdMap = []; + $prevRowSku = null; while ($bunch = $this->_dataSourceModel->getNextBunch()) { $products = []; $options = []; @@ -1275,11 +1308,14 @@ protected function _importData() $childCount = []; $optionsToRemove = []; foreach ($bunch as $rowNumber => $rowData) { - if (isset($optionId, $valueId) && - (empty($rowData[PRODUCT::COL_STORE_VIEW_CODE]) || empty($rowData['custom_options'])) - ) { - $nextOptionId = $optionId; - $nextValueId = $valueId; + $rowSku = !empty($rowData[self::COLUMN_SKU]) + ? mb_strtolower($rowData[self::COLUMN_SKU]) + : ''; + + if ($rowSku !== $prevRowSku) { + $nextOptionId = $optionId ?? $nextOptionId; + $nextValueId = $valueId ?? $nextValueId; + $prevRowSku = $rowSku; } $optionId = $nextOptionId; $valueId = $nextValueId; @@ -1312,8 +1348,8 @@ protected function _importData() $products, $prices ); - if ($optionData != null) { - $options[] = $optionData; + if ($optionData) { + $options[$optionData['option_id']] = $optionData; } $this->_collectOptionTypeData( $combinedData, @@ -1327,15 +1363,6 @@ protected function _importData() ); $this->_collectOptionTitle($combinedData, $prevOptionId, $titles); - $this->checkOptionTitles( - $options, - $titles, - $combinedData, - $prevOptionId, - $optionId, - $products, - $prices - ); } } $this->removeExistingOptions($products, $optionsToRemove); @@ -1344,76 +1371,20 @@ protected function _importData() 'prices' => $typePrices, 'titles' => $typeTitles, ]; - $this->setLastOptionTitle($titles); //Save prepared custom options data. $this->savePreparedCustomOptions( $products, - $options, + array_values($options), $titles, $prices, $types ); + $this->optionNewIdExistingIdMap = $this->markNewIdsAsExisting($this->optionNewIdExistingIdMap); + $this->optionTypeNewIdExistingIdMap = $this->markNewIdsAsExisting($this->optionTypeNewIdExistingIdMap); } return true; } - /** - * Check options titles. - * - * If products were split up between bunches, - * this function will add needed option for option titles - * - * @param array $options - * @param array $titles - * @param array $combinedData - * @param int $prevOptionId - * @param int $optionId - * @param array $products - * @param array $prices - * @return void - */ - private function checkOptionTitles( - array &$options, - array &$titles, - array $combinedData, - int &$prevOptionId, - int &$optionId, - array $products, - array $prices - ) : void { - $titlesCount = count($titles); - if ($titlesCount > 0 && count($options) !== $titlesCount) { - $combinedData[Product::COL_STORE_VIEW_CODE] = ''; - $optionId--; - $option = $this->_collectOptionMainData( - $combinedData, - $prevOptionId, - $optionId, - $products, - $prices - ); - if ($option) { - $options[] = $option; - } - } - } - - /** - * Setting last Custom Option Title - * to use it later in _collectOptionTitle - * to set correct title for default store view - * - * @param array $titles - */ - private function setLastOptionTitle(array &$titles) : void - { - if (count($titles) > 0) { - end($titles); - $key = key($titles); - $this->lastOptionTitle[$key] = $titles[$key]; - } - } - /** * Remove existing options. * @@ -1475,9 +1446,7 @@ protected function _collectOptionMainData( $optionData = null; if ($this->_rowIsMain) { - $optionData = empty($rowData[Product::COL_STORE_VIEW_CODE]) - ? $this->_getOptionData($rowData, $this->_rowProductId, $nextOptionId, $this->_rowType) - : ''; + $optionData = $this->_getOptionData($rowData, $this->_rowProductId, $nextOptionId, $this->_rowType); if (!$this->_isRowHasSpecificType($this->_rowType) && ($priceData = $this->_getPriceData($rowData, $nextOptionId, $this->_rowType)) @@ -1525,38 +1494,21 @@ protected function _collectOptionTypeData( array &$childCount ) { if ($this->_isRowHasSpecificType($this->_rowType) && $prevOptionId) { - $specificTypeData = $this->_getSpecificTypeData($rowData, $nextValueId); - //For default store + $specificTypeData = $this->_getSpecificTypeData([self::COLUMN_STORE => null] + $rowData, $nextValueId); if ($specificTypeData) { - $typeValues[$prevOptionId][] = $specificTypeData['value']; + $typeValues[$prevOptionId][$nextValueId] = $specificTypeData['value']; + $typeTitles[$nextValueId][$this->_rowStoreId] = $specificTypeData['title']; - // ensure default title is set - if (!isset($typeTitles[$nextValueId][Store::DEFAULT_STORE_ID])) { - $typeTitles[$nextValueId][Store::DEFAULT_STORE_ID] = $specificTypeData['title']; - } - - if ($specificTypeData['price']) { + if (!empty($specificTypeData['price'])) { if ($this->_isPriceGlobal) { $typePrices[$nextValueId][Store::DEFAULT_STORE_ID] = $specificTypeData['price']; } else { - // ensure default price is set - if (!isset($typePrices[$nextValueId][Store::DEFAULT_STORE_ID])) { - $typePrices[$nextValueId][Store::DEFAULT_STORE_ID] = $specificTypeData['price']; - } $typePrices[$nextValueId][$this->_rowStoreId] = $specificTypeData['price']; } } $nextValueId++; } - $specificTypeData = $this->_getSpecificTypeData($rowData, 0, false); - //For others stores - if ($specificTypeData) { - if (isset($specificTypeData['price'])) { - $typePrices[$nextValueId][$this->_rowStoreId] = $specificTypeData['price']; - } - $typeTitles[$nextValueId++][$this->_rowStoreId] = $specificTypeData['title']; - } } } @@ -1570,16 +1522,7 @@ protected function _collectOptionTypeData( */ protected function _collectOptionTitle(array $rowData, $prevOptionId, array &$titles) { - $defaultStoreId = Store::DEFAULT_STORE_ID; if (!empty($rowData[self::COLUMN_TITLE])) { - if (!isset($titles[$prevOptionId][$defaultStoreId])) { - if (isset($this->lastOptionTitle[$prevOptionId])) { - $titles[$prevOptionId] = $this->lastOptionTitle[$prevOptionId]; - unset($this->lastOptionTitle); - } else { - $titles[$prevOptionId][$defaultStoreId] = $rowData[self::COLUMN_TITLE]; - } - } $titles[$prevOptionId][$this->_rowStoreId] = $rowData[self::COLUMN_TITLE]; } } @@ -1598,7 +1541,10 @@ protected function _compareOptionsWithExisting(array &$options, array &$titles, { foreach ($options as &$optionData) { $newOptionId = $optionData['option_id']; - if ($optionId = $this->_findExistingOptionId($optionData, $titles[$newOptionId])) { + $optionId = $this->optionNewIdExistingIdMap[$newOptionId] + ?? $this->_findExistingOptionId($optionData, $titles[$newOptionId]); + $this->optionNewIdExistingIdMap[$newOptionId] = $optionId ?: null; + if ($optionId && (int) $optionId !== (int) $newOptionId) { $optionData['option_id'] = $optionId; $titles[$optionId] = $titles[$newOptionId]; unset($titles[$newOptionId]); @@ -1606,6 +1552,8 @@ protected function _compareOptionsWithExisting(array &$options, array &$titles, foreach ($prices[$newOptionId] as $storeId => $priceStoreData) { $prices[$newOptionId][$storeId]['option_id'] = $optionId; } + $prices[$optionId] = $prices[$newOptionId]; + unset($prices[$newOptionId]); } if (isset($typeValues[$newOptionId])) { $typeValues[$optionId] = $typeValues[$newOptionId]; @@ -1633,8 +1581,10 @@ private function restoreOriginalOptionTypeIds(array &$typeValues, array &$typePr foreach ($optionTypes as &$optionType) { $optionTypeId = $optionType['option_type_id']; foreach ($typeTitles[$optionTypeId] as $storeId => $optionTypeTitle) { - $existingTypeId = $this->getExistingOptionTypeId($optionId, $storeId, $optionTypeTitle); - if ($existingTypeId) { + $existingTypeId = $this->optionTypeNewIdExistingIdMap[$optionTypeId] + ?? $this->getExistingOptionTypeId($optionId, $storeId, $optionTypeTitle); + $this->optionTypeNewIdExistingIdMap[$optionTypeId] = $existingTypeId ?: null; + if ($existingTypeId && (int) $existingTypeId !== (int) $optionTypeId) { $optionType['option_type_id'] = $existingTypeId; $typeTitles[$existingTypeId] = $typeTitles[$optionTypeId]; unset($typeTitles[$optionTypeId]); @@ -1817,7 +1767,7 @@ protected function _getPriceData(array $rowData, $optionId, $type) ) { $priceData = [ 'option_id' => $optionId, - 'store_id' => $this->_rowStoreId, + 'store_id' => $this->_isPriceGlobal ? Store::DEFAULT_STORE_ID : $this->_rowStoreId, 'price_type' => 'fixed', ]; @@ -1924,7 +1874,12 @@ protected function _saveOptions(array $options) protected function _saveTitles(array $titles) { $titleRows = []; + $existingOptionIds = array_flip(array_filter($this->optionNewIdExistingIdMap)); foreach ($titles as $optionId => $storeInfo) { + // Check that if it is a new option, then make sure a record for default store will be created + if (!isset($existingOptionIds[$optionId]) && count($storeInfo) > 0) { + $storeInfo = [Store::DEFAULT_STORE_ID => reset($storeInfo)] + $storeInfo; + } //for use default $uniqStoreInfo = array_unique($storeInfo); foreach ($uniqStoreInfo as $storeId => $title) { @@ -1952,7 +1907,12 @@ protected function _savePrices(array $prices) { if ($prices) { $optionPriceRows = []; - foreach ($prices as $storesData) { + $existingOptionIds = array_flip(array_filter($this->optionNewIdExistingIdMap)); + foreach ($prices as $optionId => $storesData) { + // Check that if it is a new option, then make sure a record for default store will be created + if (!isset($existingOptionIds[$optionId]) && count($storesData) > 0) { + $storesData = [Store::DEFAULT_STORE_ID => reset($storesData)] + $storesData; + } foreach ($storesData as $row) { $optionPriceRows[] = $row; } @@ -1977,8 +1937,6 @@ protected function _savePrices(array $prices) */ protected function _saveSpecificTypeValues(array $typeValues) { - $this->_deleteSpecificTypeValues(array_keys($typeValues)); - $typeValueRows = []; foreach ($typeValues as $optionId => $optionInfo) { foreach ($optionInfo as $row) { @@ -1987,7 +1945,7 @@ protected function _saveSpecificTypeValues(array $typeValues) } } if ($typeValueRows) { - $this->_connection->insertMultiple($this->_tables['catalog_product_option_type_value'], $typeValueRows); + $this->_connection->insertOnDuplicate($this->_tables['catalog_product_option_type_value'], $typeValueRows); } return $this; @@ -2002,7 +1960,12 @@ protected function _saveSpecificTypeValues(array $typeValues) protected function _saveSpecificTypePrices(array $typePrices) { $optionTypePriceRows = []; + $existingOptionTypeIds = array_flip(array_filter($this->optionTypeNewIdExistingIdMap)); foreach ($typePrices as $optionTypeId => $storesData) { + // Check that if it is a new option value, then make sure a record for default store will be created + if (!isset($existingOptionTypeIds[$optionTypeId]) && count($storesData) > 0) { + $storesData = [Store::DEFAULT_STORE_ID => reset($storesData)] + $storesData; + } foreach ($storesData as $storeId => $row) { $row['option_type_id'] = $optionTypeId; $row['store_id'] = $storeId; @@ -2029,7 +1992,12 @@ protected function _saveSpecificTypePrices(array $typePrices) protected function _saveSpecificTypeTitles(array $typeTitles) { $optionTypeTitleRows = []; + $existingOptionTypeIds = array_flip(array_filter($this->optionTypeNewIdExistingIdMap)); foreach ($typeTitles as $optionTypeId => $storesData) { + // Check that if it is a new option value, then make sure a record for default store will be created + if (!isset($existingOptionTypeIds[$optionTypeId]) && count($storesData) > 0) { + $storesData = [Store::DEFAULT_STORE_ID => reset($storesData)] + $storesData; + } //for use default $uniqStoresData = array_unique($storesData); foreach ($uniqStoresData as $storeId => $title) { @@ -2185,14 +2153,35 @@ private function savePreparedCustomOptions( $this->_compareOptionsWithExisting($options, $titles, $prices, $types['values']); $this->restoreOriginalOptionTypeIds($types['values'], $types['prices'], $types['titles']); } - - $this->_saveOptions($options) - ->_saveTitles($titles) - ->_savePrices($prices) - ->_saveSpecificTypeValues($types['values']) - ->_saveSpecificTypePrices($types['prices']) - ->_saveSpecificTypeTitles($types['titles']) - ->_updateProducts($products); + $this->transactionManager->start($this->_connection); + try { + $this->_saveOptions($options) + ->_saveTitles($titles) + ->_savePrices($prices) + ->_saveSpecificTypeValues($types['values']) + ->_saveSpecificTypePrices($types['prices']) + ->_saveSpecificTypeTitles($types['titles']) + ->_updateProducts($products); + $this->transactionManager->commit(); + } catch (\Throwable $exception) { + $this->transactionManager->rollBack(); + throw $exception; + } } } + + /** + * Mark new IDs as existing IDs + * + * @param array $idsMap + * @return array + */ + private function markNewIdsAsExisting(array $idsMap): array + { + $newIds = array_keys(array_filter($idsMap, 'is_null')); + return array_replace( + $idsMap, + array_combine($newIds, $newIds) + ); + } }