Skip to content

Commit 1dd16bc

Browse files
committed
Merge remote-tracking branch 'commerce/2.4-develop' into ACP2E-3786
2 parents 880abef + 4f82be7 commit 1dd16bc

File tree

126 files changed

+21706
-20792
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

126 files changed

+21706
-20792
lines changed

app/code/Magento/Backend/Test/Mftf/Data/BackenedData.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<!--
33
/**
4-
* Copyright 2018 Adobe
4+
* Copyright 2025 Adobe
55
* All Rights Reserved.
66
*/
77
-->

app/code/Magento/Catalog/Helper/Category.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@
1010
use Magento\Catalog\Model\CategoryFactory;
1111
use Magento\Framework\App\Helper\AbstractHelper;
1212
use Magento\Framework\App\Helper\Context;
13+
use Magento\Framework\App\ObjectManager;
1314
use Magento\Framework\Data\CollectionFactory;
1415
use Magento\Framework\Data\Tree\Node\Collection;
16+
use Magento\Framework\Escaper;
1517
use Magento\Framework\Exception\NoSuchEntityException;
1618
use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
1719
use Magento\Store\Model\ScopeInterface;
@@ -63,24 +65,33 @@ class Category extends AbstractHelper implements ResetAfterRequestInterface
6365
*/
6466
protected $categoryRepository;
6567

68+
/**
69+
* @var Escaper|null
70+
*/
71+
private ?Escaper $escaper;
72+
6673
/**
6774
* @param Context $context
6875
* @param CategoryFactory $categoryFactory
6976
* @param StoreManagerInterface $storeManager
7077
* @param CollectionFactory $dataCollectionFactory
7178
* @param CategoryRepositoryInterface $categoryRepository
79+
* @param Escaper|null $escaper
7280
*/
7381
public function __construct(
7482
Context $context,
7583
CategoryFactory $categoryFactory,
7684
StoreManagerInterface $storeManager,
7785
CollectionFactory $dataCollectionFactory,
78-
CategoryRepositoryInterface $categoryRepository
86+
CategoryRepositoryInterface $categoryRepository,
87+
?Escaper $escaper = null
7988
) {
8089
$this->_categoryFactory = $categoryFactory;
8190
$this->_storeManager = $storeManager;
8291
$this->_dataCollectionFactory = $dataCollectionFactory;
8392
$this->categoryRepository = $categoryRepository;
93+
$this->escaper = $escaper ?: ObjectManager::getInstance()->get(Escaper::class);
94+
8495
parent::__construct($context);
8596
}
8697

@@ -204,6 +215,7 @@ public function getCanonicalUrl(string $categoryUrl): string
204215
if ($params && isset($params['p'])) {
205216
$categoryUrl = $categoryUrl . '?p=' . $params['p'];
206217
}
207-
return $categoryUrl;
218+
219+
return $this->escaper->escapeUrl($categoryUrl);
208220
}
209221
}

app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php

Lines changed: 63 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
use Magento\Framework\App\Config\ScopeConfigInterface;
1212
use Magento\Framework\DB\Select;
1313
use Magento\Store\Model\ScopeInterface;
14+
use Magento\Framework\DB\Adapter\AdapterInterface;
15+
use Magento\Framework\DB\Ddl\Table;
1416

1517
/**
1618
* Category resource collection
@@ -370,40 +372,80 @@ public function loadProductCount($items, $countRegular = true, $countAnchor = tr
370372
* @param array $categoryIds
371373
* @param int $websiteId
372374
* @return array
375+
* @throws \Zend_Db_Exception
373376
*/
374377
private function getCountFromCategoryTableBulk(
375378
array $categoryIds,
376379
int $websiteId
377380
) : array {
378-
$subSelect = clone $this->_conn->select();
379-
$subSelect->from(['ce2' => $this->getTable('catalog_category_entity')], 'ce2.entity_id')
380-
->where("ce2.path LIKE CONCAT(ce.path, '/%') OR ce2.path = ce.path");
381-
382-
$select = clone $this->_conn->select();
383-
$select->from(
384-
['ce' => $this->getTable('catalog_category_entity')],
385-
'ce.entity_id'
386-
);
387-
$joinCondition = new \Zend_Db_Expr("cp.category_id IN ({$subSelect})");
388-
$select->joinLeft(
389-
['cp' => $this->getProductTable()],
390-
$joinCondition,
391-
'COUNT(DISTINCT cp.product_id) AS product_count'
381+
$connection = $this->_conn;
382+
$tempTableName = 'temp_category_descendants_' . uniqid();
383+
$tempTable = $connection->newTable($tempTableName)
384+
->addColumn(
385+
'category_id',
386+
Table::TYPE_INTEGER,
387+
null,
388+
['unsigned' => true, 'nullable' => false],
389+
'Category ID'
390+
)
391+
->addColumn(
392+
'descendant_id',
393+
Table::TYPE_INTEGER,
394+
null,
395+
['unsigned' => true, 'nullable' => false],
396+
'Descendant ID'
397+
)
398+
->addIndex(
399+
$connection->getIndexName($tempTableName, ['category_id', 'descendant_id']),
400+
['category_id', 'descendant_id'],
401+
['type' => AdapterInterface::INDEX_TYPE_PRIMARY]
402+
);
403+
$connection->createTemporaryTable($tempTable);
404+
$selectDescendants = $connection->select()
405+
->from(
406+
['ce' => $this->getTable('catalog_category_entity')],
407+
['category_id' => 'ce.entity_id', 'descendant_id' => 'ce2.entity_id']
408+
)
409+
->joinInner(
410+
['ce2' => $this->getTable('catalog_category_entity')],
411+
'ce2.path LIKE CONCAT(ce.path, \'/%\') OR ce2.entity_id = ce.entity_id',
412+
[]
413+
)
414+
->where('ce.entity_id IN (?)', $categoryIds);
415+
416+
$connection->query(
417+
$connection->insertFromSelect(
418+
$selectDescendants,
419+
$tempTableName,
420+
['category_id', 'descendant_id']
421+
)
392422
);
423+
$select = $connection->select()
424+
->from(
425+
['t' => $tempTableName],
426+
['category_id' => 't.category_id']
427+
)
428+
->joinLeft(
429+
['cp' => $this->getTable('catalog_category_product')],
430+
'cp.category_id = t.descendant_id',
431+
['product_count' => 'COUNT(DISTINCT cp.product_id)']
432+
);
393433
if ($websiteId) {
394434
$select->join(
395435
['w' => $this->getProductWebsiteTable()],
396436
'cp.product_id = w.product_id',
397437
[]
398-
)->where(
399-
'w.website_id = ?',
400-
$websiteId
401-
);
438+
)->where('w.website_id = ?', $websiteId);
439+
}
440+
$select->group('t.category_id');
441+
$result = $connection->fetchPairs($select);
442+
$connection->dropTemporaryTable($tempTableName);
443+
$counts = array_fill_keys($categoryIds, 0);
444+
foreach ($result as $categoryId => $count) {
445+
$counts[$categoryId] = (int)$count;
402446
}
403-
$select->where('ce.entity_id IN(?)', $categoryIds);
404-
$select->group('ce.entity_id');
405447

406-
return $this->_conn->fetchPairs($select);
448+
return $counts;
407449
}
408450

409451
/**

app/code/Magento/Catalog/Test/Unit/Helper/CategoryTest.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Magento\Framework\App\Helper\Context;
1515
use Magento\Framework\App\RequestInterface;
1616
use Magento\Framework\Data\CollectionFactory;
17+
use Magento\Framework\Escaper;
1718
use Magento\Store\Model\StoreManagerInterface;
1819
use PHPUnit\Framework\MockObject\MockObject;
1920
use PHPUnit\Framework\TestCase;
@@ -63,6 +64,11 @@ class CategoryTest extends TestCase
6364
*/
6465
private $requestMock;
6566

67+
/**
68+
* @var Escaper|MockObject
69+
*/
70+
private $escaper;
71+
6672
protected function setUp(): void
6773
{
6874
$this->mockContext();
@@ -78,12 +84,16 @@ protected function setUp(): void
7884
$this->categoryRepository = $this->getMockBuilder(CategoryRepositoryInterface::class)
7985
->disableOriginalConstructor()
8086
->getMock();
87+
$this->escaper = $this->getMockBuilder(Escaper::class)
88+
->disableOriginalConstructor()
89+
->getMock();
8190
$this->categoryHelper = new Category(
8291
$this->context,
8392
$this->categoryFactory,
8493
$this->storeManager,
8594
$this->collectionFactory,
86-
$this->categoryRepository
95+
$this->categoryRepository,
96+
$this->escaper
8797
);
8898
}
8999

@@ -101,6 +111,9 @@ public function testGetCanonicalUrl(mixed $params, string $categoryUrl, string $
101111
$this->requestMock->expects($this->any())
102112
->method('getParams')
103113
->willReturn($params);
114+
$this->escaper->expects($this->any())
115+
->method('escapeUrl')
116+
->willReturn($expectedCategoryUrl);
104117
$actualCategoryUrl = $this->categoryHelper->getCanonicalUrl($categoryUrl);
105118
$this->assertEquals($actualCategoryUrl, $expectedCategoryUrl);
106119
}

app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Category/CollectionTest.php

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77

88
namespace Magento\Catalog\Test\Unit\Model\ResourceModel\Category;
99

10+
use Magento\Catalog\Model\Category;
1011
use Magento\Framework\Data\Collection\EntityFactory;
12+
use Magento\Store\Model\Store;
1113
use Psr\Log\LoggerInterface;
1214
use Magento\Framework\Data\Collection\Db\FetchStrategyInterface;
1315
use Magento\Framework\Event\ManagerInterface;
@@ -215,4 +217,58 @@ public function testLoadProductCount() : void
215217
->willReturn([]);
216218
$this->collection->loadProductCount([]);
217219
}
220+
221+
/**
222+
* Test that loadProductCount calls getCountFromCategoryTableBulk
223+
*/
224+
public function testLoadProductCountCallsBulkMethodForLargeCategoryCount()
225+
{
226+
$websiteId = 1;
227+
$storeId = 1;
228+
$categoryCount = 401;
229+
$items = [];
230+
$categoryIds = [];
231+
for ($i = 1; $i <= $categoryCount; $i++) {
232+
$category = $this->getMockBuilder(Category::class)
233+
->addMethods(['getIsAnchor'])
234+
->onlyMethods(['getId', 'setProductCount'])
235+
->disableOriginalConstructor()
236+
->getMock();
237+
$category->method('getId')->willReturn($i);
238+
$category->method('getIsAnchor')->willReturn(true);
239+
$category->expects($this->once())->method('setProductCount')->with(5);
240+
$items[$i] = $category;
241+
$categoryIds[] = $i;
242+
}
243+
$storeMock = $this->createMock(Store::class);
244+
$storeMock->method('getWebsiteId')->willReturn($websiteId);
245+
$this->storeManager->method('getStore')->with($storeId)->willReturn($storeMock);
246+
$this->connection->method('select')->willReturn($this->select);
247+
$counts = array_fill_keys($categoryIds, 5);
248+
$tableMock = $this->createMock(\Magento\Framework\DB\Ddl\Table::class);
249+
$tableMock->method('addColumn')->willReturnSelf();
250+
$tableMock->method('addIndex')->willReturnSelf();
251+
$this->connection->method('newTable')
252+
->with($this->stringContains('temp_category_descendants_'))
253+
->willReturn($tableMock);
254+
$this->connection->expects($this->once())->method('createTemporaryTable')->with($tableMock);
255+
$this->connection->expects($this->once())->method('dropTemporaryTable')
256+
->with($this->stringContains('temp_category_descendants_'));
257+
$this->select->method('from')->willReturnSelf();
258+
$this->select->method('joinInner')->willReturnSelf();
259+
$this->select->method('where')->willReturnSelf();
260+
$this->connection->method('select')->willReturn($this->select);
261+
$this->connection->method('insertFromSelect')->willReturn('INSERT QUERY');
262+
$this->connection->method('query')->with('INSERT QUERY')->willReturnSelf();
263+
$this->select->method('from')->willReturnSelf();
264+
$this->select->method('joinLeft')->willReturnSelf();
265+
$this->select->method('join')->willReturnSelf();
266+
$this->select->method('where')->willReturnSelf();
267+
$this->select->method('group')->willReturnSelf();
268+
$this->connection->method('fetchPairs')
269+
->with($this->isInstanceOf(Select::class))
270+
->willReturn($counts);
271+
$this->collection->setProductStoreId($storeId);
272+
$this->collection->loadProductCount($items, false, true);
273+
}
218274
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Catalog\ViewModel\Attribute;
9+
10+
use Magento\Framework\View\Element\Block\ArgumentInterface;
11+
use Magento\Catalog\Helper\Data as CatalogHelper;
12+
13+
class ProductAttributeHelper implements ArgumentInterface
14+
{
15+
/**
16+
* @var CatalogHelper
17+
*/
18+
private CatalogHelper $catalogHelper;
19+
20+
/**
21+
* @param CatalogHelper $catalogHelper
22+
*/
23+
public function __construct(CatalogHelper $catalogHelper)
24+
{
25+
$this->catalogHelper = $catalogHelper;
26+
}
27+
28+
/**
29+
* Retrieve the Catalog Helper instance
30+
*
31+
* @return CatalogHelper
32+
*/
33+
public function getCatalogHelper(): CatalogHelper
34+
{
35+
return $this->catalogHelper;
36+
}
37+
}

app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_attribute_edit.xml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?xml version="1.0"?>
22
<!--
33
/**
4-
* Copyright 2013 Adobe
4+
* Copyright 2014 Adobe
55
* All Rights Reserved.
66
*/
77
-->
@@ -22,7 +22,11 @@
2222
<block class="Magento\Catalog\Block\Adminhtml\Product\Attribute\Edit" name="attribute_edit_content"/>
2323
</referenceContainer>
2424
<referenceContainer name="js">
25-
<block class="Magento\Backend\Block\Template" name="attribute_edit_js" template="Magento_Catalog::catalog/product/attribute/js.phtml"/>
25+
<block class="Magento\Backend\Block\Template" name="attribute_edit_js" template="Magento_Catalog::catalog/product/attribute/js.phtml">
26+
<arguments>
27+
<argument name="view_model" xsi:type="object">Magento\Catalog\ViewModel\Attribute\ProductAttributeHelper</argument>
28+
</arguments>
29+
</block>
2630
</referenceContainer>
2731
</body>
2832
</page>

app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/js.phtml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ use Magento\Catalog\Helper\Data;
1313
<?php
1414
/** @var \Magento\Framework\Json\Helper\Data $jsonHelper */
1515
$jsonHelper = $block->getData('jsonHelper');
16+
if (!method_exists($jsonHelper, 'getAttributeHiddenFields')) {
17+
/** @var \Magento\Catalog\ViewModel\Attribute\ProductAttributeHelper $viewModel */
18+
$viewModel = $block->getViewModel();
19+
$jsonHelper = $viewModel->getCatalogHelper();
20+
}
1621
$scriptString = <<<script
1722
require([
1823
"jquery",

0 commit comments

Comments
 (0)