Skip to content

Commit 372ddb2

Browse files
committed
ACP2E-3669: [CLOUD] Product URL Rewrites Not Created for New Store: Go Live Blocker
1 parent 9c05217 commit 372ddb2

File tree

13 files changed

+285
-68
lines changed

13 files changed

+285
-68
lines changed

app/code/Magento/Catalog/Test/Mftf/Test/DeleteCategoriesTest.xml

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<!--
33
/**
4-
* Copyright © Magento, Inc. All rights reserved.
5-
* See COPYING.txt for license details.
4+
* Copyright 2018 Adobe
5+
* All Rights Reserved.
66
*/
77
-->
88

@@ -43,6 +43,13 @@
4343
</actionGroup>
4444
</before>
4545
<after>
46+
<!-- Rename New Root Category to Default category -->
47+
<actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="navigateToCategoryPageAfterStoreFrontProductsAssertions"/>
48+
<click selector="{{AdminCategorySidebarTreeSection.categoryInTree('$$createNewRootCategoryA.name$$')}}" stepKey="clickOnNewRootCategoryA"/>
49+
<waitForPageLoad stepKey="waitForPageNewRootCategoryALoad" />
50+
<fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="Default Category" stepKey="enterCategoryNameAsDefaultCategory"/>
51+
<actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategoryDefaultCategory"/>
52+
<seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="assertSuccessMessageAfterSaveDefaultCategory"/>
4653
<actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/>
4754
<deleteData createDataKey="createProduct1" stepKey="deleteProduct1"/>
4855
<deleteData createDataKey="createProduct2" stepKey="deleteProduct2"/>
@@ -78,6 +85,11 @@
7885
<click selector="{{AdminConfirmationModalSection.ok}}" stepKey="acceptModal" />
7986
<waitForElementVisible selector="{{AdminStoresGridSection.storeFilterTextField}}" stepKey="waitForPageAdminStoresGridReload"/>
8087
<see userInput="You saved the store." stepKey="seeSavedMessage"/>
88+
<!-- Run the consumer to start processing scheduled update of the product url rewrites values. -->
89+
<actionGroup ref="CliConsumerStartActionGroup" stepKey="startMessageQueue">
90+
<argument name="consumerName" value="{{CatalogProductGenerateUrlsConsumerData.consumerName}}"/>
91+
<argument name="maxMessages" value="{{CatalogProductGenerateUrlsConsumerData.messageLimit}}"/>
92+
</actionGroup>
8193

8294
<!-- @TODO: Uncomment commented below code after MQE-903 is fixed -->
8395
<!-- Perform cli reindex. -->
@@ -155,12 +167,6 @@
155167
<actionGroup ref="StorefrontCheckSimpleProductActionGroup" stepKey="browseAssertProduct3Page">
156168
<argument name="product" value="$$createProduct3$$"/>
157169
</actionGroup>
158-
<!-- Rename New Root Category to Default category -->
159-
<actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="navigateToCategoryPageAfterStoreFrontProductsAssertions"/>
160-
<click selector="{{AdminCategorySidebarTreeSection.categoryInTree('$$createNewRootCategoryA.name$$')}}" stepKey="clickOnNewRootCategoryA"/>
161-
<waitForPageLoad stepKey="waitForPageNewRootCategoryALoad" />
162-
<fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="Default Category" stepKey="enterCategoryNameAsDefaultCategory"/>
163-
<actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategoryDefaultCategory"/>
164-
<seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="assertSuccessMessageAfterSaveDefaultCategory"/>
170+
165171
</test>
166172
</tests>

app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Store/Group.php

Lines changed: 22 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
<?php
22
/**
3-
* Copyright © Magento, Inc. All rights reserved.
4-
* See COPYING.txt for license details.
3+
* Copyright 2014 Adobe
4+
* All Rights Reserved.
55
*/
66
namespace Magento\CatalogUrlRewrite\Model\Category\Plugin\Store;
77

8+
use Magento\CatalogUrlRewrite\Model\Scheduler;
89
use Magento\Store\Model\ResourceModel\Group as StoreGroup;
910
use Magento\UrlRewrite\Model\UrlPersistInterface;
1011
use Magento\UrlRewrite\Service\V1\Data\UrlRewrite;
@@ -53,28 +54,35 @@ class Group
5354
*/
5455
protected $storeManager;
5556

57+
/**
58+
* @var Scheduler
59+
*/
60+
private $scheduler;
5661
/**
5762
* @param UrlPersistInterface $urlPersist
5863
* @param CategoryFactory $categoryFactory
5964
* @param ProductFactory $productFactory
6065
* @param CategoryUrlRewriteGenerator $categoryUrlRewriteGenerator
6166
* @param ProductUrlRewriteGenerator $productUrlRewriteGenerator
6267
* @param StoreManagerInterface $storeManager
68+
* @param Scheduler $scheduler
6369
*/
6470
public function __construct(
6571
UrlPersistInterface $urlPersist,
6672
CategoryFactory $categoryFactory,
6773
ProductFactory $productFactory,
6874
CategoryUrlRewriteGenerator $categoryUrlRewriteGenerator,
6975
ProductUrlRewriteGenerator $productUrlRewriteGenerator,
70-
StoreManagerInterface $storeManager
76+
StoreManagerInterface $storeManager,
77+
Scheduler $scheduler
7178
) {
7279
$this->urlPersist = $urlPersist;
7380
$this->categoryFactory = $categoryFactory;
7481
$this->productFactory = $productFactory;
7582
$this->categoryUrlRewriteGenerator = $categoryUrlRewriteGenerator;
7683
$this->productUrlRewriteGenerator = $productUrlRewriteGenerator;
7784
$this->storeManager = $storeManager;
85+
$this->scheduler = $scheduler;
7886
}
7987

8088
/**
@@ -105,39 +113,20 @@ public function afterSave(
105113
$this->generateCategoryUrls($group->getRootCategoryId(), $group->getStoreIds())
106114
);
107115

108-
$this->urlPersist->replace(
109-
$this->generateProductUrls($group->getWebsiteId(), $group->getOrigData('website_id'))
110-
);
111-
}
116+
$websiteId = $group->getWebsiteId();
117+
$originWebsiteId = $group->getOrigData('website_id');
112118

113-
return $result;
114-
}
115-
116-
/**
117-
* Generate url rewrites for products assigned to website
118-
*
119-
* @param int $websiteId
120-
* @param int $originWebsiteId
121-
* @return array
122-
*/
123-
protected function generateProductUrls($websiteId, $originWebsiteId)
124-
{
125-
$urls = [];
126-
$websiteIds = $websiteId != $originWebsiteId
127-
? [$websiteId, $originWebsiteId]
128-
: [$websiteId];
129-
$collection = $this->productFactory->create()
130-
->getCollection()
131-
->addCategoryIds()
132-
->addAttributeToSelect(['name', 'url_path', 'url_key', 'visibility'])
133-
->addWebsiteFilter($websiteIds);
134-
foreach ($collection as $product) {
135-
/** @var \Magento\Catalog\Model\Product $product */
136-
$product->setStoreId(Store::DEFAULT_STORE_ID);
137-
$urls[] = $this->productUrlRewriteGenerator->generate($product);
119+
if ($originWebsiteId !== null && $websiteId !== $originWebsiteId) {
120+
$websiteIds = [$websiteId, $originWebsiteId];
121+
} else {
122+
$websiteIds = [$websiteId];
123+
}
124+
foreach ($websiteIds as $websiteId) {
125+
$this->scheduler->execute($websiteId);
126+
}
138127
}
139128

140-
return array_merge([], ...$urls);
129+
return $result;
141130
}
142131

143132
/**
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\CatalogUrlRewrite\Model;
9+
10+
use Magento\AsynchronousOperations\Api\Data\OperationInterface;
11+
use Magento\AsynchronousOperations\Api\Data\OperationInterfaceFactory;
12+
use Magento\Framework\Bulk\BulkManagementInterface;
13+
use Magento\Framework\DataObject\IdentityGeneratorInterface;
14+
use Magento\Framework\Exception\LocalizedException;
15+
use Magento\Framework\Serialize\SerializerInterface;
16+
17+
class Scheduler
18+
{
19+
private const TOPIC_NAME = 'catalog_product_generate_urls';
20+
21+
/**
22+
* @param BulkManagementInterface $bulkManagement
23+
* @param IdentityGeneratorInterface $identityGenerator
24+
* @param OperationInterfaceFactory $operationFactory
25+
* @param SerializerInterface $serializer
26+
*/
27+
public function __construct(
28+
private BulkManagementInterface $bulkManagement,
29+
private IdentityGeneratorInterface $identityGenerator,
30+
private OperationInterfaceFactory $operationFactory,
31+
private SerializerInterface $serializer
32+
) {
33+
}
34+
35+
/**
36+
* Schedule updating product url rewrites values.
37+
*
38+
* @param int $websiteId
39+
* @return void
40+
* @throws LocalizedException
41+
*/
42+
public function execute(int $websiteId): void
43+
{
44+
$bulkUuid = $this->identityGenerator->generateId();
45+
$operation = $this->operationFactory->create(
46+
[
47+
'data' => [
48+
'bulk_uuid' => $bulkUuid,
49+
'topic_name' => self::TOPIC_NAME,
50+
'serialized_data' => $this->serializer->serialize(['website_id' => $websiteId]),
51+
'status' => OperationInterface::STATUS_TYPE_OPEN,
52+
]
53+
]
54+
);
55+
$bulkDescription = __('Update Product Url Rewrites values');
56+
$result = $this->bulkManagement->scheduleBulk($bulkUuid, [$operation], $bulkDescription);
57+
if (!$result) {
58+
throw new LocalizedException(__('Something went wrong while scheduling operations.'));
59+
}
60+
}
61+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\CatalogUrlRewrite\Model;
9+
10+
use Magento\AsynchronousOperations\Api\Data\OperationInterface;
11+
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory;
12+
use Magento\Framework\DB\Adapter\DeadlockException;
13+
use Magento\Framework\DB\Adapter\LockWaitException;
14+
use Magento\Framework\EntityManager\EntityManager;
15+
use Magento\Framework\Exception\LocalizedException;
16+
use Magento\Framework\Serialize\SerializerInterface;
17+
use Magento\Store\Model\Store;
18+
use Magento\UrlRewrite\Model\UrlPersistInterface;
19+
use Psr\Log\LoggerInterface;
20+
21+
/**
22+
* Product url rewrites updater.
23+
*/
24+
class UpdateProductUrlRewrite
25+
{
26+
/**
27+
* @param EntityManager $entityManager
28+
* @param SerializerInterface $serializer
29+
* @param LoggerInterface $logger
30+
* @param UrlPersistInterface $urlPersist
31+
* @param CollectionFactory $productCollectionFactory
32+
* @param ProductUrlRewriteGenerator $productUrlRewriteGenerator
33+
* @param int $batchSize
34+
*/
35+
public function __construct(
36+
private EntityManager $entityManager,
37+
private SerializerInterface $serializer,
38+
private LoggerInterface $logger,
39+
private UrlPersistInterface $urlPersist,
40+
private CollectionFactory $productCollectionFactory,
41+
private ProductUrlRewriteGenerator $productUrlRewriteGenerator,
42+
private int $batchSize = 5000
43+
) {
44+
}
45+
46+
/**
47+
* Process generation of url rewrites for products.
48+
*
49+
* @param OperationInterface $operation
50+
* @return void
51+
*/
52+
public function process(OperationInterface $operation): void
53+
{
54+
try {
55+
$serializedData = $operation->getSerializedData();
56+
$data = $this->serializer->unserialize($serializedData);
57+
$this->urlPersist->replace($this->generateProductUrls($data['website_id']));
58+
$operation->setStatus(OperationInterface::STATUS_TYPE_COMPLETE);
59+
$operation->setResultMessage(null);
60+
} catch (LockWaitException|DeadlockException $e) {
61+
$operation->setStatus(OperationInterface::STATUS_TYPE_RETRIABLY_FAILED);
62+
$operation->setErrorCode($e->getCode());
63+
$operation->setResultMessage($e->getMessage());
64+
} catch (LocalizedException $e) {
65+
$operation->setStatus(OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED);
66+
$operation->setErrorCode($e->getCode());
67+
$operation->setResultMessage($e->getMessage());
68+
} catch (\Throwable $e) {
69+
$this->logger->critical($e);
70+
$operation->setStatus(OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED);
71+
$operation->setErrorCode($e->getCode());
72+
$operation->setResultMessage(
73+
__('Sorry, something went wrong during update synchronization. Please see log for details.')
74+
);
75+
}
76+
$this->entityManager->save($operation);
77+
}
78+
79+
/**
80+
* Generate url rewrites for products assigned to website
81+
*
82+
* @param int $websiteId
83+
* @return array
84+
*/
85+
private function generateProductUrls(int $websiteId): array
86+
{
87+
$urls = [];
88+
$collection = $this->productCollectionFactory->create()
89+
->addCategoryIds()
90+
->addAttributeToSelect(['name', 'url_path', 'url_key', 'visibility'])
91+
->addWebsiteFilter([$websiteId]);
92+
93+
$collection->setPageSize($this->batchSize);
94+
$pages = $collection->getLastPageNumber();
95+
96+
for ($currentPage = 1; $currentPage <= $pages; $currentPage++) {
97+
$collection->setCurPage($currentPage);
98+
99+
foreach ($collection as $product) {
100+
$product->setStoreId(Store::DEFAULT_STORE_ID);
101+
$urls[] = $this->productUrlRewriteGenerator->generate($product);
102+
}
103+
$collection->clear();
104+
}
105+
106+
return array_merge([], ...$urls);
107+
}
108+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
/**
4+
* Copyright © Magento, Inc. All rights reserved.
5+
* See COPYING.txt for license details.
6+
*/
7+
-->
8+
9+
<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
10+
xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd">
11+
<entity name="CatalogProductGenerateUrlsConsumerData">
12+
<data key="consumerName">catalog_product_generate_urls</data>
13+
<data key="messageLimit">1</data>
14+
</entity>
15+
</entities>

app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Category/Plugin/Store/GroupTest.php

Lines changed: 6 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?php
22
/**
3-
* Copyright © Magento, Inc. All rights reserved.
4-
* See COPYING.txt for license details.
3+
* Copyright 2016 Adobe
4+
* All Rights Reserved.
55
*/
66
declare(strict_types=1);
77

@@ -86,7 +86,7 @@ protected function setUp(): void
8686
$this->objectManager = new ObjectManager($this);
8787
$this->abstractModelMock = $this->getMockBuilder(AbstractModel::class)
8888
->disableOriginalConstructor()
89-
->addMethods(['getStoreIds'])
89+
->addMethods(['getStoreIds', 'getWebsiteId'])
9090
->onlyMethods(['isObjectNew', 'dataHasChangedFor'])
9191
->getMockForAbstractClass();
9292
$this->subjectMock = $this->getMockBuilder(Group::class)
@@ -139,6 +139,9 @@ public function testAfterSave()
139139
$this->abstractModelMock->expects($this->any())
140140
->method('getStoreIds')
141141
->willReturn(['1']);
142+
$this->abstractModelMock->expects($this->once())
143+
->method('getWebsiteId')
144+
->willReturn(1);
142145
$this->abstractModelMock->expects($this->once())
143146
->method('dataHasChangedFor')
144147
->with('website_id')
@@ -151,29 +154,6 @@ public function testAfterSave()
151154
$this->categoryFactoryMock->expects($this->once())
152155
->method('create')
153156
->willReturn($this->categoryMock);
154-
$this->productFactoryMock->expects($this->once())
155-
->method('create')
156-
->willReturn($this->productMock);
157-
$this->productMock->expects($this->once())
158-
->method('getCollection')
159-
->willReturn($this->productCollectionMock);
160-
$this->productCollectionMock->expects($this->once())
161-
->method('addCategoryIds')
162-
->willReturn($this->productCollectionMock);
163-
$this->productCollectionMock->expects($this->once())
164-
->method('addAttributeToSelect')
165-
->willReturn($this->productCollectionMock);
166-
$this->productCollectionMock->expects($this->once())
167-
->method('addWebsiteFilter')
168-
->willReturn($this->productCollectionMock);
169-
$arrayIteratorMock = new \ArrayIterator([$this->productMock]);
170-
$this->productCollectionMock->expects($this->once())
171-
->method('getIterator')
172-
->willReturn($arrayIteratorMock);
173-
$this->productUrlRewriteGeneratorMock->expects($this->once())
174-
->method('generate')
175-
->with($this->productMock)
176-
->willReturn([]);
177157

178158
$this->assertSame(
179159
$this->subjectMock,

0 commit comments

Comments
 (0)