Skip to content

Commit 9b8af87

Browse files
committed
MAGETWO-90149: Product Import does not allow store-specific Custom Option labels
1 parent c46a17c commit 9b8af87

File tree

15 files changed

+648
-149
lines changed

15 files changed

+648
-149
lines changed

app/code/Magento/BundleImportExport/Model/Export/RowCustomizer.php

Lines changed: 128 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@
99
use Magento\CatalogImportExport\Model\Export\RowCustomizerInterface;
1010
use Magento\CatalogImportExport\Model\Import\Product as ImportProductModel;
1111
use Magento\Bundle\Model\ResourceModel\Selection\Collection as SelectionCollection;
12-
use Magento\ImportExport\Controller\Adminhtml\Import;
1312
use Magento\ImportExport\Model\Import as ImportModel;
1413
use \Magento\Catalog\Model\Product\Type\AbstractType;
14+
use \Magento\Framework\App\ObjectManager;
15+
use \Magento\Store\Model\StoreManagerInterface;
1516

1617
/**
1718
* Class RowCustomizer
@@ -105,6 +106,35 @@ class RowCustomizer implements RowCustomizerInterface
105106
AbstractType::SHIPMENT_SEPARATELY => 'separately',
106107
];
107108

109+
/**
110+
* @var \Magento\Bundle\Model\ResourceModel\Option\Collection[]
111+
*/
112+
private $optionCollections = [];
113+
114+
/**
115+
* @var array
116+
*/
117+
private $storeIdToCode = [];
118+
119+
/**
120+
* @var string
121+
*/
122+
private $optionCollectionCacheKey = '_cache_instance_options_collection';
123+
124+
/**
125+
* @var StoreManagerInterface
126+
*/
127+
private $storeManager;
128+
129+
/**
130+
* @param StoreManagerInterface $storeManager
131+
* @throws \RuntimeException
132+
*/
133+
public function __construct(StoreManagerInterface $storeManager)
134+
{
135+
$this->storeManager = $storeManager;
136+
}
137+
108138
/**
109139
* Retrieve list of bundle specific columns
110140
* @return array
@@ -207,15 +237,13 @@ protected function populateBundleData($collection)
207237
*/
208238
protected function getFormattedBundleOptionValues($product)
209239
{
210-
/** @var \Magento\Bundle\Model\ResourceModel\Option\Collection $optionsCollection */
211-
$optionsCollection = $product->getTypeInstance()
212-
->getOptionsCollection($product)
213-
->setOrder('position', Collection::SORT_ORDER_ASC);
214-
240+
$optionCollections = $this->getProductOptionCollections($product);
215241
$bundleData = '';
216-
foreach ($optionsCollection as $option) {
242+
$optionTitles = $this->getBundleOptionTitles($product);
243+
foreach ($optionCollections->getItems() as $option) {
244+
$optionValues = $this->getFormattedOptionValues($option, $optionTitles);
217245
$bundleData .= $this->getFormattedBundleSelections(
218-
$this->getFormattedOptionValues($option),
246+
$optionValues,
219247
$product->getTypeInstance()
220248
->getSelectionsCollection([$option->getId()], $product)
221249
->setOrder('position', Collection::SORT_ORDER_ASC)
@@ -267,16 +295,23 @@ function ($value, $key) {
267295
* Retrieve option value of bundle product
268296
*
269297
* @param \Magento\Bundle\Model\Option $option
298+
* @param string[] $optionTitles
270299
* @return string
271300
*/
272-
protected function getFormattedOptionValues($option)
301+
protected function getFormattedOptionValues($option, $optionTitles = [])
273302
{
274-
return 'name' . ImportProductModel::PAIR_NAME_VALUE_SEPARATOR
275-
. $option->getTitle() . ImportModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR
276-
. 'type' . ImportProductModel::PAIR_NAME_VALUE_SEPARATOR
277-
. $option->getType() . ImportModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR
278-
. 'required' . ImportProductModel::PAIR_NAME_VALUE_SEPARATOR
279-
. $option->getRequired();
303+
$names = implode(ImportModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR, array_map(
304+
function ($title, $storeName) {
305+
return $storeName . ImportProductModel::PAIR_NAME_VALUE_SEPARATOR . $title;
306+
},
307+
$optionTitles[$option->getOptionId()],
308+
array_keys($optionTitles[$option->getOptionId()])
309+
));
310+
return $names . ImportModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR
311+
. 'type' . ImportProductModel::PAIR_NAME_VALUE_SEPARATOR
312+
. $option->getType() . ImportModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR
313+
. 'required' . ImportProductModel::PAIR_NAME_VALUE_SEPARATOR
314+
. $option->getRequired();
280315
}
281316

282317
/**
@@ -381,4 +416,82 @@ private function parseAdditionalAttributes($additionalAttributes)
381416
}
382417
return $preparedAttributes;
383418
}
419+
420+
/**
421+
* Get product options titles.
422+
*
423+
* Values for all store views (default) should be specified with 'name' key.
424+
* If user want to specify value or change existing for non default store views it should be specified with
425+
* 'name_' prefix and needed store view suffix.
426+
*
427+
* For example:
428+
* - 'name=All store views name' for all store views
429+
* - 'name_specific_store=Specific store name' for store view with 'specific_store' store code
430+
*
431+
* @param \Magento\Catalog\Model\Product $product
432+
* @return array
433+
*/
434+
private function getBundleOptionTitles(\Magento\Catalog\Model\Product $product): array
435+
{
436+
$optionCollections = $this->getProductOptionCollections($product);
437+
$optionsTitles = [];
438+
/** @var \Magento\Bundle\Model\Option $option */
439+
foreach ($optionCollections->getItems() as $option) {
440+
$optionsTitles[$option->getId()]['name'] = $option->getTitle();
441+
}
442+
$storeIds = $product->getStoreIds();
443+
if (count($storeIds) > 1) {
444+
foreach ($storeIds as $storeId) {
445+
$optionCollections = $this->getProductOptionCollections($product, $storeId);
446+
/** @var \Magento\Bundle\Model\Option $option */
447+
foreach ($optionCollections->getItems() as $option) {
448+
$optionTitle = $option->getTitle();
449+
if ($optionsTitles[$option->getId()]['name'] != $optionTitle) {
450+
$optionsTitles[$option->getId()]['name_' . $this->getStoreCodeById($storeId)] = $optionTitle;
451+
}
452+
}
453+
}
454+
}
455+
return $optionsTitles;
456+
}
457+
458+
/**
459+
* Get product options collection by provided product model.
460+
*
461+
* Set given store id to the product if it was defined (default store id will be set if was not).
462+
*
463+
* @param \Magento\Catalog\Model\Product $product $product
464+
* @param int $storeId
465+
* @return \Magento\Bundle\Model\ResourceModel\Option\Collection
466+
*/
467+
private function getProductOptionCollections(
468+
\Magento\Catalog\Model\Product $product,
469+
$storeId = \Magento\Store\Model\Store::DEFAULT_STORE_ID
470+
): \Magento\Bundle\Model\ResourceModel\Option\Collection {
471+
$productSku = $product->getSku();
472+
if (!isset($this->optionCollections[$productSku][$storeId])) {
473+
$product->unsetData($this->optionCollectionCacheKey);
474+
$product->setStoreId($storeId);
475+
$this->optionCollections[$productSku][$storeId] = $product->getTypeInstance()
476+
->getOptionsCollection($product)
477+
->setOrder('position', Collection::SORT_ORDER_ASC);
478+
}
479+
return $this->optionCollections[$productSku][$storeId];
480+
}
481+
482+
/**
483+
* Retrieve store code by it's ID.
484+
*
485+
* Collect store id in $storeIdToCode[] private variable if it was not initialized earlier.
486+
*
487+
* @param int $storeId
488+
* @return string
489+
*/
490+
private function getStoreCodeById($storeId): string
491+
{
492+
if (!isset($this->storeIdToCode[$storeId])) {
493+
$this->storeIdToCode[$storeId] = $this->storeManager->getStore($storeId)->getCode();
494+
}
495+
return $this->storeIdToCode[$storeId];
496+
}
384497
}

app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle.php

Lines changed: 59 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
use \Magento\Framework\App\ObjectManager;
1212
use \Magento\Bundle\Model\Product\Price as BundlePrice;
1313
use \Magento\Catalog\Model\Product\Type\AbstractType;
14-
use Magento\CatalogImportExport\Model\Import\Product;
14+
use \Magento\CatalogImportExport\Model\Import\Product;
15+
use \Magento\Store\Model\StoreManagerInterface;
1516

1617
/**
1718
* Class Bundle
@@ -136,26 +137,40 @@ class Bundle extends \Magento\CatalogImportExport\Model\Import\Product\Type\Abst
136137
*/
137138
private $relationsDataSaver;
138139

140+
/**
141+
* @var StoreManagerInterface
142+
*/
143+
private $storeManager;
144+
145+
/**
146+
* @var array
147+
*/
148+
private $storeCodeToId = [];
149+
139150
/**
140151
* @param \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory $attrSetColFac
141152
* @param \Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory $prodAttrColFac
142153
* @param \Magento\Framework\App\ResourceConnection $resource
143154
* @param array $params
144155
* @param \Magento\Framework\EntityManager\MetadataPool|null $metadataPool
145156
* @param Bundle\RelationsDataSaver|null $relationsDataSaver
157+
* @param StoreManagerInterface $storeManager
146158
*/
147159
public function __construct(
148160
\Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory $attrSetColFac,
149161
\Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory $prodAttrColFac,
150162
\Magento\Framework\App\ResourceConnection $resource,
151163
array $params,
152164
\Magento\Framework\EntityManager\MetadataPool $metadataPool = null,
153-
Bundle\RelationsDataSaver $relationsDataSaver = null
165+
Bundle\RelationsDataSaver $relationsDataSaver = null,
166+
StoreManagerInterface $storeManager = null
154167
) {
155168
parent::__construct($attrSetColFac, $prodAttrColFac, $resource, $params, $metadataPool);
156169

157170
$this->relationsDataSaver = $relationsDataSaver
158171
?: ObjectManager::getInstance()->get(Bundle\RelationsDataSaver::class);
172+
$this->storeManager = $storeManager
173+
?: ObjectManager::getInstance()->get(StoreManagerInterface::class);
159174
}
160175

161176
/**
@@ -262,20 +277,28 @@ protected function populateOptionTemplate($option, $entityId, $index = null)
262277
* @param array $option
263278
* @param int $optionId
264279
* @param int $storeId
265-
*
266-
* @return array|bool
280+
* @return array
267281
*/
268282
protected function populateOptionValueTemplate($option, $optionId, $storeId = 0)
269283
{
270-
if (!isset($option['name']) || !isset($option['parent_id']) || !$optionId) {
271-
return false;
284+
$optionValues = [];
285+
if (isset($option['name']) && isset($option['parent_id']) && $optionId) {
286+
$pattern = '/^name[_]?(.*)/';
287+
$keys = array_keys($option);
288+
$optionNames = preg_grep($pattern, $keys);
289+
foreach ($optionNames as $optionName) {
290+
preg_match($pattern, $optionName, $storeCodes);
291+
$storeCode = array_pop($storeCodes);
292+
$storeId = $storeCode ? $this->getStoreIdByCode($storeCode) : $storeId;
293+
$optionValues[] = [
294+
'option_id' => $optionId,
295+
'parent_product_id' => $option['parent_id'],
296+
'store_id' => $storeId,
297+
'title' => $option[$optionName],
298+
];
299+
}
272300
}
273-
return [
274-
'option_id' => $optionId,
275-
'parent_product_id' => $option['parent_id'],
276-
'store_id' => $storeId,
277-
'title' => $option['name'],
278-
];
301+
return $optionValues;
279302
}
280303

281304
/**
@@ -285,7 +308,7 @@ protected function populateOptionValueTemplate($option, $optionId, $storeId = 0)
285308
* @param int $optionId
286309
* @param int $parentId
287310
* @param int $index
288-
* @return array
311+
* @return array|bool
289312
* @SuppressWarnings(PHPMD.NPathComplexity)
290313
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
291314
*/
@@ -576,21 +599,24 @@ protected function insertOptions()
576599
*/
577600
protected function populateInsertOptionValues($optionIds)
578601
{
579-
$insertValues = [];
602+
$optionValues = [];
580603
foreach ($this->_cachedOptions as $entityId => $options) {
581604
foreach ($options as $key => $option) {
582605
foreach ($optionIds as $optionId => $assoc) {
583606
if ($assoc['position'] == $this->_cachedOptions[$entityId][$key]['index']
584607
&& $assoc['parent_id'] == $entityId) {
585608
$option['parent_id'] = $entityId;
586-
$insertValues[] = $this->populateOptionValueTemplate($option, $optionId);
609+
$optionValues = array_merge(
610+
$optionValues,
611+
$this->populateOptionValueTemplate($option, $optionId)
612+
);
587613
$this->_cachedOptions[$entityId][$key]['option_id'] = $optionId;
588614
break;
589615
}
590616
}
591617
}
592618
}
593-
return $insertValues;
619+
return $optionValues;
594620
}
595621

596622
/**
@@ -711,4 +737,21 @@ protected function clear()
711737
$this->_cachedSkuToProducts = [];
712738
return $this;
713739
}
740+
741+
/**
742+
* Get store id by store code.
743+
*
744+
* @param string $storeCode
745+
* @return int
746+
*/
747+
private function getStoreIdByCode(string $storeCode): int
748+
{
749+
if (!isset($this->storeIdToCode[$storeCode])) {
750+
/** @var $store \Magento\Store\Model\Store */
751+
foreach ($this->storeManager->getStores() as $store) {
752+
$this->storeCodeToId[$store->getCode()] = $store->getId();
753+
}
754+
}
755+
return $this->storeCodeToId[$storeCode];
756+
}
714757
}

0 commit comments

Comments
 (0)