Skip to content

Commit fe5d25a

Browse files
authored
ENGCOM-4776: #21737 Duplicating product with translated url keys over multiple sto… #22178
2 parents d55e677 + 553c9db commit fe5d25a

File tree

2 files changed

+132
-32
lines changed

2 files changed

+132
-32
lines changed

app/code/Magento/Catalog/Model/Product/Copier.php

Lines changed: 82 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
<?php
22
/**
3-
* Catalog product copier. Creates product duplicate
4-
*
53
* Copyright © Magento, Inc. All rights reserved.
64
* See COPYING.txt for license details.
75
*/
@@ -11,7 +9,11 @@
119
use Magento\Catalog\Model\Product;
1210

1311
/**
14-
* The copier creates product duplicates.
12+
* Catalog product copier.
13+
*
14+
* Creates product duplicate.
15+
*
16+
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
1517
*/
1618
class Copier
1719
{
@@ -74,22 +76,9 @@ public function copy(Product $product)
7476
$duplicate->setUpdatedAt(null);
7577
$duplicate->setId(null);
7678
$duplicate->setStoreId(\Magento\Store\Model\Store::DEFAULT_STORE_ID);
77-
7879
$this->copyConstructor->build($product, $duplicate);
79-
$isDuplicateSaved = false;
80-
do {
81-
$urlKey = $duplicate->getUrlKey();
82-
$urlKey = preg_match('/(.*)-(\d+)$/', $urlKey, $matches)
83-
? $matches[1] . '-' . ($matches[2] + 1)
84-
: $urlKey . '-1';
85-
$duplicate->setUrlKey($urlKey);
86-
$duplicate->setData('url_path', null);
87-
try {
88-
$duplicate->save();
89-
$isDuplicateSaved = true;
90-
} catch (\Magento\Framework\Exception\AlreadyExistsException $e) {
91-
}
92-
} while (!$isDuplicateSaved);
80+
$this->setDefaultUrl($product, $duplicate);
81+
$this->setStoresUrl($product, $duplicate);
9382
$this->getOptionRepository()->duplicate($product, $duplicate);
9483
$product->getResource()->duplicate(
9584
$product->getData($metadata->getLinkField()),
@@ -98,6 +87,81 @@ public function copy(Product $product)
9887
return $duplicate;
9988
}
10089

90+
/**
91+
* Set default URL.
92+
*
93+
* @param Product $product
94+
* @param Product $duplicate
95+
* @return void
96+
*/
97+
private function setDefaultUrl(Product $product, Product $duplicate) : void
98+
{
99+
$duplicate->setStoreId(\Magento\Store\Model\Store::DEFAULT_STORE_ID);
100+
$resource = $product->getResource();
101+
$attribute = $resource->getAttribute('url_key');
102+
$productId = $product->getId();
103+
$urlKey = $resource->getAttributeRawValue($productId, 'url_key', \Magento\Store\Model\Store::DEFAULT_STORE_ID);
104+
do {
105+
$urlKey = $this->modifyUrl($urlKey);
106+
$duplicate->setUrlKey($urlKey);
107+
} while (!$attribute->getEntity()->checkAttributeUniqueValue($attribute, $duplicate));
108+
$duplicate->setData('url_path', null);
109+
$duplicate->save();
110+
}
111+
112+
/**
113+
* Set URL for each store.
114+
*
115+
* @param Product $product
116+
* @param Product $duplicate
117+
* @return void
118+
*/
119+
private function setStoresUrl(Product $product, Product $duplicate) : void
120+
{
121+
$storeIds = $duplicate->getStoreIds();
122+
$productId = $product->getId();
123+
$productResource = $product->getResource();
124+
$defaultUrlKey = $productResource->getAttributeRawValue(
125+
$productId,
126+
'url_key',
127+
\Magento\Store\Model\Store::DEFAULT_STORE_ID
128+
);
129+
$duplicate->setData('save_rewrites_history', false);
130+
foreach ($storeIds as $storeId) {
131+
$isDuplicateSaved = false;
132+
$duplicate->setStoreId($storeId);
133+
$urlKey = $productResource->getAttributeRawValue($productId, 'url_key', $storeId);
134+
if ($urlKey === $defaultUrlKey) {
135+
continue;
136+
}
137+
do {
138+
$urlKey = $this->modifyUrl($urlKey);
139+
$duplicate->setUrlKey($urlKey);
140+
$duplicate->setData('url_path', null);
141+
try {
142+
$duplicate->save();
143+
$isDuplicateSaved = true;
144+
// phpcs:ignore Magento2.CodeAnalysis.EmptyBlock
145+
} catch (\Magento\Framework\Exception\AlreadyExistsException $e) {
146+
}
147+
} while (!$isDuplicateSaved);
148+
}
149+
$duplicate->setStoreId(\Magento\Store\Model\Store::DEFAULT_STORE_ID);
150+
}
151+
152+
/**
153+
* Modify URL key.
154+
*
155+
* @param string $urlKey
156+
* @return string
157+
*/
158+
private function modifyUrl(string $urlKey) : string
159+
{
160+
return preg_match('/(.*)-(\d+)$/', $urlKey, $matches)
161+
? $matches[1] . '-' . ($matches[2] + 1)
162+
: $urlKey . '-1';
163+
}
164+
101165
/**
102166
* Returns product option repository.
103167
*

app/code/Magento/Catalog/Test/Unit/Model/Product/CopierTest.php

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
namespace Magento\Catalog\Test\Unit\Model\Product;
77

88
use Magento\Catalog\Api\Data\ProductInterface;
9-
use \Magento\Catalog\Model\Product\Copier;
109
use Magento\Catalog\Model\Product;
10+
use Magento\Catalog\Model\Product\Copier;
1111

1212
/**
13+
* Test for Magento\Catalog\Model\Product\Copier class.
14+
*
1315
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
1416
*/
1517
class CopierTest extends \PHPUnit\Framework\TestCase
@@ -76,6 +78,9 @@ protected function setUp()
7678
]);
7779
}
7880

81+
/**
82+
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
83+
*/
7984
public function testCopy()
8085
{
8186
$stockItem = $this->getMockBuilder(\Magento\CatalogInventory\Api\Data\StockItemInterface::class)
@@ -103,8 +108,44 @@ public function testCopy()
103108
['linkField', null, '1'],
104109
]);
105110

106-
$resourceMock = $this->createMock(\Magento\Catalog\Model\ResourceModel\Product::class);
107-
$this->productMock->expects($this->once())->method('getResource')->will($this->returnValue($resourceMock));
111+
$entityMock = $this->getMockForAbstractClass(
112+
\Magento\Eav\Model\Entity\AbstractEntity::class,
113+
[],
114+
'',
115+
false,
116+
true,
117+
true,
118+
['checkAttributeUniqueValue']
119+
);
120+
$entityMock->expects($this->any())
121+
->method('checkAttributeUniqueValue')
122+
->willReturn(true);
123+
124+
$attributeMock = $this->getMockForAbstractClass(
125+
\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class,
126+
[],
127+
'',
128+
false,
129+
true,
130+
true,
131+
['getEntity']
132+
);
133+
$attributeMock->expects($this->any())
134+
->method('getEntity')
135+
->willReturn($entityMock);
136+
137+
$resourceMock = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Product::class)
138+
->disableOriginalConstructor()
139+
->setMethods(['getAttributeRawValue', 'duplicate', 'getAttribute'])
140+
->getMock();
141+
$resourceMock->expects($this->any())
142+
->method('getAttributeRawValue')
143+
->willReturn('urk-key-1');
144+
$resourceMock->expects($this->any())
145+
->method('getAttribute')
146+
->willReturn($attributeMock);
147+
148+
$this->productMock->expects($this->any())->method('getResource')->will($this->returnValue($resourceMock));
108149

109150
$duplicateMock = $this->createPartialMock(
110151
Product::class,
@@ -119,11 +160,11 @@ public function testCopy()
119160
'setCreatedAt',
120161
'setUpdatedAt',
121162
'setId',
122-
'setStoreId',
123163
'getEntityId',
124164
'save',
125165
'setUrlKey',
126-
'getUrlKey',
166+
'setStoreId',
167+
'getStoreIds',
127168
]
128169
);
129170
$this->productFactoryMock->expects($this->once())->method('create')->will($this->returnValue($duplicateMock));
@@ -138,27 +179,22 @@ public function testCopy()
138179
)->with(
139180
\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_DISABLED
140181
);
182+
$duplicateMock->expects($this->atLeastOnce())->method('setStoreId');
141183
$duplicateMock->expects($this->once())->method('setCreatedAt')->with(null);
142184
$duplicateMock->expects($this->once())->method('setUpdatedAt')->with(null);
143185
$duplicateMock->expects($this->once())->method('setId')->with(null);
144-
$duplicateMock->expects(
145-
$this->once()
146-
)->method(
147-
'setStoreId'
148-
)->with(
149-
\Magento\Store\Model\Store::DEFAULT_STORE_ID
150-
);
186+
$duplicateMock->expects($this->atLeastOnce())->method('getStoreIds')->willReturn([]);
151187
$duplicateMock->expects($this->atLeastOnce())->method('setData')->willReturn($duplicateMock);
152188
$this->copyConstructorMock->expects($this->once())->method('build')->with($this->productMock, $duplicateMock);
153-
$duplicateMock->expects($this->once())->method('getUrlKey')->willReturn('urk-key-1');
154189
$duplicateMock->expects($this->once())->method('setUrlKey')->with('urk-key-2')->willReturn($duplicateMock);
155190
$duplicateMock->expects($this->once())->method('save');
156191

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

159194
$duplicateMock->expects($this->any())->method('getData')->willReturnMap([
160195
['linkField', null, '2'],
161-
]); $this->optionRepositoryMock->expects($this->once())
196+
]);
197+
$this->optionRepositoryMock->expects($this->once())
162198
->method('duplicate')
163199
->with($this->productMock, $duplicateMock);
164200
$resourceMock->expects($this->once())->method('duplicate')->with(1, 2);

0 commit comments

Comments
 (0)