Skip to content

fix Product duplication of imported products "fails" due to url rewrites #32141

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: 2.4-develop
Choose a base branch
from
54 changes: 51 additions & 3 deletions app/code/Magento/Catalog/Model/Product/Copier.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,15 @@
use Magento\Catalog\Model\Product\Attribute\Source\Status;
use Magento\Catalog\Model\Product\Option\Repository as OptionRepository;
use Magento\Catalog\Model\ProductFactory;
use Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\App\ObjectManager;
use Magento\Framework\App\ResourceConnection;
use Magento\Framework\EntityManager\MetadataPool;
use Magento\Store\Model\Store;
use Magento\UrlRewrite\Model\Exception\UrlAlreadyExistsException;
use Magento\UrlRewrite\Model\ResourceModel\UrlRewriteCollectionFactory;
use Magento\UrlRewrite\Service\V1\Data\UrlRewrite;

/**
* Catalog product copier.
Expand All @@ -25,6 +30,8 @@
*/
class Copier
{
private const ENTITY_TYPE = 'product';

/**
* @var Option\Repository
*/
Expand All @@ -50,25 +57,42 @@ class Copier
*/
private $scopeOverriddenValue;

/**
* @var ScopeConfigInterface
*/
private $scopeConfig;

/**
* @var ResourceConnection
*/
private $urlRewriteCollectionFactory;

/**
* @param CopyConstructorInterface $copyConstructor
* @param ProductFactory $productFactory
* @param ScopeOverriddenValue $scopeOverriddenValue
* @param OptionRepository|null $optionRepository
* @param MetadataPool|null $metadataPool
* @param ScopeConfigInterface|null $scopeConfig
* @param UrlRewriteCollectionFactory|null $urlRewriteCollectionFactory
*/
public function __construct(
CopyConstructorInterface $copyConstructor,
ProductFactory $productFactory,
ScopeOverriddenValue $scopeOverriddenValue,
OptionRepository $optionRepository,
MetadataPool $metadataPool
MetadataPool $metadataPool,
?ScopeConfigInterface $scopeConfig = null,
?UrlRewriteCollectionFactory $urlRewriteCollectionFactory = null
) {
$this->productFactory = $productFactory;
$this->copyConstructor = $copyConstructor;
$this->scopeOverriddenValue = $scopeOverriddenValue;
$this->optionRepository = $optionRepository;
$this->metadataPool = $metadataPool;
$this->scopeConfig = $scopeConfig ?: ObjectManager::getInstance()->get(ScopeConfigInterface::class);
$this->urlRewriteCollectionFactory = $urlRewriteCollectionFactory
?: ObjectManager::getInstance()->get(UrlRewriteCollectionFactory::class);
}

/**
Expand Down Expand Up @@ -119,13 +143,12 @@ private function setDefaultUrl(Product $product, Product $duplicate) : void
{
$duplicate->setStoreId(Store::DEFAULT_STORE_ID);
$resource = $product->getResource();
$attribute = $resource->getAttribute('url_key');
$productId = $product->getId();
$urlKey = $resource->getAttributeRawValue($productId, 'url_key', Store::DEFAULT_STORE_ID);
do {
$urlKey = $this->modifyUrl($urlKey);
$duplicate->setUrlKey($urlKey);
} while (!$attribute->getEntity()->checkAttributeUniqueValue($attribute, $duplicate));
} while ($this->isUrlRewriteExists($urlKey));
$duplicate->setData('url_path', null);
$duplicate->save();
}
Expand Down Expand Up @@ -206,4 +229,29 @@ private function removeStockItem(array $productData): array
}
return $productData;
}

/**
* Verify if generated url rewrite exists in url_rewrite table
*
* @param string $urlKey
* @return bool
*/
private function isUrlRewriteExists(string $urlKey): bool
{
$urlRewriteCollection = $this->urlRewriteCollectionFactory->create();
$urlRewriteCollection->addFieldToFilter(UrlRewrite::ENTITY_TYPE, self::ENTITY_TYPE)
->addFieldToFilter(UrlRewrite::REQUEST_PATH, $urlKey . $this->getUrlSuffix());

return $urlRewriteCollection->getSize() !== 0;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not have a dependency on other module implementations, ideally, we should either depend on an API (interface) or handle this with a Plugin, in this case probably on the module CatalogUrlRewrite.


/**
* Returns default product url suffix config
*
* @return string|null
*/
private function getUrlSuffix(): ?string
{
return $this->scopeConfig->getValue(ProductUrlPathGenerator::XML_PATH_PRODUCT_URL_SUFFIX);
}
}
53 changes: 30 additions & 23 deletions app/code/Magento/Catalog/Test/Unit/Model/Product/CopierTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@
use Magento\CatalogInventory\Api\Data\StockItemInterface;
use Magento\Eav\Model\Entity\AbstractEntity;
use Magento\Eav\Model\Entity\Attribute\AbstractAttribute;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\EntityManager\EntityMetadata;
use Magento\Framework\EntityManager\MetadataPool;
use Magento\UrlRewrite\Model\Exception\UrlAlreadyExistsException;
use Magento\UrlRewrite\Model\ResourceModel\UrlRewriteCollectionFactory;
use Magento\UrlRewrite\Model\ResourceModel\UrlRewriteCollection;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;

Expand Down Expand Up @@ -68,6 +71,16 @@ class CopierTest extends TestCase
*/
private $metadata;

/**
* @var ScopeConfigInterface|MockObject
*/
private $scopeConfigMock;

/**
* @var UrlRewriteCollection|MockObject
*/
private $urlRewriteCollectionMock;

/**
* @ingeritdoc
*/
Expand All @@ -78,6 +91,14 @@ protected function setUp(): void
$this->scopeOverriddenValueMock = $this->createMock(ScopeOverriddenValue::class);
$this->optionRepositoryMock = $this->createMock(Repository::class);
$this->productMock = $this->createMock(Product::class);
$this->scopeConfigMock = $this->createMock(ScopeConfigInterface::class);

$this->urlRewriteCollectionMock = $this->createMock(UrlRewriteCollection::class);
$this->urlRewriteCollectionMock->method('addFieldToFilter')
->willReturnSelf();
$urlRewriteCollectionFactoryMock = $this->createMock(UrlRewriteCollectionFactory::class);
$urlRewriteCollectionFactoryMock->method('create')
->willReturn($this->urlRewriteCollectionMock);

$this->metadata = $this->getMockBuilder(EntityMetadata::class)
->disableOriginalConstructor()
Expand All @@ -95,7 +116,9 @@ protected function setUp(): void
$this->productFactoryMock,
$this->scopeOverriddenValueMock,
$this->optionRepositoryMock,
$metadataPool
$metadataPool,
$this->scopeConfigMock,
$urlRewriteCollectionFactoryMock
);
}

Expand Down Expand Up @@ -144,22 +167,9 @@ public function testCopy(): void
true,
['checkAttributeUniqueValue']
);
$entityMock->expects($this->once())
->method('checkAttributeUniqueValue')
->willReturn(true);

$attributeMock = $this->getMockForAbstractClass(
AbstractAttribute::class,
[],
'',
false,
true,
true,
['getEntity']
);
$attributeMock->expects($this->once())
->method('getEntity')
->willReturn($entityMock);
$this->urlRewriteCollectionMock->expects($this->once())
->method('getSize')
->willReturn(0);

$resourceMock = $this->getMockBuilder(ProductResourceModel::class)
->disableOriginalConstructor()
Expand All @@ -168,9 +178,6 @@ public function testCopy(): void
$resourceMock->expects($this->once())
->method('getAttributeRawValue')
->willReturn('urk-key-1');
$resourceMock->expects($this->exactly(2))
->method('getAttribute')
->willReturn($attributeMock);

$this->productMock->expects($this->exactly(2))
->method('getResource')
Expand Down Expand Up @@ -301,9 +308,9 @@ public function testUrlAlreadyExistsExceptionWhileCopyStoresUrl(): void
true,
['checkAttributeUniqueValue']
);
$entityMock->expects($this->exactly(11))
->method('checkAttributeUniqueValue')
->willReturn(true, false);
$this->urlRewriteCollectionMock->expects($this->once())
->method('getSize')
->willReturn(0);

$attributeMock = $this->getMockForAbstractClass(
AbstractAttribute::class,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,41 +7,87 @@

namespace Magento\Catalog\Model\Product;

use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Catalog\Model\ProductRepository;
use Magento\Framework\ObjectManagerInterface;
use Magento\Store\Model\Store;
use Magento\TestFramework\Helper\Bootstrap;
use PHPUnit\Framework\TestCase;

/**
* Tests product copier.
* @magentoAppArea adminhtml
*/
class CopierTest extends TestCase
{
/**
* @var Copier
*/
private $copier;

/**
* @var ProductRepositoryInterface
*/
private $productRepository;

/**
* @inheritdoc
*/
protected function setUp(): void
{
$objectManager = Bootstrap::getObjectManager();
$this->copier = $objectManager->get(Copier::class);
$this->productRepository = $objectManager->get(ProductRepositoryInterface::class);
}

/**
* Tests copying of product.
*
* Case when url_key is set for store view and has equal value to default store.
*
* @magentoDataFixture Magento/Catalog/_files/product_simple_multistore_with_url_key.php
* @magentoAppArea adminhtml
*
* @return void
*/
public function testProductCopyWithExistingUrlKey()
public function testProductCopyWithExistingUrlKey(): void
{
$productSKU = 'simple_100';
/** @var ProductRepository $productRepository */
$productRepository = Bootstrap::getObjectManager()->get(ProductRepository::class);
$copier = Bootstrap::getObjectManager()->get(Copier::class);
$product = $this->productRepository->get($productSKU);
$duplicate = $this->copier->copy($product);

$product = $productRepository->get($productSKU);
$duplicate = $copier->copy($product);

$duplicateStoreView = $productRepository->getById($duplicate->getId(), false, Store::DISTRO_STORE_ID);
$productStoreView = $productRepository->get($productSKU, false, Store::DISTRO_STORE_ID);
$duplicateStoreView = $this->productRepository->getById($duplicate->getId(), false, Store::DISTRO_STORE_ID);
$productStoreView = $this->productRepository->get($productSKU, false, Store::DISTRO_STORE_ID);

$this->assertNotEquals(
$duplicateStoreView->getUrlKey(),
$productStoreView->getUrlKey(),
'url_key of product duplicate should be different then url_key of the product for the same store view'
);
}

/**
* Tests copying of product when url_key exists.
*
* @magentoDataFixture Magento/CatalogUrlRewrite/_files/product_simple.php
* @magentoDbIsolation disabled
*
* @return void
*/
public function testProductCopyWithExistingUrlKeyPermanentType(): void
{
$product = $this->productRepository->get('simple');
$duplicate = $this->copier->copy($product);

$data = [
'url_key' => 'new-url-key',
'url_key_create_redirect' => $duplicate->getUrlKey(),
'save_rewrites_history' => true,
];
$duplicate->addData($data);
$this->productRepository->save($duplicate);

$secondDuplicate = $this->copier->copy($product);

$this->assertNotNull($secondDuplicate->getId());
}
}