Skip to content
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

#21737 Duplicating product with translated url keys over multiple sto… #22178

100 changes: 82 additions & 18 deletions app/code/Magento/Catalog/Model/Product/Copier.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
<?php
/**
* Catalog product copier. Creates product duplicate
*
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
Expand All @@ -11,7 +9,11 @@
use Magento\Catalog\Model\Product;

/**
* The copier creates product duplicates.
* Catalog product copier.
*
* Creates product duplicate.
*
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class Copier
{
Expand Down Expand Up @@ -74,22 +76,9 @@ public function copy(Product $product)
$duplicate->setUpdatedAt(null);
$duplicate->setId(null);
$duplicate->setStoreId(\Magento\Store\Model\Store::DEFAULT_STORE_ID);

$this->copyConstructor->build($product, $duplicate);
$isDuplicateSaved = false;
do {
$urlKey = $duplicate->getUrlKey();
$urlKey = preg_match('/(.*)-(\d+)$/', $urlKey, $matches)
? $matches[1] . '-' . ($matches[2] + 1)
: $urlKey . '-1';
$duplicate->setUrlKey($urlKey);
$duplicate->setData('url_path', null);
try {
$duplicate->save();
$isDuplicateSaved = true;
} catch (\Magento\Framework\Exception\AlreadyExistsException $e) {
}
} while (!$isDuplicateSaved);
$this->setDefaultUrl($product, $duplicate);
$this->setStoresUrl($product, $duplicate);
$this->getOptionRepository()->duplicate($product, $duplicate);
$product->getResource()->duplicate(
$product->getData($metadata->getLinkField()),
Expand All @@ -98,6 +87,81 @@ public function copy(Product $product)
return $duplicate;
}

/**
* Set default URL.
*
* @param Product $product
* @param Product $duplicate
* @return void
*/
private function setDefaultUrl(Product $product, Product $duplicate) : void
{
$duplicate->setStoreId(\Magento\Store\Model\Store::DEFAULT_STORE_ID);
$resource = $product->getResource();
$attribute = $resource->getAttribute('url_key');
$productId = $product->getId();
$urlKey = $resource->getAttributeRawValue($productId, 'url_key', \Magento\Store\Model\Store::DEFAULT_STORE_ID);
do {
$urlKey = $this->modifyUrl($urlKey);
$duplicate->setUrlKey($urlKey);
} while (!$attribute->getEntity()->checkAttributeUniqueValue($attribute, $duplicate));
$duplicate->setData('url_path', null);
$duplicate->save();
}

/**
* Set URL for each store.
*
* @param Product $product
* @param Product $duplicate
* @return void
*/
private function setStoresUrl(Product $product, Product $duplicate) : void
{
$storeIds = $duplicate->getStoreIds();
$productId = $product->getId();
$productResource = $product->getResource();
$defaultUrlKey = $productResource->getAttributeRawValue(
$productId,
'url_key',
\Magento\Store\Model\Store::DEFAULT_STORE_ID
);
$duplicate->setData('save_rewrites_history', false);
foreach ($storeIds as $storeId) {
$isDuplicateSaved = false;
$duplicate->setStoreId($storeId);
$urlKey = $productResource->getAttributeRawValue($productId, 'url_key', $storeId);
if ($urlKey === $defaultUrlKey) {
continue;
}
do {
$urlKey = $this->modifyUrl($urlKey);
$duplicate->setUrlKey($urlKey);
$duplicate->setData('url_path', null);
try {
$duplicate->save();
$isDuplicateSaved = true;
// phpcs:ignore Magento2.CodeAnalysis.EmptyBlock
} catch (\Magento\Framework\Exception\AlreadyExistsException $e) {
}
} while (!$isDuplicateSaved);
}
$duplicate->setStoreId(\Magento\Store\Model\Store::DEFAULT_STORE_ID);
}

/**
* Modify URL key.
*
* @param string $urlKey
* @return string
*/
private function modifyUrl(string $urlKey) : string
{
return preg_match('/(.*)-(\d+)$/', $urlKey, $matches)
? $matches[1] . '-' . ($matches[2] + 1)
: $urlKey . '-1';
}

/**
* Returns product option repository.
*
Expand Down
64 changes: 50 additions & 14 deletions app/code/Magento/Catalog/Test/Unit/Model/Product/CopierTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
namespace Magento\Catalog\Test\Unit\Model\Product;

use Magento\Catalog\Api\Data\ProductInterface;
use \Magento\Catalog\Model\Product\Copier;
use Magento\Catalog\Model\Product;
use Magento\Catalog\Model\Product\Copier;

/**
* Test for Magento\Catalog\Model\Product\Copier class.
*
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class CopierTest extends \PHPUnit\Framework\TestCase
Expand Down Expand Up @@ -76,6 +78,9 @@ protected function setUp()
]);
}

/**
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
*/
public function testCopy()
{
$stockItem = $this->getMockBuilder(\Magento\CatalogInventory\Api\Data\StockItemInterface::class)
Expand Down Expand Up @@ -103,8 +108,44 @@ public function testCopy()
['linkField', null, '1'],
]);

$resourceMock = $this->createMock(\Magento\Catalog\Model\ResourceModel\Product::class);
$this->productMock->expects($this->once())->method('getResource')->will($this->returnValue($resourceMock));
$entityMock = $this->getMockForAbstractClass(
\Magento\Eav\Model\Entity\AbstractEntity::class,
[],
'',
false,
true,
true,
['checkAttributeUniqueValue']
);
$entityMock->expects($this->any())
->method('checkAttributeUniqueValue')
->willReturn(true);

$attributeMock = $this->getMockForAbstractClass(
\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class,
[],
'',
false,
true,
true,
['getEntity']
);
$attributeMock->expects($this->any())
->method('getEntity')
->willReturn($entityMock);

$resourceMock = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Product::class)
->disableOriginalConstructor()
->setMethods(['getAttributeRawValue', 'duplicate', 'getAttribute'])
->getMock();
$resourceMock->expects($this->any())
->method('getAttributeRawValue')
->willReturn('urk-key-1');
$resourceMock->expects($this->any())
->method('getAttribute')
->willReturn($attributeMock);

$this->productMock->expects($this->any())->method('getResource')->will($this->returnValue($resourceMock));

$duplicateMock = $this->createPartialMock(
Product::class,
Expand All @@ -119,11 +160,11 @@ public function testCopy()
'setCreatedAt',
'setUpdatedAt',
'setId',
'setStoreId',
'getEntityId',
'save',
'setUrlKey',
'getUrlKey',
'setStoreId',
'getStoreIds',
]
);
$this->productFactoryMock->expects($this->once())->method('create')->will($this->returnValue($duplicateMock));
Expand All @@ -138,27 +179,22 @@ public function testCopy()
)->with(
\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_DISABLED
);
$duplicateMock->expects($this->atLeastOnce())->method('setStoreId');
$duplicateMock->expects($this->once())->method('setCreatedAt')->with(null);
$duplicateMock->expects($this->once())->method('setUpdatedAt')->with(null);
$duplicateMock->expects($this->once())->method('setId')->with(null);
$duplicateMock->expects(
$this->once()
)->method(
'setStoreId'
)->with(
\Magento\Store\Model\Store::DEFAULT_STORE_ID
);
$duplicateMock->expects($this->atLeastOnce())->method('getStoreIds')->willReturn([]);
$duplicateMock->expects($this->atLeastOnce())->method('setData')->willReturn($duplicateMock);
$this->copyConstructorMock->expects($this->once())->method('build')->with($this->productMock, $duplicateMock);
$duplicateMock->expects($this->once())->method('getUrlKey')->willReturn('urk-key-1');
$duplicateMock->expects($this->once())->method('setUrlKey')->with('urk-key-2')->willReturn($duplicateMock);
$duplicateMock->expects($this->once())->method('save');

$this->metadata->expects($this->any())->method('getLinkField')->willReturn('linkField');

$duplicateMock->expects($this->any())->method('getData')->willReturnMap([
['linkField', null, '2'],
]); $this->optionRepositoryMock->expects($this->once())
]);
$this->optionRepositoryMock->expects($this->once())
->method('duplicate')
->with($this->productMock, $duplicateMock);
$resourceMock->expects($this->once())->method('duplicate')->with(1, 2);
Expand Down