Skip to content

Commit 5d33160

Browse files
committed
Merge remote-tracking branch 'origin/2.4-develop' into Hammer-Plateform-Health-05Nov24
2 parents 0457d78 + 0ba9eef commit 5d33160

File tree

91 files changed

+4549
-952
lines changed

Some content is hidden

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

91 files changed

+4549
-952
lines changed

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

+52-44
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 2011 Adobe
4+
* All Rights Reserved.
55
*/
66
namespace Magento\Catalog\Model\ResourceModel\Category;
77

@@ -337,16 +337,63 @@ public function loadProductCount($items, $countRegular = true, $countAnchor = tr
337337
$categoryIds = array_keys($anchor);
338338
$countSelect = $this->getProductsCountQuery($categoryIds, (bool)$websiteId);
339339
$categoryProductsCount = $this->_conn->fetchPairs($countSelect);
340+
$countFromCategoryTable = $this->getCountFromCategoryTable($categoryIds, (int)$websiteId);
341+
340342
foreach ($anchor as $item) {
341-
$productsCount = isset($categoryProductsCount[$item->getId()])
342-
? (int)$categoryProductsCount[$item->getId()]
343-
: $this->getProductsCountFromCategoryTable($item, $websiteId);
343+
$productsCount = 0;
344+
if (isset($categoryProductsCount[$item->getId()])) {
345+
$productsCount = (int)$categoryProductsCount[$item->getId()];
346+
} elseif (isset($countFromCategoryTable[$item->getId()])) {
347+
$productsCount = (int)$countFromCategoryTable[$item->getId()];
348+
}
344349
$item->setProductCount($productsCount);
345350
}
346351
}
347352
return $this;
348353
}
349354

355+
/**
356+
* Get products number for each category with bulk query
357+
*
358+
* @param array $categoryIds
359+
* @param int $websiteId
360+
* @return array
361+
*/
362+
private function getCountFromCategoryTable(
363+
array $categoryIds,
364+
int $websiteId
365+
) : array {
366+
$subSelect = clone $this->_conn->select();
367+
$subSelect->from(['ce2' => $this->getTable('catalog_category_entity')], 'ce2.entity_id')
368+
->where("ce2.path LIKE CONCAT(ce.path, '/%')");
369+
370+
$select = clone $this->_conn->select();
371+
$select->from(
372+
['ce' => $this->getTable('catalog_category_entity')],
373+
'ce.entity_id'
374+
);
375+
$joinCondition = new \Zend_Db_Expr("ce.entity_id=cp.category_id OR cp.category_id IN ({$subSelect})");
376+
$select->joinLeft(
377+
['cp' => $this->getProductTable()],
378+
$joinCondition,
379+
'COUNT(DISTINCT cp.product_id) AS product_count'
380+
);
381+
if ($websiteId) {
382+
$select->join(
383+
['w' => $this->getProductWebsiteTable()],
384+
'cp.product_id = w.product_id',
385+
[]
386+
)->where(
387+
'w.website_id = ?',
388+
$websiteId
389+
);
390+
}
391+
$select->where('ce.entity_id IN(?)', $categoryIds);
392+
$select->group('ce.entity_id');
393+
394+
return $this->_conn->fetchPairs($select);
395+
}
396+
350397
/**
351398
* Add category path filter
352399
*
@@ -519,45 +566,6 @@ public function getProductTable()
519566
return $this->_productTable;
520567
}
521568

522-
/**
523-
* Get products count using catalog_category_entity table
524-
*
525-
* @param Category $item
526-
* @param string $websiteId
527-
* @return int
528-
*/
529-
private function getProductsCountFromCategoryTable(Category $item, string $websiteId): int
530-
{
531-
$productCount = 0;
532-
533-
if ($item->getAllChildren()) {
534-
$bind = ['entity_id' => $item->getId(), 'c_path' => $item->getPath() . '/%'];
535-
$select = $this->_conn->select();
536-
$select->from(
537-
['main_table' => $this->getProductTable()],
538-
new \Zend_Db_Expr('COUNT(DISTINCT main_table.product_id)')
539-
)->joinInner(
540-
['e' => $this->getTable('catalog_category_entity')],
541-
'main_table.category_id=e.entity_id',
542-
[]
543-
)->where(
544-
'(e.entity_id = :entity_id OR e.path LIKE :c_path)'
545-
);
546-
if ($websiteId) {
547-
$select->join(
548-
['w' => $this->getProductWebsiteTable()],
549-
'main_table.product_id = w.product_id',
550-
[]
551-
)->where(
552-
'w.website_id = ?',
553-
$websiteId
554-
);
555-
}
556-
$productCount = (int)$this->_conn->fetchOne($select, $bind);
557-
}
558-
return $productCount;
559-
}
560-
561569
/**
562570
* Get query for retrieve count of products per category
563571
*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
<?php
2+
/**
3+
* Copyright 2024 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Catalog\Test\Unit\Model\ResourceModel\Category;
9+
10+
use Magento\Framework\Data\Collection\EntityFactory;
11+
use Psr\Log\LoggerInterface;
12+
use Magento\Framework\Data\Collection\Db\FetchStrategyInterface;
13+
use Magento\Framework\Event\ManagerInterface;
14+
use Magento\Eav\Model\Config;
15+
use Magento\Framework\App\ResourceConnection;
16+
use Magento\Eav\Model\EntityFactory as EavEntityFactory;
17+
use Magento\Eav\Model\ResourceModel\Helper;
18+
use Magento\Framework\Validator\UniversalFactory;
19+
use Magento\Store\Model\StoreManagerInterface;
20+
use Magento\Store\Api\Data\StoreInterface;
21+
use Magento\Framework\DB\Adapter\AdapterInterface;
22+
use Magento\Framework\DB\Select;
23+
use Magento\Framework\App\Config\ScopeConfigInterface;
24+
use Magento\Catalog\Model\Product\Visibility;
25+
use Magento\Catalog\Model\ResourceModel\Category\Collection;
26+
use Magento\Catalog\Model\ResourceModel\Category as CategoryEntity;
27+
use PHPUnit\Framework\MockObject\MockObject;
28+
use PHPUnit\Framework\TestCase;
29+
30+
/**
31+
* @SuppressWarnings(PHPMD.TooManyFields)
32+
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
33+
*/
34+
class CollectionTest extends TestCase
35+
{
36+
37+
/**
38+
* @var Collection
39+
*/
40+
private $collection;
41+
42+
/**
43+
* @var EntityFactory|MockObject
44+
*/
45+
private $entityFactory;
46+
47+
/**
48+
* @var LoggerInterface|MockObject
49+
*/
50+
private $logger;
51+
52+
/**
53+
* @var FetchStrategyInterface|MockObject
54+
*/
55+
private $fetchStrategy;
56+
57+
/**
58+
* @var ManagerInterface|MockObject
59+
*/
60+
private $eventManager;
61+
62+
/**
63+
* @var Config|MockObject
64+
*/
65+
private $eavConfig;
66+
67+
/**
68+
* @var ResourceConnection|MockObject
69+
*/
70+
private $resource;
71+
72+
/**
73+
* @var EavEntityFactory|MockObject
74+
*/
75+
private $eavEntityFactory;
76+
77+
/**
78+
* @var Helper|MockObject
79+
*/
80+
private $resourceHelper;
81+
82+
/**
83+
* @var UniversalFactory|MockObject
84+
*/
85+
private $universalFactory;
86+
87+
/**
88+
* @var StoreManagerInterface|MockObject
89+
*/
90+
private $storeManager;
91+
92+
/**
93+
* @var AdapterInterface|MockObject
94+
*/
95+
private $connection;
96+
97+
/**
98+
* @var ScopeConfigInterface|MockObject
99+
*/
100+
private $scopeConfig;
101+
102+
/**
103+
* @var Visibility|MockObject
104+
*/
105+
private $catalogProductVisibility;
106+
107+
/**
108+
* @var CategoryEntity|MockObject
109+
*/
110+
private $categoryEntity;
111+
112+
/**
113+
* @var Select|MockObject
114+
*/
115+
private $select;
116+
117+
/**
118+
* @var StoreInterface|MockObject
119+
*/
120+
private $store;
121+
122+
/**
123+
* {@inheritdoc}
124+
*/
125+
public function setUp(): void
126+
{
127+
$this->entityFactory = $this->getMockBuilder(EntityFactory::class)
128+
->disableOriginalConstructor(true)
129+
->getMock();
130+
$this->logger = $this->getMockBuilder(LoggerInterface::class)
131+
->getMock();
132+
$this->fetchStrategy = $this->getMockBuilder(FetchStrategyInterface::class)
133+
->getMock();
134+
$this->eventManager = $this->getMockBuilder(ManagerInterface::class)
135+
->getMock();
136+
$this->eavConfig = $this->getMockBuilder(Config::class)
137+
->disableOriginalConstructor(true)
138+
->getMock();
139+
$this->resource = $this->getMockBuilder(ResourceConnection::class)
140+
->disableOriginalConstructor(true)
141+
->getMock();
142+
$this->eavEntityFactory = $this->getMockBuilder(EavEntityFactory::class)
143+
->disableOriginalConstructor(true)
144+
->getMock();
145+
$this->resourceHelper = $this->getMockBuilder(Helper::class)
146+
->disableOriginalConstructor(true)
147+
->getMock();
148+
$this->universalFactory = $this->getMockBuilder(UniversalFactory::class)
149+
->disableOriginalConstructor(true)
150+
->getMock();
151+
$this->storeManager = $this->getMockBuilder(StoreManagerInterface::class)
152+
->getMock();
153+
$this->connection = $this->getMockBuilder(AdapterInterface::class)
154+
->getMock();
155+
$this->scopeConfig = $this->getMockBuilder(ScopeConfigInterface::class)
156+
->getMock();
157+
$this->catalogProductVisibility = $this->getMockBuilder(Visibility::class)
158+
->disableOriginalConstructor(true)
159+
->getMock();
160+
161+
$this->categoryEntity = $this->getMockBuilder(CategoryEntity::class)
162+
->disableOriginalConstructor()
163+
->getMock();
164+
$this->universalFactory->expects($this->any())
165+
->method('create')
166+
->willReturn($this->categoryEntity);
167+
$this->categoryEntity->expects($this->any())
168+
->method('getConnection')
169+
->willReturn($this->connection);
170+
$this->categoryEntity->expects($this->any())
171+
->method('getDefaultAttributes')
172+
->willReturn([]);
173+
174+
$this->select = $this->getMockBuilder(Select::class)
175+
->disableOriginalConstructor()
176+
->getMock();
177+
$this->connection->expects($this->any())
178+
->method('select')
179+
->willReturn($this->select);
180+
181+
$this->store = $this->getMockBuilder(StoreInterface::class)
182+
->getMock();
183+
$this->storeManager->expects($this->any())
184+
->method('getStore')
185+
->willReturn($this->store);
186+
187+
$this->collection = new Collection(
188+
$this->entityFactory,
189+
$this->logger,
190+
$this->fetchStrategy,
191+
$this->eventManager,
192+
$this->eavConfig,
193+
$this->resource,
194+
$this->eavEntityFactory,
195+
$this->resourceHelper,
196+
$this->universalFactory,
197+
$this->storeManager,
198+
$this->connection,
199+
$this->scopeConfig,
200+
$this->catalogProductVisibility
201+
);
202+
}
203+
204+
public function testLoadProductCount() : void
205+
{
206+
$this->select->expects($this->exactly(3))
207+
->method('from')
208+
->willReturnSelf();
209+
$this->select->expects($this->exactly(3))
210+
->method('where')
211+
->willReturnSelf();
212+
$this->select->expects($this->exactly(1))
213+
->method('group')
214+
->willReturnSelf();
215+
$this->connection->expects($this->exactly(2))
216+
->method('fetchPairs')
217+
->with($this->select)
218+
->willReturn([]);
219+
$this->collection->loadProductCount([]);
220+
}
221+
}

app/code/Magento/Catalog/etc/di.xml

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
<?xml version="1.0"?>
22
<!--
33
/**
4-
* Copyright © Magento, Inc. All rights reserved.
5-
* See COPYING.txt for license details.
4+
* Copyright 2013 Adobe
5+
* All Rights Reserved.
66
*/
77
-->
88
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
@@ -73,7 +73,6 @@
7373
<preference for="Magento\Catalog\Model\Indexer\Product\Price\UpdateIndexInterface" type="Magento\Catalog\Model\Indexer\Product\Price\InvalidateIndex" />
7474
<preference for="Magento\Catalog\Model\Product\Gallery\ImagesConfigFactoryInterface" type="Magento\Catalog\Model\Product\Gallery\ImagesConfigFactory" />
7575
<preference for="Magento\Catalog\Model\Product\Configuration\Item\ItemResolverInterface" type="Magento\Catalog\Model\Product\Configuration\Item\ItemResolverComposite" />
76-
<preference for="Magento\Catalog\Api\Data\MassActionInterface" type="Magento\Catalog\Model\MassAction" />
7776
<preference for="Magento\Catalog\Model\ProductLink\Data\ListCriteriaInterface" type="Magento\Catalog\Model\ProductLink\Data\ListCriteria" />
7877
<preference for="Magento\Catalog\Api\CategoryListDeleteBySkuInterface" type="Magento\Catalog\Model\CategoryLinkRepository"/>
7978
<preference for="Magento\Theme\CustomerData\MessagesProviderInterface" type="Magento\Catalog\Model\Theme\CustomerData\MessagesProvider"/>

0 commit comments

Comments
 (0)