Skip to content

Commit 7ffe23a

Browse files
committed
Merge branch 'ACP2E-2840' of https://github.com/adobe-commerce-tier-4/magento2ce into PR-03-12-2024-anna
2 parents bd85db3 + d2ca765 commit 7ffe23a

File tree

4 files changed

+226
-5
lines changed

4 files changed

+226
-5
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: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ class Validator extends AbstractValidator implements RowValidatorInterface
5151
*/
5252
protected $invalidAttribute;
5353

54+
/**
55+
* @var UniqueAttributeValidator
56+
*/
57+
private $uniqueAttributeValidator;
58+
5459
/**
5560
* @var TimezoneInterface
5661
*/
@@ -60,16 +65,20 @@ class Validator extends AbstractValidator implements RowValidatorInterface
6065
* @param StringUtils $string
6166
* @param RowValidatorInterface[] $validators
6267
* @param TimezoneInterface|null $localeDate
68+
* @param UniqueAttributeValidator|null $uniqueAttributeValidator
6369
*/
6470
public function __construct(
6571
\Magento\Framework\Stdlib\StringUtils $string,
6672
$validators = [],
67-
?TimezoneInterface $localeDate = null
73+
?TimezoneInterface $localeDate = null,
74+
?UniqueAttributeValidator $uniqueAttributeValidator = null
6875
) {
6976
$this->string = $string;
7077
$this->validators = $validators;
7178
$this->localeDate = $localeDate ?: ObjectManager::getInstance()
7279
->get(TimezoneInterface::class);
80+
$this->uniqueAttributeValidator = $uniqueAttributeValidator
81+
?: ObjectManager::getInstance()->get(UniqueAttributeValidator::class);
7382
}
7483

7584
/**
@@ -242,7 +251,14 @@ public function isAttributeValid($attrCode, array $attrParams, array $rowData)
242251

243252
if ($valid && !empty($attrParams['is_unique'])) {
244253
if (isset($this->_uniqueAttributes[$attrCode][$rowData[$attrCode]])
245-
&& ($this->_uniqueAttributes[$attrCode][$rowData[$attrCode]] != $rowData[Product::COL_SKU])) {
254+
&& ($this->_uniqueAttributes[$attrCode][$rowData[$attrCode]] != $rowData[Product::COL_SKU])
255+
|| !$this->uniqueAttributeValidator->isValid(
256+
$this->context,
257+
(string) $attrCode,
258+
(string) $rowData[Product::COL_SKU],
259+
(string) $rowData[$attrCode]
260+
)
261+
) {
246262
$this->_addMessages([RowValidatorInterface::ERROR_DUPLICATE_UNIQUE_ATTRIBUTE]);
247263
return false;
248264
}
@@ -452,11 +468,23 @@ private function isCategoriesValid(string|array $value) : bool
452468
*/
453469
public function init($context)
454470
{
471+
$this->_uniqueAttributes = [];
472+
$this->uniqueAttributeValidator->clearCache();
455473
$this->context = $context;
456474
foreach ($this->validators as $validator) {
457475
$validator->init($context);
458476
}
459477

460478
return $this;
461479
}
480+
481+
/**
482+
* @inheritdoc
483+
*/
484+
public function _resetState(): void
485+
{
486+
$this->_uniqueAttributes = [];
487+
$this->uniqueAttributeValidator->clearCache();
488+
parent::_resetState();
489+
}
462490
}

app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/ValidatorTest.php

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
use Magento\CatalogImportExport\Model\Import\Product;
1111
use Magento\CatalogImportExport\Model\Import\Product\Type\Simple;
12+
use Magento\CatalogImportExport\Model\Import\Product\UniqueAttributeValidator;
1213
use Magento\CatalogImportExport\Model\Import\Product\Validator;
1314
use Magento\CatalogImportExport\Model\Import\Product\Validator\Media;
1415
use Magento\CatalogImportExport\Model\Import\Product\Validator\Website;
@@ -41,6 +42,11 @@ class ValidatorTest extends TestCase
4142
/** @var Validator\Website|MockObject */
4243
protected $validatorTwo;
4344

45+
/**
46+
* @var UniqueAttributeValidator|MockObject
47+
*/
48+
private $uniqueAttributeValidator;
49+
4450
protected function setUp(): void
4551
{
4652
$entityTypeModel = $this->createPartialMock(
@@ -63,6 +69,7 @@ protected function setUp(): void
6369
Website::class,
6470
['init', 'isValid', 'getMessages']
6571
);
72+
$this->uniqueAttributeValidator = $this->createMock(UniqueAttributeValidator::class);
6673

6774
$this->validators = [$this->validatorOne, $this->validatorTwo];
6875
$timezone = $this->createMock(\Magento\Framework\Stdlib\DateTime\TimezoneInterface::class);
@@ -77,7 +84,8 @@ function ($date = null) {
7784
$this->validator = new Validator(
7885
new StringUtils(),
7986
$this->validators,
80-
$timezone
87+
$timezone,
88+
$this->uniqueAttributeValidator
8189
);
8290
$this->validator->init($this->context);
8391
}
@@ -88,10 +96,18 @@ function ($date = null) {
8896
* @param array $rowData
8997
* @param bool $isValid
9098
* @param string $attrCode
99+
* @param bool $uniqueAttributeValidatorResult
91100
* @dataProvider attributeValidationProvider
92101
*/
93-
public function testAttributeValidation($behavior, $attrParams, $rowData, $isValid, $attrCode = 'attribute_code')
94-
{
102+
public function testAttributeValidation(
103+
string $behavior,
104+
array $attrParams,
105+
array $rowData,
106+
bool $isValid,
107+
string $attrCode = 'attribute_code',
108+
bool $uniqueAttributeValidatorResult = true
109+
) {
110+
$this->uniqueAttributeValidator->method('isValid')->willReturn($uniqueAttributeValidatorResult);
95111
$this->context->method('getMultipleValueSeparator')->willReturn(Product::PSEUDO_MULTI_LINE_SEPARATOR);
96112
$this->context->expects($this->any())->method('getBehavior')->willReturn($behavior);
97113
$result = $this->validator->isAttributeValid(
@@ -226,6 +242,14 @@ public function attributeValidationProvider()
226242
['product_type' => 'any', 'unique_attribute' => 'unique-value', Product::COL_SKU => 'sku-0'],
227243
true,
228244
'unique_attribute'
245+
],
246+
[
247+
Import::BEHAVIOR_APPEND,
248+
['is_required' => true, 'type' => 'varchar', 'is_unique' => true],
249+
['product_type' => 'any', 'unique_attribute' => 'unique-value', Product::COL_SKU => 'sku-0'],
250+
false,
251+
'unique_attribute',
252+
false
229253
]
230254
];
231255
}

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)