Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a9f2d5e
Fix category product count not including products assigned to the cat…
Nov 17, 2025
f28ff95
Merge branch '2.4-develop' into fix/category-product-count-missing-se…
mohaelmrabet Nov 17, 2025
10a5204
Merge branch '2.4-develop' into fix/category-product-count-missing-se…
mohaelmrabet Nov 19, 2025
93225de
Merge branch '2.4-develop' into fix/category-product-count-missing-se…
mohaelmrabet Nov 19, 2025
dcb78fe
Merge branch '2.4-develop' into fix/category-product-count-missing-se…
mohaelmrabet Nov 20, 2025
58c7e59
Merge branch '2.4-develop' into fix/category-product-count-missing-se…
mohaelmrabet Nov 20, 2025
15db49d
PR review fix
Nov 20, 2025
015bce6
Updating unit test
Nov 20, 2025
4109db7
Adding integration test
Nov 21, 2025
d518ce9
cs code
Nov 21, 2025
a0d86b2
cs code
Nov 21, 2025
df7b608
Optimize the test
Nov 21, 2025
801e996
Merge branch '2.4-develop' into fix/category-product-count-missing-se…
mohaelmrabet Nov 21, 2025
6c3dc6f
fix static check
Nov 21, 2025
9e5ead3
fixing error fixture
Nov 21, 2025
bb6eccc
fixing error fixture
Nov 21, 2025
4fad9fb
fix static issue
Nov 22, 2025
63b9932
fix static issue
Nov 22, 2025
d5f27ef
Merge branch '2.4-develop' into fix/category-product-count-missing-se…
mohaelmrabet Nov 23, 2025
625881e
Remove the BULK_PROCESSING_LIMIT
Nov 23, 2025
b298337
Merge branch '2.4-develop' into fix/category-product-count-missing-se…
mohaelmrabet Nov 24, 2025
4d90a39
fix issue check
Nov 25, 2025
2d0bf92
Merge branch 'fix/category-product-count-missing-self-reference-40263…
Nov 25, 2025
5c1aaa7
Merge branch '2.4-develop' into fix/category-product-count-missing-se…
mohaelmrabet Nov 26, 2025
6384f1b
Merge branch '2.4-develop' into fix/category-product-count-missing-se…
mohaelmrabet Nov 28, 2025
44e7e48
Merge branch '2.4-develop' into fix/category-product-count-missing-se…
mohaelmrabet Nov 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -412,14 +412,21 @@ private function getCountFromCategoryTableBulk(
[]
)
->where('ce.entity_id IN (?)', $categoryIds);

$connection->query(
$connection->insertFromSelect(
$selectDescendants,
$tempTableName,
['category_id', 'descendant_id']
)
);
$data = [];
foreach ($categoryIds as $catId) {
$data[] = [
'category_id' => $catId,
'descendant_id' => $catId
];
}
$connection->insertMultiple($tempTableName, $data);
$select = $connection->select()
->from(
['t' => $tempTableName],
Expand Down
27 changes: 27 additions & 0 deletions app/code/Magento/Catalog/Test/Unit/Helper/CategoryTestHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php
/**
* Copyright 2025 Adobe
* All Rights Reserved.
*/
declare(strict_types=1);

namespace Magento\Catalog\Test\Unit\Helper;

use Magento\Catalog\Model\Category;

/**
* Test helper class for Catalog Category with custom methods
*/
class CategoryTestHelper extends Category
{

/**
* Get is anchor
*
* @return bool
*/
public function getIsAnchor(): bool
{
return $this->getData('is_anchor');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,27 @@

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

use Magento\Catalog\Model\Category;
use Magento\Framework\Data\Collection\EntityFactory;
use Magento\Store\Model\Store;
use Psr\Log\LoggerInterface;
use Magento\Framework\Data\Collection\Db\FetchStrategyInterface;
use Magento\Framework\Event\ManagerInterface;
use Magento\Catalog\Model\Product\Visibility;
use Magento\Catalog\Model\ResourceModel\Category as CategoryEntity;
use Magento\Catalog\Model\ResourceModel\Category\Collection;
use Magento\Catalog\Test\Unit\Helper\CategoryTestHelper;
use Magento\Eav\Model\Config;
use Magento\Framework\App\ResourceConnection;
use Magento\Eav\Model\EntityFactory as EavEntityFactory;
use Magento\Eav\Model\ResourceModel\Helper;
use Magento\Framework\Validator\UniversalFactory;
use Magento\Store\Model\StoreManagerInterface;
use Magento\Store\Api\Data\StoreInterface;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\App\ResourceConnection;
use Magento\Framework\Data\Collection\Db\FetchStrategyInterface;
use Magento\Framework\Data\Collection\EntityFactory;
use Magento\Framework\DB\Adapter\AdapterInterface;
use Magento\Framework\DB\Select;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Catalog\Model\Product\Visibility;
use Magento\Catalog\Model\ResourceModel\Category\Collection;
use Magento\Catalog\Model\ResourceModel\Category as CategoryEntity;
use Magento\Framework\Event\ManagerInterface;
use Magento\Framework\Validator\UniversalFactory;
use Magento\Store\Api\Data\StoreInterface;
use Magento\Store\Model\Store;
use Magento\Store\Model\StoreManagerInterface;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;

/**
* @SuppressWarnings(PHPMD.TooManyFields)
Expand Down Expand Up @@ -229,11 +229,7 @@ public function testLoadProductCountCallsBulkMethodForLargeCategoryCount()
$items = [];
$categoryIds = [];
for ($i = 1; $i <= $categoryCount; $i++) {
$category = $this->getMockBuilder(Category::class)
->addMethods(['getIsAnchor'])
->onlyMethods(['getId', 'setProductCount'])
->disableOriginalConstructor()
->getMock();
$category = $this->createMock(CategoryTestHelper::class);
$category->method('getId')->willReturn($i);
$category->method('getIsAnchor')->willReturn(true);
$category->expects($this->once())->method('setProductCount')->with(5);
Expand Down Expand Up @@ -265,6 +261,20 @@ public function testLoadProductCountCallsBulkMethodForLargeCategoryCount()
$this->connection->method('select')->willReturn($this->select);
$this->connection->method('insertFromSelect')->willReturn('INSERT QUERY');
$this->connection->method('query')->with('INSERT QUERY')->willReturnSelf();
$withs = [];
foreach ($categoryIds as $categoryId) {
$withs[] = [
'category_id' => $categoryId,
'descendant_id' => $categoryId
];
}
$this->connection
->expects($this->once())
->method('insertMultiple')
->with(
$this->stringContains('temp_category_descendants_'),
$withs
);
$this->select->method('from')->willReturnSelf();
$this->select->method('joinLeft')->willReturnSelf();
$this->select->method('join')->willReturnSelf();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,35 +1,38 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
* Copyright 2018 Adobe
* All Rights Reserved.
*/
declare(strict_types=1);

namespace Magento\Catalog\Model\ResourceModel\Category;

use Magento\Store\Model\StoreManagerInterface;
use Magento\TestFramework\Helper\Bootstrap;
use Magento\Catalog\Model\ResourceModel\Category\Collection as CategoryCollection;
use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory;

class CollectionTest extends \PHPUnit\Framework\TestCase
{
/**
* @var \Magento\Catalog\Model\ResourceModel\Category\Collection
*/
private $collection;
private Collection $collection;
private CollectionFactory $categoryCollectionFactory;

/**
* Sets up the fixture, for example, opens a network connection.
* This method is called before a test is executed.
*/
protected function setUp(): void
{
$this->collection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(
\Magento\Catalog\Model\ResourceModel\Category\Collection::class
);
$objectManager = Bootstrap::getObjectManager();
$this->collection = Bootstrap::getObjectManager()->create(Collection::class);
$this->categoryCollectionFactory = $objectManager->get(CollectionFactory::class);
}

protected function setDown()
protected function tearDown(): void
{
/* Refresh stores memory cache after store deletion */
\Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(
\Magento\Store\Model\StoreManagerInterface::class
Bootstrap::getObjectManager()->get(
StoreManagerInterface::class
)->reinitStores();
}

Expand All @@ -54,7 +57,7 @@ public function testJoinUrlRewriteOnDefault()
*/
public function testJoinUrlRewriteNotOnDefaultStore()
{
$store = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()
$store = Bootstrap::getObjectManager()
->create(\Magento\Store\Model\Store::class);
$storeId = $store->load('second_category_store', 'code')->getId();
$categories = $this->collection->setStoreId($storeId)->joinUrlRewrite()->addPathFilter('1/2/3');
Expand All @@ -63,4 +66,39 @@ public function testJoinUrlRewriteNotOnDefaultStore()
$category = $categories->getFirstItem();
$this->assertStringEndsWith('category-3-on-2.html', $category->getUrl());
}

/**
* @magentoAppIsolation enabled
* @magentoDbIsolation enabled
* @magentoAppArea adminhtml
* @magentoDataFixture Magento/Catalog/Model/ResourceModel/_files/categories_with_products_large.php
*/
public function testBulkProcessingModeIsTriggered()
{
/** @var CategoryCollection $collection */
$collection = $this->categoryCollectionFactory->create();
$collection->addAttributeToSelect('*');
$collection->addAttributeToFilter('name', ['like' => 'bulk_test_123%']);
$collection->setLoadProductCount(true);
$collection->load();

$this->assertGreaterThan(
400,
$collection->getSize(),
'Bulk limit path not triggered.'
);

foreach ($collection as $category) {
$productCount = $category->getProductCount();
$this->assertNotNull(
$productCount,
'ProductCount missing for category ' . $category->getId()
);
$this->assertGreaterThan(
0,
$productCount,
sprintf('Invalid product count for category %d.', $category->getId())
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
<?php
/**
* Copyright 2025 Adobe
* All Rights Reserved.
*/
declare(strict_types=1);

use Magento\Catalog\Api\CategoryRepositoryInterface;
use Magento\Catalog\Model\ResourceModel\Product as ProductResource;
use Magento\Framework\App\ResourceConnection;
use Magento\TestFramework\Helper\Bootstrap;
use Magento\Catalog\Model\CategoryFactory;
use Magento\Catalog\Model\ProductFactory;
use Magento\Store\Model\StoreManagerInterface;

$om = Bootstrap::getObjectManager();

$storeManager = $om->get(StoreManagerInterface::class);
$rootCategoryId = (int)$storeManager->getStore()->getRootCategoryId();

$categoryFactory = $om->get(CategoryFactory::class);
$categoryRepository = $om->get(CategoryRepositoryInterface::class);
$productFactory = $om->get(ProductFactory::class);
$productResource = $om->get(ProductResource::class);

$resource = $om->get(ResourceConnection::class);
$conn = $resource->getConnection();
$table = $resource->getTableName('catalog_category_product');

$identifier = 'bulk_test_123';
$categoriesCount = 401;

/* products */
$products = [];
$totalProducts = 5;

for ($i = 1; $i <= $totalProducts; $i++) {
$p = $productFactory->create();
$p->setTypeId('simple')
->setAttributeSetId(4)
->setSku("{$identifier}_prd_{$i}")
->setName("Bulk Test Product {$i}")
->setPrice(10 + $i)
->setVisibility(4)
->setStatus(1)
->setStockData(['qty' => 10, 'is_in_stock' => 1]);

$productResource->save($p);
$products[] = $p->getId();
}

/* category generator + inline product assignment */
$createCategory = function(string $name, int $parentId, bool $isLeaf = false)
use ($categoryFactory, $categoryRepository, $products, $conn, $table) {

$cat = $categoryFactory->create();
$cat->setName($name)
->setIsActive(true)
->setIsAnchor(1)
->setParentId($parentId);

$categoryRepository->save($cat);
$catId = (int)$cat->getId();

if ($isLeaf) {
$count = random_int(1, 5);
$sel = [];

for ($i = 0; $i < $count; $i++) {
$sel[] = $products[random_int(0, count($products) - 1)];
}

$sel = array_unique($sel);
$rows = [];

foreach ($sel as $pid) {
$rows[] = [
'category_id' => $catId,
'product_id' => $pid,
'position' => 0
];
}

if ($rows) {
$conn->insertMultiple($table, $rows);
}
}

return $catId;
};

$leafCategories = [];
$parentCategories = [];

/* level 1 */
$level1 = array_map(
fn($i) => $createCategory("{$identifier}_l1_{$i}", $rootCategoryId),
range(1, 4)
);
$parentCategories = array_merge($parentCategories, $level1);

$lastL1 = end($level1);

foreach (range(1, 10) as $i) {
$leafCategories[] = $createCategory("{$identifier}_l1_leaf_{$i}", $lastL1, true);
}

/* level 2 */
$level2 = [];

foreach ($level1 as $parentId) {

$createdThisLoop = [];

foreach (range(1, 13) as $i) {
$createdThisLoop[] = $createCategory("{$identifier}_l2_{$parentId}_{$i}", $parentId);
}

$level2 = array_merge($level2, $createdThisLoop);
$parentCategories = array_merge($parentCategories, $createdThisLoop);

foreach (range(1, 5) as $i) {
$leafCategories[] = $createCategory("{$identifier}_l2_{$parentId}_leaf_{$i}", $parentId, true);
}
}

/* level 3 leafs */
foreach ($level2 as $parentId) {
$leafCategories[] = $createCategory("{$identifier}_l3_{$parentId}_leaf", $parentId, true);
}

/* extend up to target count */
$totalCreated = count($parentCategories) + count($leafCategories);
$missing = max(0, $categoriesCount - $totalCreated);

for ($i = 1; $i <= $missing; $i++) {
$parentId = $parentCategories[random_int(0, count($parentCategories) - 1)];
$leafCategories[] = $createCategory("{$identifier}_extra_{$i}", $parentId, true);
}