Skip to content

Commit 5a85cff

Browse files
committed
ACP2E-2840: It's possible to set non-unique values via product import
1 parent 1cfd2cc commit 5a85cff

File tree

3 files changed

+200
-2
lines changed

3 files changed

+200
-2
lines changed
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<?php
2+
/************************************************************************
3+
*
4+
* Copyright 2024 Adobe
5+
* All Rights Reserved.
6+
*
7+
* NOTICE: All information contained herein is, and remains
8+
* the property of Adobe and its suppliers, if any. The intellectual
9+
* and technical concepts contained herein are proprietary to Adobe
10+
* and its suppliers and are protected by all applicable intellectual
11+
* property laws, including trade secret and copyright laws.
12+
* Dissemination of this information or reproduction of this material
13+
* is strictly forbidden unless prior written permission is obtained
14+
* from Adobe.
15+
* ************************************************************************
16+
*/
17+
declare(strict_types=1);
18+
19+
namespace Magento\CatalogImportExport\Model\Import\Product;
20+
21+
use Magento\Catalog\Api\Data\ProductInterface;
22+
use Magento\CatalogImportExport\Model\Import\Product;
23+
use Magento\Eav\Model\Entity\Attribute\AbstractAttribute;
24+
use Magento\Framework\EntityManager\MetadataPool;
25+
use Magento\Framework\Exception\LocalizedException;
26+
27+
class UniqueAttributeValidator
28+
{
29+
/**
30+
* @var array
31+
*/
32+
private array $cache = [];
33+
34+
/**
35+
* @param MetadataPool $metadataPool
36+
* @param SkuStorage $skuStorage
37+
*/
38+
public function __construct(
39+
private readonly MetadataPool $metadataPool,
40+
private readonly SkuStorage $skuStorage
41+
) {
42+
}
43+
44+
/**
45+
* Check if provided value is unique for the attribute
46+
*
47+
* @param Product $context
48+
* @param string $attributeCode
49+
* @param string $sku
50+
* @param string $value
51+
* @return bool
52+
* @throws \Exception
53+
*/
54+
public function isValid(Product $context, string $attributeCode, string $sku, string $value): bool
55+
{
56+
$cacheKey = strtolower($attributeCode);
57+
if (!isset($this->cache[$cacheKey])) {
58+
$this->cache[$cacheKey] = $this->load($context, $attributeCode);
59+
}
60+
$entityData = $this->skuStorage->get($sku);
61+
$id = null;
62+
if ($entityData !== null) {
63+
$id = $entityData[$this->metadataPool->getMetadata(ProductInterface::class)->getLinkField()];
64+
}
65+
return !isset($this->cache[$cacheKey][$value]) || in_array($id, $this->cache[$cacheKey][$value]);
66+
}
67+
68+
/**
69+
* Load attribute values with corresponding entity ids
70+
*
71+
* @param Product $context
72+
* @param string $attributeCode
73+
* @return array
74+
* @throws LocalizedException
75+
*/
76+
private function load(Product $context, string $attributeCode): array
77+
{
78+
/** @var AbstractAttribute $attributeObject */
79+
$attributeObject = $context->retrieveAttributeByCode($attributeCode);
80+
if ($attributeObject->isStatic()) {
81+
return [];
82+
}
83+
$metadata = $this->metadataPool->getMetadata(ProductInterface::class);
84+
$connection = $context->getConnection();
85+
$idField = $metadata->getLinkField();
86+
$select = $connection->select()
87+
->from(
88+
$attributeObject->getBackend()->getTable(),
89+
['value', $idField]
90+
)
91+
->where(
92+
'attribute_id = :attribute_id'
93+
);
94+
$result = [];
95+
foreach ($connection->fetchAll($select, ['attribute_id' => $attributeObject->getId()]) as $row) {
96+
$result[$row['value']][] = $row[$idField];
97+
}
98+
return $result;
99+
}
100+
101+
/**
102+
* Clear cached attribute values
103+
*
104+
* @return void
105+
*/
106+
public function clearCache(): void
107+
{
108+
$this->cache = [];
109+
}
110+
}

app/code/Magento/CatalogImportExport/Model/Import/Product/Validator.php

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
namespace Magento\CatalogImportExport\Model\Import\Product;
77

88
use Magento\CatalogImportExport\Model\Import\Product;
9+
use Magento\Framework\App\ObjectManager;
910
use Magento\Framework\Validator\AbstractValidator;
1011
use Magento\Catalog\Model\Product\Attribute\Backend\Sku;
1112

@@ -48,16 +49,25 @@ class Validator extends AbstractValidator implements RowValidatorInterface
4849
*/
4950
protected $invalidAttribute;
5051

52+
/**
53+
* @var UniqueAttributeValidator
54+
*/
55+
private $uniqueAttributeValidator;
56+
5157
/**
5258
* @param \Magento\Framework\Stdlib\StringUtils $string
5359
* @param RowValidatorInterface[] $validators
60+
* @param UniqueAttributeValidator|null $uniqueAttributeValidator
5461
*/
5562
public function __construct(
5663
\Magento\Framework\Stdlib\StringUtils $string,
57-
$validators = []
64+
$validators = [],
65+
UniqueAttributeValidator $uniqueAttributeValidator = null
5866
) {
5967
$this->string = $string;
6068
$this->validators = $validators;
69+
$this->uniqueAttributeValidator = $uniqueAttributeValidator
70+
?? ObjectManager::getInstance()->get(UniqueAttributeValidator::class);
6171
}
6272

6373
/**
@@ -230,7 +240,14 @@ public function isAttributeValid($attrCode, array $attrParams, array $rowData)
230240

231241
if ($valid && !empty($attrParams['is_unique'])) {
232242
if (isset($this->_uniqueAttributes[$attrCode][$rowData[$attrCode]])
233-
&& ($this->_uniqueAttributes[$attrCode][$rowData[$attrCode]] != $rowData[Product::COL_SKU])) {
243+
&& ($this->_uniqueAttributes[$attrCode][$rowData[$attrCode]] != $rowData[Product::COL_SKU])
244+
|| !$this->uniqueAttributeValidator->isValid(
245+
$this->context,
246+
(string) $attrCode,
247+
(string) $rowData[Product::COL_SKU],
248+
(string) $rowData[$attrCode]
249+
)
250+
) {
234251
$this->_addMessages([RowValidatorInterface::ERROR_DUPLICATE_UNIQUE_ATTRIBUTE]);
235252
return false;
236253
}
@@ -431,11 +448,23 @@ private function isCategoriesValid(string|array $value) : bool
431448
*/
432449
public function init($context)
433450
{
451+
$this->_uniqueAttributes = [];
452+
$this->uniqueAttributeValidator->clearCache();
434453
$this->context = $context;
435454
foreach ($this->validators as $validator) {
436455
$validator->init($context);
437456
}
438457

439458
return $this;
440459
}
460+
461+
/**
462+
* @inheritdoc
463+
*/
464+
public function _resetState(): void
465+
{
466+
$this->_uniqueAttributes = [];
467+
$this->uniqueAttributeValidator->clearCache();
468+
parent::_resetState();
469+
}
441470
}

dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest/ProductValidationTest.php

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,19 @@
99

1010
use Magento\Catalog\Model\Product;
1111
use Magento\Catalog\Model\ResourceModel\Product as ProductResource;
12+
use Magento\Catalog\Test\Fixture\Attribute as AttributeFixture;
13+
use Magento\Catalog\Test\Fixture\Product as ProductFixture;
1214
use Magento\CatalogImportExport\Model\Import\Product as ImportProduct;
15+
use Magento\CatalogImportExport\Model\Import\Product\RowValidatorInterface;
1316
use Magento\CatalogImportExport\Model\Import\ProductTestBase;
1417
use Magento\Framework\App\Filesystem\DirectoryList;
1518
use Magento\Framework\Filesystem;
1619
use Magento\ImportExport\Model\Import;
1720
use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface;
1821
use Magento\ImportExport\Model\Import\Source\Csv;
22+
use Magento\ImportExport\Test\Fixture\CsvFile as CsvFileFixture;
23+
use Magento\TestFramework\Fixture\DataFixture;
24+
use Magento\TestFramework\Fixture\DataFixtureStorageManager;
1925
use Magento\TestFramework\Helper\Bootstrap as BootstrapHelper;
2026

2127
/**
@@ -401,4 +407,57 @@ public function testValidateMultiselectValuesWithCustomSeparator(): void
401407

402408
$this->assertEmpty($errors->getAllErrors());
403409
}
410+
411+
#[
412+
DataFixture(AttributeFixture::class, ['is_unique' => 1, 'attribute_code' => 'uniq_test_attr']),
413+
DataFixture(ProductFixture::class, ['uniq_test_attr' => 'uniq_test_attr_val'], as: 'p1'),
414+
DataFixture(ProductFixture::class, as: 'p2'),
415+
DataFixture(
416+
CsvFileFixture::class,
417+
[
418+
'rows' => [
419+
['sku', 'product_type', 'additional_attributes'],
420+
['$p2.sku$', 'simple', 'uniq_test_attr=uniq_test_attr_val'],
421+
]
422+
],
423+
'file'
424+
)
425+
]
426+
public function testUniqueValidationShouldFailIfValueExistForAnotherProduct(): void
427+
{
428+
$fixtures = DataFixtureStorageManager::getStorage();
429+
$pathToFile = $fixtures->get('file')->getAbsolutePath();
430+
$importModel = $this->createImportModel($pathToFile);
431+
$errors = $importModel->validateData();
432+
$this->assertErrorsCount(1, $errors);
433+
$this->assertEquals(
434+
RowValidatorInterface::ERROR_DUPLICATE_UNIQUE_ATTRIBUTE,
435+
$errors->getErrorByRowNumber(0)[0]->getErrorCode()
436+
);
437+
}
438+
439+
#[
440+
DataFixture(AttributeFixture::class, ['is_unique' => 1, 'attribute_code' => 'uniq_test_attr']),
441+
DataFixture(ProductFixture::class, ['uniq_test_attr' => 'uniq_test_attr_val'], as: 'p1'),
442+
DataFixture(ProductFixture::class, ['uniq_test_attr' => 'uniq_test_attr_val2'], as: 'p2'),
443+
DataFixture(
444+
CsvFileFixture::class,
445+
[
446+
'rows' => [
447+
['sku', 'product_type', 'additional_attributes'],
448+
['$p1.sku$', 'simple', 'uniq_test_attr=uniq_test_attr_val'],
449+
]
450+
],
451+
'file'
452+
)
453+
]
454+
public function testUniqueValidationShouldNotFailIfValueExistForTheImportedProductOnly(): void
455+
{
456+
$fixtures = DataFixtureStorageManager::getStorage();
457+
$pathToFile = $fixtures->get('file')->getAbsolutePath();
458+
$importModel = $this->createImportModel($pathToFile);
459+
$errors = $importModel->validateData();
460+
$this->assertErrorsCount(0, $errors);
461+
$importModel->importData();
462+
}
404463
}

0 commit comments

Comments
 (0)