Skip to content

Commit a16ed5e

Browse files
committed
Merge branch 'ACP2E-2641' of https://github.com/magento-l3/magento2ce into 2.4-develop
2 parents 1871319 + e9bfbf3 commit a16ed5e

File tree

4 files changed

+244
-15
lines changed

4 files changed

+244
-15
lines changed

app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlPathGenerator.php

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
namespace Magento\CatalogUrlRewrite\Model;
77

88
use Magento\Catalog\Api\CategoryRepositoryInterface;
9+
use Magento\Catalog\Api\Data\CategoryInterface;
910
use Magento\Catalog\Model\Category;
1011

1112
/**
@@ -16,12 +17,12 @@ class CategoryUrlPathGenerator
1617
/**
1718
* Minimal category level that can be considered for generate path
1819
*/
19-
const MINIMAL_CATEGORY_LEVEL_FOR_PROCESSING = 3;
20+
public const MINIMAL_CATEGORY_LEVEL_FOR_PROCESSING = 3;
2021

2122
/**
2223
* XML path for category url suffix
2324
*/
24-
const XML_PATH_CATEGORY_URL_SUFFIX = 'catalog/seo/category_url_suffix';
25+
public const XML_PATH_CATEGORY_URL_SUFFIX = 'catalog/seo/category_url_suffix';
2526

2627
/**
2728
* Cache for category rewrite suffix
@@ -73,14 +74,12 @@ public function getUrlPath($category, $parentCategory = null)
7374
if (in_array($category->getParentId(), [Category::ROOT_CATEGORY_ID, Category::TREE_ROOT_ID])) {
7475
return '';
7576
}
76-
$path = $category->getUrlPath();
77-
if ($path !== null && !$category->dataHasChangedFor('url_key') && !$category->dataHasChangedFor('parent_id')) {
78-
return $path;
79-
}
80-
$path = $category->getUrlKey();
81-
if ($path === false) {
77+
78+
if ($this->shouldReturnCurrentUrlPath($category)) {
8279
return $category->getUrlPath();
8380
}
81+
82+
$path = $category->getUrlKey();
8483
if ($this->isNeedToGenerateUrlPathForParent($category)) {
8584
$parentCategory = $parentCategory === null ?
8685
$this->categoryRepository->get($category->getParentId(), $category->getStoreId()) : $parentCategory;
@@ -90,6 +89,27 @@ public function getUrlPath($category, $parentCategory = null)
9089
return $path;
9190
}
9291

92+
/**
93+
* Check if current category url path is valid to be returned
94+
*
95+
* @param CategoryInterface $category
96+
* @return bool
97+
*/
98+
private function shouldReturnCurrentUrlPath(CategoryInterface $category): bool
99+
{
100+
$path = $category->getUrlPath();
101+
if ($path !== null && !$category->dataHasChangedFor('url_key') && !$category->dataHasChangedFor('parent_id')) {
102+
$parentPath = $this->generateParentUrlPathFromUrlKeys($category);
103+
if (strlen($parentPath) && str_contains($path, $parentPath) !== false) {
104+
return true;
105+
}
106+
}
107+
if (empty($category->getUrlKey())) {
108+
return true;
109+
}
110+
return false;
111+
}
112+
93113
/**
94114
* Define whether we should generate URL path for parent
95115
*
@@ -159,4 +179,27 @@ public function getUrlKey($category)
159179
$urlKey = $category->getUrlKey();
160180
return $category->formatUrlKey($urlKey === '' || $urlKey === null ? $category->getName() : $urlKey);
161181
}
182+
183+
/**
184+
* Generate a parent url path based on custom scoped url keys
185+
*
186+
* @param CategoryInterface $category
187+
* @return string
188+
*/
189+
private function generateParentUrlPathFromUrlKeys(CategoryInterface $category): string
190+
{
191+
$storeId = $category->getStoreId();
192+
$currentStore = $this->storeManager->getStore();
193+
$this->storeManager->setCurrentStore($storeId);
194+
195+
$parentPath = [];
196+
foreach ($category->getParentCategories() as $parentCategory) {
197+
if ($parentCategory->getUrlKey() && (int)$category->getId() !== (int)$parentCategory->getId()) {
198+
$parentPath[] = $parentCategory->getUrlKey();
199+
}
200+
}
201+
202+
$this->storeManager->setCurrentStore($currentStore);
203+
return implode('/', $parentPath);
204+
}
162205
}

app/code/Magento/CatalogUrlRewrite/Observer/CategoryUrlPathAutogeneratorObserver.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,6 @@ public function execute(Observer $observer)
125125
}
126126
}
127127
}
128-
$category->setUrlKey(null)->setUrlPath(null);
129128
}
130129
}
131130

@@ -210,7 +209,7 @@ protected function updateUrlPathForChildren(Category $category)
210209
Category::ENTITY
211210
)) {
212211
$child = $this->categoryRepository->get($childId, $storeId);
213-
$this->updateUrlPathForCategory($child);
212+
$this->updateUrlPathForCategory($child, $category);
214213
}
215214
}
216215
}

app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/CategoryUrlPathGeneratorTest.php

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ protected function setUp(): void
5151
'getId',
5252
'formatUrlKey',
5353
'getName',
54-
'isObjectNew'
54+
'isObjectNew',
55+
'getParentCategories'
5556
]
5657
)
5758
->disableOriginalConstructor()
@@ -60,6 +61,10 @@ protected function setUp(): void
6061
$this->scopeConfig = $this->getMockForAbstractClass(ScopeConfigInterface::class);
6162
$this->categoryRepository = $this->getMockForAbstractClass(CategoryRepositoryInterface::class);
6263

64+
$this->category->expects($this->any())
65+
->method('getParentCategories')
66+
->willReturn([]);
67+
6368
$this->categoryUrlPathGenerator = (new ObjectManager($this))->getObject(
6469
CategoryUrlPathGenerator::class,
6570
[
@@ -154,10 +159,21 @@ public function testGetUrlPathWithParent(
154159
$this->category->expects($this->any())->method('getUrlPath')->willReturn($urlPath);
155160
$this->category->expects($this->any())->method('getUrlKey')->willReturn($urlKey);
156161
$this->category->expects($this->any())->method('isObjectNew')->willReturn($isCategoryNew);
162+
$this->category->expects($this->any())->method('getStoreId')->willReturn(Store::DEFAULT_STORE_ID);
157163

158164
$parentCategory = $this->getMockBuilder(Category::class)
159165
->addMethods(['getUrlPath'])
160-
->onlyMethods(['__wakeup', 'getParentId', 'getLevel', 'dataHasChangedFor', 'load'])
166+
->onlyMethods(
167+
[
168+
'__wakeup',
169+
'getParentId',
170+
'getLevel',
171+
'dataHasChangedFor',
172+
'load',
173+
'getStoreId',
174+
'getParentCategories'
175+
]
176+
)
161177
->disableOriginalConstructor()
162178
->getMock();
163179
$parentCategory->expects($this->any())->method('getParentId')
@@ -166,10 +182,16 @@ public function testGetUrlPathWithParent(
166182
$parentCategory->expects($this->any())->method('getUrlPath')->willReturn($parentUrlPath);
167183
$parentCategory->expects($this->any())->method('dataHasChangedFor')
168184
->willReturnMap([['url_key', false], ['path_ids', false]]);
185+
$parentCategory->expects($this->any())->method('getStoreId')->willReturn(Store::DEFAULT_STORE_ID);
186+
$parentCategory->expects($this->any())->method('getParentCategories')->willReturn([]);
169187

170188
$this->categoryRepository->expects($this->any())->method('get')->with(13)
171189
->willReturn($parentCategory);
172190

191+
$store = $this->createMock(Store::class);
192+
$store->expects($this->any())->method('getId')->willReturn(0);
193+
$this->storeManager->expects($this->any())->method('getStore')->willReturn($store);
194+
173195
$this->assertEquals($result, $this->categoryUrlPathGenerator->getUrlPath($this->category));
174196
}
175197

@@ -196,7 +218,7 @@ public function testGetUrlPathWithSuffixAndStore($urlPath, $storeId, $categorySt
196218
{
197219
$this->category->expects($this->any())->method('getStoreId')->willReturn($categoryStoreId);
198220
$this->category->expects($this->once())->method('getParentId')->willReturn(123);
199-
$this->category->expects($this->once())->method('getUrlPath')->willReturn($urlPath);
221+
$this->category->expects($this->exactly(2))->method('getUrlPath')->willReturn($urlPath);
200222
$this->category->expects($this->exactly(2))->method('dataHasChangedFor')
201223
->willReturnMap([['url_key', false], ['path_ids', false]]);
202224

@@ -221,13 +243,13 @@ public function testGetUrlPathWithSuffixWithoutStore()
221243

222244
$this->category->expects($this->any())->method('getStoreId')->willReturn($storeId);
223245
$this->category->expects($this->once())->method('getParentId')->willReturn(2);
224-
$this->category->expects($this->once())->method('getUrlPath')->willReturn($urlPath);
246+
$this->category->expects($this->exactly(2))->method('getUrlPath')->willReturn($urlPath);
225247
$this->category->expects($this->exactly(2))->method('dataHasChangedFor')
226248
->willReturnMap([['url_key', false], ['path_ids', false]]);
227249

228250
$store = $this->createMock(Store::class);
229251
$store->expects($this->once())->method('getId')->willReturn($currentStoreId);
230-
$this->storeManager->expects($this->once())->method('getStore')->willReturn($store);
252+
$this->storeManager->expects($this->exactly(2))->method('getStore')->willReturn($store);
231253
$this->scopeConfig->expects($this->once())->method('getValue')
232254
->with(CategoryUrlPathGenerator::XML_PATH_CATEGORY_URL_SUFFIX, ScopeInterface::SCOPE_STORE, $currentStoreId)
233255
->willReturn($suffix);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
<?php
2+
/************************************************************************
3+
*
4+
* Copyright 2023 Adobe
5+
* All Rights Reserved.
6+
*
7+
* NOTICE: All information contained herein is, and remains
8+
* the property of Adobe and its suppliers, if any. The intellectual
9+
* and technical concepts contained herein are proprietary to Adobe
10+
* and its suppliers and are protected by all applicable intellectual
11+
* property laws, including trade secret and copyright laws.
12+
* Dissemination of this information or reproduction of this material
13+
* is strictly forbidden unless prior written permission is obtained
14+
* from Adobe.
15+
* ************************************************************************
16+
*/
17+
declare(strict_types=1);
18+
19+
namespace Magento\CatalogUrlRewrite\Observer;
20+
21+
use Magento\Catalog\Api\CategoryRepositoryInterface;
22+
use Magento\Catalog\Model\CategoryFactory;
23+
use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory;
24+
use Magento\Catalog\Test\Fixture\Category as CategoryFixture;
25+
use Magento\Framework\App\Response\Http;
26+
use Magento\Framework\ObjectManagerInterface;
27+
use Magento\Store\Model\Store as StoreModel;
28+
use Magento\Store\Model\StoreManagerInterface;
29+
use Magento\Store\Test\Fixture\Group;
30+
use Magento\Store\Test\Fixture\Store;
31+
use Magento\Store\Test\Fixture\Website;
32+
use Magento\TestFramework\Fixture\AppIsolation;
33+
use Magento\TestFramework\Fixture\DataFixture;
34+
use Magento\TestFramework\Fixture\DataFixtureBeforeTransaction;
35+
use Magento\TestFramework\Fixture\DataFixtureStorage;
36+
use Magento\TestFramework\Fixture\DataFixtureStorageManager;
37+
use Magento\TestFramework\Fixture\DbIsolation;
38+
use Magento\TestFramework\Helper\Bootstrap;
39+
use Magento\TestFramework\TestCase\AbstractController;
40+
41+
class CategoryUrlPathAutogeneratorObserverTest extends AbstractController
42+
{
43+
/**
44+
* @var ObjectManagerInterface
45+
*/
46+
private $objectManager;
47+
48+
/**
49+
* @var DataFixtureStorage
50+
*/
51+
private $fixtures;
52+
53+
/** @var StoreManagerInterface */
54+
private $storeManager;
55+
56+
/** @var CategoryRepositoryInterface */
57+
private $categoryRepository;
58+
59+
/**
60+
* @var CategoryFactory
61+
*/
62+
private $categoryFactory;
63+
64+
/**
65+
* @var CollectionFactory
66+
*/
67+
private $categoryCollectionFactory;
68+
protected function setUp(): void
69+
{
70+
parent::setUp();
71+
$this->objectManager = Bootstrap::getObjectManager();
72+
$this->storeManager = $this->objectManager->get(StoreManagerInterface::class);
73+
$this->categoryRepository = $this->objectManager->get(CategoryRepositoryInterface::class);
74+
$this->categoryFactory = $this->objectManager->get(CategoryFactory::class);
75+
$this->categoryCollectionFactory = $this->objectManager->get(CollectionFactory::class);
76+
$this->fixtures = DataFixtureStorageManager::getStorage();
77+
}
78+
79+
#[
80+
DbIsolation(true),
81+
AppIsolation(true),
82+
DataFixtureBeforeTransaction(Website::class, as: 'website2'),
83+
DataFixtureBeforeTransaction(Group::class, ['website_id' => '$website2.id$'], as:'group2'),
84+
DataFixtureBeforeTransaction(
85+
Store::class,
86+
['website_id' => '$website2.id$', 'group_id' => '$group2.id$'],
87+
as:'store2'
88+
),
89+
DataFixture(CategoryFixture::class, ['url_key' => 'default-store-category1'], as:'category1')
90+
]
91+
public function testChildrenUrlPathContainsParentCustomScopeUrlKey()
92+
{
93+
$category1 = $this->fixtures->get('category1');
94+
$secondStore = $this->fixtures->get('store2');
95+
96+
$this->storeManager->setCurrentStore($secondStore);
97+
98+
$secondStoreCategory1 = $this->categoryRepository->get($category1->getId(), $secondStore->getId());
99+
$secondStoreCategory1->setUrlKey('second-store-category-'.$category1->getId());
100+
$this->categoryRepository->save($secondStoreCategory1);
101+
102+
$this->storeManager->setCurrentStore(StoreModel::DEFAULT_STORE_ID);
103+
104+
$categoryData2 = $this->categoryFactory->create()->setData(
105+
[
106+
'parent_id' => $category1->getId(),
107+
'name' => 'Category 2',
108+
'url_key' => null,
109+
'is_active' => true
110+
]
111+
);
112+
$category2 = $this->categoryRepository->save($categoryData2);
113+
114+
$this->storeManager->setCurrentStore($secondStore);
115+
116+
$category2 = $this->categoryRepository->get($category2->getId());
117+
$category2->setUrlKey(null);
118+
$this->categoryRepository->save($category2);
119+
120+
$this->storeManager->setCurrentStore(StoreModel::DEFAULT_STORE_ID);
121+
122+
$categoryData3 = $this->categoryFactory->create()->setData(
123+
[
124+
'parent_id' => $category2->getId(),
125+
'name' => 'Category 3',
126+
'url_key' => 'default-store-category3',
127+
'is_active' => true
128+
]
129+
);
130+
$category3 = $this->categoryRepository->save($categoryData3);
131+
132+
$this->storeManager->setCurrentStore($secondStore);
133+
134+
$categories = $this->categoryCollectionFactory->create()
135+
->addAttributeToSelect('*')
136+
->setStoreId($secondStore->getId())
137+
->addFieldToFilter(
138+
'entity_id',
139+
[
140+
'in' =>
141+
[
142+
$category1->getId(),
143+
$category2->getId(),
144+
$category3->getId()
145+
]
146+
]
147+
);
148+
149+
$fullPath = [];
150+
foreach ($categories as $category) {
151+
$fullPath[] = $category->getUrlKey();
152+
}
153+
154+
$fullPath = implode('/', $fullPath) . '.html';
155+
$this->dispatch($fullPath);
156+
$response = $this->getResponse();
157+
158+
$this->assertStringContainsString($fullPath, $response->getBody());
159+
$this->assertEquals(
160+
Http::STATUS_CODE_200,
161+
$response->getHttpResponseCode(),
162+
'Response code does not match expected value'
163+
);
164+
}
165+
}

0 commit comments

Comments
 (0)