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