From a9f2d5e1d8c103ba83fa6a446b26ceaf66fa23f1 Mon Sep 17 00:00:00 2001 From: Mohamed El Mrabet Date: Mon, 17 Nov 2025 10:39:25 +0100 Subject: [PATCH 01/14] Fix category product count not including products assigned to the category itself --- .../ResourceModel/Category/Collection.php | 10 ++++++- .../Test/Unit/Helper/CategoryTestHelper.php | 30 +++++++++++++++++++ .../ResourceModel/Category/CollectionTest.php | 27 +++++++++++++---- 3 files changed, 61 insertions(+), 6 deletions(-) create mode 100644 app/code/Magento/Catalog/Test/Unit/Helper/CategoryTestHelper.php diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php index 9aa073ceacb85..d15116bd50fc9 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php @@ -412,7 +412,6 @@ private function getCountFromCategoryTableBulk( [] ) ->where('ce.entity_id IN (?)', $categoryIds); - $connection->query( $connection->insertFromSelect( $selectDescendants, @@ -420,6 +419,15 @@ private function getCountFromCategoryTableBulk( ['category_id', 'descendant_id'] ) ); + foreach ($categoryIds as $catId) { + $connection->insert( + $tempTableName, + [ + 'category_id' => $catId, + 'descendant_id' => $catId + ] + ); + } $select = $connection->select() ->from( ['t' => $tempTableName], diff --git a/app/code/Magento/Catalog/Test/Unit/Helper/CategoryTestHelper.php b/app/code/Magento/Catalog/Test/Unit/Helper/CategoryTestHelper.php new file mode 100644 index 0000000000000..08743b3959893 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Helper/CategoryTestHelper.php @@ -0,0 +1,30 @@ +getData('is_anchor'); + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Category/CollectionTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Category/CollectionTest.php index f29bfedc48511..d70704871e469 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Category/CollectionTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Category/CollectionTest.php @@ -8,6 +8,7 @@ namespace Magento\Catalog\Test\Unit\Model\ResourceModel\Category; use Magento\Catalog\Model\Category; +use Magento\Catalog\Test\Unit\Helper\CategoryTestHelper; use Magento\Framework\Data\Collection\EntityFactory; use Magento\Store\Model\Store; use Psr\Log\LoggerInterface; @@ -229,11 +230,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); @@ -265,6 +262,26 @@ 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 + ]; + } + $callIndex = 0; + $this->connection + ->expects($this->exactly(count($categoryIds))) + ->method('insert') + ->with( + $this->stringContains('temp_category_descendants_'), + $this->callback(function($args) use (&$callIndex, $withs) { + $expected = $withs[$callIndex]; + $valid = $args === $expected; + $callIndex++; + return $valid; + }) + ); $this->select->method('from')->willReturnSelf(); $this->select->method('joinLeft')->willReturnSelf(); $this->select->method('join')->willReturnSelf(); From 15db49dc161e3d0c2bf82d7f5ebaffd2ff4b9ec3 Mon Sep 17 00:00:00 2001 From: Mohamed El Mrabet Date: Thu, 20 Nov 2025 23:06:34 +0100 Subject: [PATCH 02/14] PR review fix --- .../Model/ResourceModel/Category/Collection.php | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php index d15116bd50fc9..9a3ff8451215e 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php @@ -419,15 +419,14 @@ private function getCountFromCategoryTableBulk( ['category_id', 'descendant_id'] ) ); + $data = []; foreach ($categoryIds as $catId) { - $connection->insert( - $tempTableName, - [ - 'category_id' => $catId, - 'descendant_id' => $catId - ] - ); + $data[] = [ + 'category_id' => $catId, + 'descendant_id' => $catId + ]; } + $connection->insertMultiple($tempTableName, $data); $select = $connection->select() ->from( ['t' => $tempTableName], From 015bce64e27dfd068c795feda2bf85af14dcace5 Mon Sep 17 00:00:00 2001 From: Mohamed El Mrabet Date: Thu, 20 Nov 2025 23:10:12 +0100 Subject: [PATCH 03/14] Updating unit test --- .../ResourceModel/Category/CollectionTest.php | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Category/CollectionTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Category/CollectionTest.php index d70704871e469..fc3e322ffdc5c 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Category/CollectionTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Category/CollectionTest.php @@ -265,22 +265,16 @@ public function testLoadProductCountCallsBulkMethodForLargeCategoryCount() $withs = []; foreach ($categoryIds as $categoryId) { $withs[] = [ - 'category_id' => $categoryId, + 'category_id' => $categoryId, 'descendant_id' => $categoryId ]; } - $callIndex = 0; $this->connection - ->expects($this->exactly(count($categoryIds))) - ->method('insert') + ->expects($this->once()) + ->method('insertMultiple') ->with( $this->stringContains('temp_category_descendants_'), - $this->callback(function($args) use (&$callIndex, $withs) { - $expected = $withs[$callIndex]; - $valid = $args === $expected; - $callIndex++; - return $valid; - }) + $withs ); $this->select->method('from')->willReturnSelf(); $this->select->method('joinLeft')->willReturnSelf(); From 4109db746e6482e9842b5f20ba1923f75f24cc71 Mon Sep 17 00:00:00 2001 From: Mohamed El Mrabet Date: Fri, 21 Nov 2025 12:29:27 +0100 Subject: [PATCH 04/14] Adding integration test --- .../ResourceModel/Category/Collection.php | 2 +- .../ResourceModel/Category/CollectionTest.php | 51 ++++++++- .../_files/categories_with_products_large.php | 108 ++++++++++++++++++ 3 files changed, 158 insertions(+), 3 deletions(-) create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/_files/categories_with_products_large.php diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php index 9a3ff8451215e..df841ae996ca6 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php @@ -422,7 +422,7 @@ private function getCountFromCategoryTableBulk( $data = []; foreach ($categoryIds as $catId) { $data[] = [ - 'category_id' => $catId, + 'category_id' => $catId, 'descendant_id' => $catId ]; } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Category/CollectionTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Category/CollectionTest.php index 874862b725341..0c2747b19f7c5 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Category/CollectionTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Category/CollectionTest.php @@ -1,12 +1,17 @@ collection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( \Magento\Catalog\Model\ResourceModel\Category\Collection::class ); + $objectManager = Bootstrap::getObjectManager(); + $this->categoryCollectionFactory = $objectManager->get(CollectionFactory::class); } protected function setDown() @@ -63,4 +72,42 @@ public function testJoinUrlRewriteNotOnDefaultStore() $category = $categories->getFirstItem(); $this->assertStringEndsWith('category-3-on-2.html', $category->getUrl()); } + + /** + * @magentoAppIsolation enabled + * @magentoDbIsolation enabled + * @magentoDataFixture Magento/Catalog/Model/ResourceModel/_files/categories_with_products_large.php + * @throws LocalizedException + */ + 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->count(), + 'Bulk limit path not triggered.' + ); + + foreach ($collection as $category) { + $this->assertNotNull( + $category->getProductCount(), + 'ProductCount missing for category ' . $category->getId() + ); + $this->assertIsInt( + $category->getProductCount(), + 'ProductCount is not int for category ' . $category->getId() + ); + $this->assertGreaterThan( + 0, + $category->getProductCount(), + 'Invalid product count value.' + ); + } + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/_files/categories_with_products_large.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/_files/categories_with_products_large.php new file mode 100644 index 0000000000000..2696d5321f893 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/_files/categories_with_products_large.php @@ -0,0 +1,108 @@ +get(StoreManagerInterface::class); +$rootCategoryId = (int)$storeManager->getStore()->getRootCategoryId(); + +$categoryFactory = $om->get(CategoryFactory::class); +$categoryRepo = $om->get(CategoryRepositoryInterface::class); +$productFactory = $om->get(ProductFactory::class); + +$identifier = 'bulk_test_123'; +$categoriesCount = 401; + +$createCategory = function(string $name, int $parentId) +use ($categoryFactory, $categoryRepo) { + $cat = $categoryFactory->create(); + $cat->setName($name)->setIsActive(true)->setIsAnchor(1)->setParentId($parentId); + $categoryRepo->save($cat); + return (int)$cat->getId(); +}; + +$leafCategories = []; + +/* LEVEL 1 */ +$level1 = array_map(fn($i) => $createCategory("{$identifier}_L1_{$i}", $rootCategoryId), range(1, 4)); +$lastL1 = end($level1); +foreach (range(1, 10) as $i) { + $leafCategories[] = $createCategory("{$identifier}_L1_LEAF_{$i}", $lastL1); +} + +/* LEVEL 2 */ +$level2 = []; +foreach ($level1 as $parentId) { + foreach (range(1, 13) as $i) { + $level2[] = $createCategory("{$identifier}_L2_{$parentId}_{$i}", $parentId); + } + foreach (range(1, 5) as $i) { + $leafCategories[] = $createCategory("{$identifier}_L2_{$parentId}_LEAF_{$i}", $parentId); + } +} + +/* LEVEL 3 */ +foreach ($level2 as $parentId) { + $leafCategories[] = $createCategory("{$identifier}_L3_{$parentId}_LEAF_1", $parentId); +} + +/* EXTEND TO 401 CATEGORIES */ +$totalCreated = count($level1) + count($level2) + count($leafCategories); +$missing = max(0, $categoriesCount - $totalCreated); + +for ($i = 1; $i <= $missing; $i++) { + $parentId = $level2[array_rand($level2)]; + $leafCategories[] = $createCategory("{$identifier}_L3_EXTRA_{$i}", $parentId); +} + +/* CREATE PRODUCTS */ +$leafCount = count($leafCategories); +$totalProducts = max(41, (int)($leafCount * 0.5)); + +for ($p = 1; $p <= $totalProducts; $p++) { + + $leafIds = [$leafCategories[array_rand($leafCategories)]]; + $extra = array_rand($leafCategories, min(random_int(3, 8), $leafCount)); + $extra = (array)$extra; + + foreach ($extra as $key) { + $leafIds[] = $leafCategories[$key]; + } + + $product = $productFactory->create(); + $product->setTypeId('simple') + ->setAttributeSetId(4) + ->setSku("{$identifier}_prd_{$p}") + ->setName("Bulk Test Product {$p}") + ->setPrice(10 + $p) + ->setVisibility(4) + ->setStatus(1) + ->setStockData(['qty' => 10, 'is_in_stock' => 1]) + ->setCategoryIds(array_unique($leafIds)) + ->save(); +} + +/* ENSURE EACH LEAF HAS AT LEAST 1 PRODUCT */ +$productSkus = array_map(fn($i) => "{$identifier}_prd_{$i}", range(1, $totalProducts)); + +foreach ($leafCategories as $leafId) { + $sku = $productSkus[array_rand($productSkus)]; + $product = $productFactory->create()->loadByAttribute('sku', $sku); + + if ($product) { + $product->setCategoryIds(array_unique([ + ...((array)$product->getCategoryIds()), + $leafId + ]))->save(); + } +} From d518ce967411ac3f6fce1db8e255586036b251ce Mon Sep 17 00:00:00 2001 From: Mohamed El Mrabet Date: Fri, 21 Nov 2025 13:22:23 +0100 Subject: [PATCH 05/14] cs code --- .../ResourceModel/Category/CollectionTest.php | 27 ++++++++-------- .../ResourceModel/Category/CollectionTest.php | 32 ++++++++----------- 2 files changed, 26 insertions(+), 33 deletions(-) diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Category/CollectionTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Category/CollectionTest.php index fc3e322ffdc5c..5f5e9e5d25d25 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Category/CollectionTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Category/CollectionTest.php @@ -7,28 +7,27 @@ namespace Magento\Catalog\Test\Unit\Model\ResourceModel\Category; -use Magento\Catalog\Model\Category; +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\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\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) diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Category/CollectionTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Category/CollectionTest.php index 0c2747b19f7c5..c598a48ead76f 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Category/CollectionTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Category/CollectionTest.php @@ -7,18 +7,14 @@ namespace Magento\Catalog\Model\ResourceModel\Category; -use Magento\Framework\Exception\LocalizedException; +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; /** @@ -27,18 +23,16 @@ class CollectionTest extends \PHPUnit\Framework\TestCase */ 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(); } @@ -63,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'); @@ -77,7 +71,6 @@ public function testJoinUrlRewriteNotOnDefaultStore() * @magentoAppIsolation enabled * @magentoDbIsolation enabled * @magentoDataFixture Magento/Catalog/Model/ResourceModel/_files/categories_with_products_large.php - * @throws LocalizedException */ public function testBulkProcessingModeIsTriggered() { @@ -90,23 +83,24 @@ public function testBulkProcessingModeIsTriggered() $this->assertGreaterThan( 400, - $collection->count(), + $collection->getSize(), 'Bulk limit path not triggered.' ); foreach ($collection as $category) { + $productCount = $category->getProductCount(); $this->assertNotNull( - $category->getProductCount(), + $productCount, 'ProductCount missing for category ' . $category->getId() ); $this->assertIsInt( - $category->getProductCount(), + $productCount, 'ProductCount is not int for category ' . $category->getId() ); $this->assertGreaterThan( 0, - $category->getProductCount(), - 'Invalid product count value.' + $productCount, + sprintf('Invalid product count for category %d.', $category->getId()) ); } } From a0d86b2997fb5fdbdf22c9898cf3b4e4af296b93 Mon Sep 17 00:00:00 2001 From: Mohamed El Mrabet Date: Fri, 21 Nov 2025 13:23:17 +0100 Subject: [PATCH 06/14] cs code --- .../Magento/Catalog/Test/Unit/Helper/CategoryTestHelper.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/code/Magento/Catalog/Test/Unit/Helper/CategoryTestHelper.php b/app/code/Magento/Catalog/Test/Unit/Helper/CategoryTestHelper.php index 08743b3959893..98199d5f4b3d0 100644 --- a/app/code/Magento/Catalog/Test/Unit/Helper/CategoryTestHelper.php +++ b/app/code/Magento/Catalog/Test/Unit/Helper/CategoryTestHelper.php @@ -11,9 +11,6 @@ /** * Test helper class for Catalog Category with custom methods - * - * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) - * @SuppressWarnings(PHPMD.ExcessivePublicCount) */ class CategoryTestHelper extends Category { From df7b60862ec45657caa591b179b6f437592c5cf3 Mon Sep 17 00:00:00 2001 From: Mohamed El Mrabet Date: Fri, 21 Nov 2025 14:31:29 +0100 Subject: [PATCH 07/14] Optimize the test --- .../ResourceModel/Category/CollectionTest.php | 5 +- .../_files/categories_with_products_large.php | 165 +++++++++++------- 2 files changed, 99 insertions(+), 71 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Category/CollectionTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Category/CollectionTest.php index c598a48ead76f..1fde7bd0722c5 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Category/CollectionTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Category/CollectionTest.php @@ -70,6 +70,7 @@ public function testJoinUrlRewriteNotOnDefaultStore() /** * @magentoAppIsolation enabled * @magentoDbIsolation enabled + * @magentoAppArea adminhtml * @magentoDataFixture Magento/Catalog/Model/ResourceModel/_files/categories_with_products_large.php */ public function testBulkProcessingModeIsTriggered() @@ -93,10 +94,6 @@ public function testBulkProcessingModeIsTriggered() $productCount, 'ProductCount missing for category ' . $category->getId() ); - $this->assertIsInt( - $productCount, - 'ProductCount is not int for category ' . $category->getId() - ); $this->assertGreaterThan( 0, $productCount, diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/_files/categories_with_products_large.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/_files/categories_with_products_large.php index 2696d5321f893..8d3896c23a7b8 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/_files/categories_with_products_large.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/_files/categories_with_products_large.php @@ -5,104 +5,135 @@ */ declare(strict_types=1); -use Magento\TestFramework\Helper\Bootstrap; 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(); +$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]); -$categoryFactory = $om->get(CategoryFactory::class); -$categoryRepo = $om->get(CategoryRepositoryInterface::class); -$productFactory = $om->get(ProductFactory::class); + $productResource->save($p); + $products[] = $p->getId(); +} -$identifier = 'bulk_test_123'; -$categoriesCount = 401; +/* category generator + inline product assignment */ +$createCategory = function(string $name, int $parentId, bool $isLeaf = false) +use ($categoryFactory, $categoryRepository, $products, $conn, $table) { -$createCategory = function(string $name, int $parentId) -use ($categoryFactory, $categoryRepo) { $cat = $categoryFactory->create(); - $cat->setName($name)->setIsActive(true)->setIsAnchor(1)->setParentId($parentId); - $categoryRepo->save($cat); - return (int)$cat->getId(); + $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); -/* LEVEL 1 */ -$level1 = array_map(fn($i) => $createCategory("{$identifier}_L1_{$i}", $rootCategoryId), range(1, 4)); $lastL1 = end($level1); + foreach (range(1, 10) as $i) { - $leafCategories[] = $createCategory("{$identifier}_L1_LEAF_{$i}", $lastL1); + $leafCategories[] = $createCategory("{$identifier}_l1_leaf_{$i}", $lastL1, true); } -/* LEVEL 2 */ +/* level 2 */ $level2 = []; + foreach ($level1 as $parentId) { + + $createdThisLoop = []; + foreach (range(1, 13) as $i) { - $level2[] = $createCategory("{$identifier}_L2_{$parentId}_{$i}", $parentId); + $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); + $leafCategories[] = $createCategory("{$identifier}_l2_{$parentId}_leaf_{$i}", $parentId, true); } } -/* LEVEL 3 */ +/* level 3 leafs */ foreach ($level2 as $parentId) { - $leafCategories[] = $createCategory("{$identifier}_L3_{$parentId}_LEAF_1", $parentId); + $leafCategories[] = $createCategory("{$identifier}_l3_{$parentId}_leaf", $parentId, true); } -/* EXTEND TO 401 CATEGORIES */ -$totalCreated = count($level1) + count($level2) + count($leafCategories); +/* extend up to target count */ +$totalCreated = count($parentCategories) + count($leafCategories); $missing = max(0, $categoriesCount - $totalCreated); for ($i = 1; $i <= $missing; $i++) { - $parentId = $level2[array_rand($level2)]; - $leafCategories[] = $createCategory("{$identifier}_L3_EXTRA_{$i}", $parentId); -} - -/* CREATE PRODUCTS */ -$leafCount = count($leafCategories); -$totalProducts = max(41, (int)($leafCount * 0.5)); - -for ($p = 1; $p <= $totalProducts; $p++) { - - $leafIds = [$leafCategories[array_rand($leafCategories)]]; - $extra = array_rand($leafCategories, min(random_int(3, 8), $leafCount)); - $extra = (array)$extra; - - foreach ($extra as $key) { - $leafIds[] = $leafCategories[$key]; - } - - $product = $productFactory->create(); - $product->setTypeId('simple') - ->setAttributeSetId(4) - ->setSku("{$identifier}_prd_{$p}") - ->setName("Bulk Test Product {$p}") - ->setPrice(10 + $p) - ->setVisibility(4) - ->setStatus(1) - ->setStockData(['qty' => 10, 'is_in_stock' => 1]) - ->setCategoryIds(array_unique($leafIds)) - ->save(); -} - -/* ENSURE EACH LEAF HAS AT LEAST 1 PRODUCT */ -$productSkus = array_map(fn($i) => "{$identifier}_prd_{$i}", range(1, $totalProducts)); - -foreach ($leafCategories as $leafId) { - $sku = $productSkus[array_rand($productSkus)]; - $product = $productFactory->create()->loadByAttribute('sku', $sku); - - if ($product) { - $product->setCategoryIds(array_unique([ - ...((array)$product->getCategoryIds()), - $leafId - ]))->save(); - } + $parentId = $parentCategories[random_int(0, count($parentCategories) - 1)]; + $leafCategories[] = $createCategory("{$identifier}_extra_{$i}", $parentId, true); } From 6c3dc6fec303bf8d5f5cff6afeda2b19ef72f081 Mon Sep 17 00:00:00 2001 From: Mohamed El Mrabet Date: Fri, 21 Nov 2025 23:09:02 +0100 Subject: [PATCH 08/14] fix static check --- .../Test/Fixture/CategoryTreeWithProducts.php | 283 ++++++++++++++++++ .../Test/Unit/Helper/CategoryTestHelper.php | 2 +- .../ResourceModel/Category/CollectionTest.php | 38 ++- .../_files/categories_with_products_large.php | 139 --------- 4 files changed, 315 insertions(+), 147 deletions(-) create mode 100644 app/code/Magento/Catalog/Test/Fixture/CategoryTreeWithProducts.php delete mode 100644 dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/_files/categories_with_products_large.php diff --git a/app/code/Magento/Catalog/Test/Fixture/CategoryTreeWithProducts.php b/app/code/Magento/Catalog/Test/Fixture/CategoryTreeWithProducts.php new file mode 100644 index 0000000000000..2e596e265a7fd --- /dev/null +++ b/app/code/Magento/Catalog/Test/Fixture/CategoryTreeWithProducts.php @@ -0,0 +1,283 @@ + 'CategoryBulk%uniqid%', + 'category_count' => 10, + 'product_identifier' => 'ProductBulk%uniqid%', + 'product_count' => 0, + 'depth' => 1, + 'fanout' => [], + 'root_id' => null, + ]; + + private AdapterInterface $connection; + private string $categoryProductTable; + + public function __construct( + private readonly ProcessorInterface $dataProcessor, + private readonly DataMerger $dataMerger, + private readonly CategoryRepositoryInterface $categoryRepository, + private readonly CategoryFactory $categoryFactory, + private readonly StoreManagerInterface $storeManager, + private readonly ProductFactory $productFactory, + private readonly ProductResource $productResource, + ResourceConnection $resource + ) { + $this->connection = $resource->getConnection(); + $this->categoryProductTable = $resource->getTableName('catalog_category_product'); + } + + /** + * Execute fixture. + */ + public function apply(array $data = []): ?DataObject + { + $data = $this->prepareData($data); + $productIdentifier = $data['product_identifier']; + $productsCount = (int)$data['product_count']; + $categoryIdentifier = $data['category_identifier']; + $categoriesCount = (int)$data['category_count']; + $depth = (int)$data['depth']; + $fanoutInput = $data['fanout']; + $requestedRootId = $data['root_id']; + + if ($depth < 0 || $depth > 5) { + throw new \RuntimeException("Parameter 'depth' must be between 0 and 5."); + } + + /** + * Resolve root_id + */ + if ($requestedRootId === null) { + $rootId = (int)$this->storeManager->getStore()->getRootCategoryId(); + } else { + try { + $root = $this->categoryRepository->get((int)$requestedRootId); + $rootId = (int)$root->getId(); + } catch (\Exception $e) { + throw new \RuntimeException("Invalid root_id '{$requestedRootId}': " . $e->getMessage()); + } + } + + /** Compute fanout */ + $fanout = $this->computeFanout($categoriesCount, $depth, $fanoutInput); + + /** Create products */ + $products = $productsCount > 0 + ? $this->createProducts($productsCount, $productIdentifier) + : []; + + $leafCategories = []; + $parentCategories = []; + $levelParents = []; + + /* ---------------- LEVEL 0 ---------------- */ + $levelParents[0] = []; + foreach (range(1, $fanout[0]) as $i) { + $levelParents[0][] = $this->createCategoryNode( + "{$categoryIdentifier}_l0_{$rootId}_{$i}", + $rootId, + ($depth === 0), + $products, + $leafCategories, + $parentCategories + ); + } + + /* ---------------- LEVELS 1 → depth ---------------- */ + for ($level = 1; $level <= $depth; $level++) { + + $levelParents[$level] = []; + + foreach ($levelParents[$level - 1] as $parentId) { + foreach (range(1, $fanout[$level]) as $i) { + + $levelParents[$level][] = $this->createCategoryNode( + "{$categoryIdentifier}_l{$level}_{$parentId}_{$i}", + $parentId, + ($level === $depth), + $products, + $leafCategories, + $parentCategories + ); + } + } + } + + return $this->finalize( + $categoriesCount, + $categoryIdentifier, + $products, + $leafCategories, + $parentCategories + ); + } + + /** + * Compute fanout + */ + private function computeFanout(int $total, int $depth, array $fanout): array + { + $levels = $depth + 1; + $computed = []; + + for ($i = 0; $i < $levels; $i++) { + if (isset($fanout[$i]) && $fanout[$i] > 0) { + $computed[$i] = (int)$fanout[$i]; + continue; + } + // AUTO distribute + $computed[$i] = max(1, (int)floor(pow($total, 1 / $levels))); + } + + return $computed; + } + + private function finalize( + int $categoriesCount, + string $identifier, + array $products, + array $leafCategories, + array $parentCategories + ): DataObject { + + $total = count($parentCategories) + count($leafCategories); + $missing = max(0, $categoriesCount - $total); + + for ($i = 1; $i <= $missing; $i++) { + $randomParentId = $parentCategories[random_int(0, count($parentCategories) - 1)]; + + $this->createCategoryNode( + "{$identifier}_extra_{$randomParentId}_{$i}", + $randomParentId, + true, + $products, + $leafCategories, + $parentCategories + ); + } + + return new DataObject([ + 'products' => $products, + 'leaf_categories' => $leafCategories, + 'all_categories' => array_merge($parentCategories, $leafCategories), + ]); + } + + /** Create products */ + private function createProducts(int $count, string $prefix): array + { + $ids = []; + + for ($i = 1; $i <= $count; $i++) { + $product = $this->productFactory->create(); + $product->setTypeId('simple') + ->setAttributeSetId(4) + ->setSku("{$prefix}_{$i}") + ->setName("Bulk Test Product {$i}") + ->setPrice(10 + $i) + ->setVisibility(4) + ->setStatus(1); + + $this->productResource->save($product); + $ids[] = (int)$product->getId(); + } + + return $ids; + } + + /** Create category node */ + private function createCategoryNode( + string $name, + int $parentId, + bool $isLeaf, + array $products, + array &$leafCategories, + array &$parentCategories + ): int { + + $cat = $this->categoryFactory->create(); + $cat->setName($name) + ->setIsActive(true) + ->setIsAnchor(1) + ->setParentId($parentId); + + $this->categoryRepository->save($cat); + + $id = (int)$cat->getId(); + + if ($isLeaf && count($products)) { + $this->assignRandomProductsToLeaf($id, $products); + $leafCategories[] = $id; + } else { + $parentCategories[] = $id; + } + + return $id; + } + + /** Assign products to leaf category */ + private function assignRandomProductsToLeaf(int $catId, array $products): void + { + $count = random_int(1, 5); + $selected = []; + + for ($i = 0; $i < $count; $i++) { + $selected[] = $products[random_int(0, count($products) - 1)]; + } + + $selected = array_unique($selected); + + $rows = []; + foreach ($selected as $pid) { + $rows[] = [ + 'category_id' => $catId, + 'product_id' => $pid, + 'position' => 0 + ]; + } + + if ($rows) { + $this->connection->insertMultiple($this->categoryProductTable, $rows); + } + } + + private function prepareData(array $data): array + { + return $this->dataProcessor->process( + $this, + $this->dataMerger->merge(self::DEFAULT_DATA, $data) + ); + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Helper/CategoryTestHelper.php b/app/code/Magento/Catalog/Test/Unit/Helper/CategoryTestHelper.php index 98199d5f4b3d0..5e64e8cedbb8b 100644 --- a/app/code/Magento/Catalog/Test/Unit/Helper/CategoryTestHelper.php +++ b/app/code/Magento/Catalog/Test/Unit/Helper/CategoryTestHelper.php @@ -20,7 +20,7 @@ class CategoryTestHelper extends Category * * @return bool */ - public function getIsAnchor(): bool + public function getIsAnchor() { return $this->getData('is_anchor'); } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Category/CollectionTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Category/CollectionTest.php index 1fde7bd0722c5..d2a26a8ce0fbc 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Category/CollectionTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Category/CollectionTest.php @@ -7,14 +7,27 @@ namespace Magento\Catalog\Model\ResourceModel\Category; +use Magento\Catalog\Test\Fixture\CategoryTreeWithProducts as CategoryTreeWithProductsFixture; +use Magento\Catalog\Test\Fixture\CategoryTree as CategoryTreeFixture; use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Fixture\AppArea; +use Magento\TestFramework\Fixture\AppIsolation; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DbIsolation; 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 Collection|mixed + */ private Collection $collection; + + /** + * @var CollectionFactory + */ private CollectionFactory $categoryCollectionFactory; /** @@ -67,18 +80,29 @@ public function testJoinUrlRewriteNotOnDefaultStore() $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 - */ + #[ + DataFixture ( + CategoryTreeWithProductsFixture ::class, + [ + 'category_identifier' => 'bulk_test_123_cat', + 'category_count' => 401, + 'product_identifier' => 'bulk_test_123_prd', + 'product_count' => 20, + 'depth' => 3, + 'fanout' => [5, 10, 4, 6], + ], + 'cats' + ), + AppArea('adminhtml'), + DbIsolation(true), + AppIsolation(true) + ] public function testBulkProcessingModeIsTriggered() { /** @var CategoryCollection $collection */ $collection = $this->categoryCollectionFactory->create(); $collection->addAttributeToSelect('*'); - $collection->addAttributeToFilter('name', ['like' => 'bulk_test_123%']); + $collection->addAttributeToFilter('name', ['like' => 'bulk_test_123_cat%']); $collection->setLoadProductCount(true); $collection->load(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/_files/categories_with_products_large.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/_files/categories_with_products_large.php deleted file mode 100644 index 8d3896c23a7b8..0000000000000 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/_files/categories_with_products_large.php +++ /dev/null @@ -1,139 +0,0 @@ -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); -} From 9e5ead322014bf9e79bc245335b35eee516ec985 Mon Sep 17 00:00:00 2001 From: Mohamed El Mrabet Date: Fri, 21 Nov 2025 23:25:50 +0100 Subject: [PATCH 09/14] fixing error fixture --- .../Test/Fixture/CategoryTreeWithProducts.php | 40 +++++++++---------- .../ResourceModel/Category/CollectionTest.php | 3 +- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/app/code/Magento/Catalog/Test/Fixture/CategoryTreeWithProducts.php b/app/code/Magento/Catalog/Test/Fixture/CategoryTreeWithProducts.php index 2e596e265a7fd..3e74b30d7409b 100644 --- a/app/code/Magento/Catalog/Test/Fixture/CategoryTreeWithProducts.php +++ b/app/code/Magento/Catalog/Test/Fixture/CategoryTreeWithProducts.php @@ -114,27 +114,24 @@ public function apply(array $data = []): ?DataObject $parentCategories ); } - - /* ---------------- LEVELS 1 → depth ---------------- */ - for ($level = 1; $level <= $depth; $level++) { - - $levelParents[$level] = []; - - foreach ($levelParents[$level - 1] as $parentId) { - foreach (range(1, $fanout[$level]) as $i) { - - $levelParents[$level][] = $this->createCategoryNode( - "{$categoryIdentifier}_l{$level}_{$parentId}_{$i}", - $parentId, - ($level === $depth), - $products, - $leafCategories, - $parentCategories - ); + if (count($fanout) > 1) { + /* ---------------- LEVELS 1 → depth ---------------- */ + for ($level = 1; $level <= $depth; $level++) { + $levelParents[$level] = []; + foreach ($levelParents[$level - 1] as $parentId) { + foreach (range(1, $fanout[$level]) as $i) { + $levelParents[$level][] = $this->createCategoryNode( + "{$categoryIdentifier}_l{$level}_{$parentId}_{$i}", + $parentId, + ($level === $depth), + $products, + $leafCategories, + $parentCategories + ); + } } } } - return $this->finalize( $categoriesCount, $categoryIdentifier, @@ -149,9 +146,12 @@ public function apply(array $data = []): ?DataObject */ private function computeFanout(int $total, int $depth, array $fanout): array { - $levels = $depth + 1; $computed = []; - + if (count($fanout) && array_sum($fanout) > $total) { + $computed[] = $total; + return $computed; + } + $levels = $depth + 1; for ($i = 0; $i < $levels; $i++) { if (isset($fanout[$i]) && $fanout[$i] > 0) { $computed[$i] = (int)$fanout[$i]; diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Category/CollectionTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Category/CollectionTest.php index d2a26a8ce0fbc..7dff777192a33 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Category/CollectionTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Category/CollectionTest.php @@ -88,8 +88,7 @@ public function testJoinUrlRewriteNotOnDefaultStore() 'category_count' => 401, 'product_identifier' => 'bulk_test_123_prd', 'product_count' => 20, - 'depth' => 3, - 'fanout' => [5, 10, 4, 6], + 'depth' => 3 ], 'cats' ), From bb6eccc8b96d7b60477ac2af3b0c53c202f7f509 Mon Sep 17 00:00:00 2001 From: Mohamed El Mrabet Date: Sat, 22 Nov 2025 00:31:46 +0100 Subject: [PATCH 10/14] fixing error fixture --- .../Test/Fixture/CategoryTreeWithProducts.php | 7 +++++ .../Test/Unit/Helper/CategoryTestHelper.php | 27 ------------------- .../ResourceModel/Category/CollectionTest.php | 8 +++++- .../ResourceModel/Category/CollectionTest.php | 2 +- 4 files changed, 15 insertions(+), 29 deletions(-) delete mode 100644 app/code/Magento/Catalog/Test/Unit/Helper/CategoryTestHelper.php diff --git a/app/code/Magento/Catalog/Test/Fixture/CategoryTreeWithProducts.php b/app/code/Magento/Catalog/Test/Fixture/CategoryTreeWithProducts.php index 3e74b30d7409b..bc28d7bcdd2ef 100644 --- a/app/code/Magento/Catalog/Test/Fixture/CategoryTreeWithProducts.php +++ b/app/code/Magento/Catalog/Test/Fixture/CategoryTreeWithProducts.php @@ -41,7 +41,14 @@ class CategoryTreeWithProducts implements DataFixtureInterface 'root_id' => null, ]; + /** + * @var AdapterInterface + */ private AdapterInterface $connection; + + /** + * @var string + */ private string $categoryProductTable; public function __construct( diff --git a/app/code/Magento/Catalog/Test/Unit/Helper/CategoryTestHelper.php b/app/code/Magento/Catalog/Test/Unit/Helper/CategoryTestHelper.php deleted file mode 100644 index 5e64e8cedbb8b..0000000000000 --- a/app/code/Magento/Catalog/Test/Unit/Helper/CategoryTestHelper.php +++ /dev/null @@ -1,27 +0,0 @@ -getData('is_anchor'); - } -} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Category/CollectionTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Category/CollectionTest.php index 5f5e9e5d25d25..7aca9f966241b 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Category/CollectionTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Category/CollectionTest.php @@ -7,11 +7,13 @@ namespace Magento\Catalog\Test\Unit\Model\ResourceModel\Category; +use Magento\Catalog\Model\Category; 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\Eav\Model\Entity\Attribute\AttributeInterface; use Magento\Eav\Model\EntityFactory as EavEntityFactory; use Magento\Eav\Model\ResourceModel\Helper; use Magento\Framework\App\Config\ScopeConfigInterface; @@ -229,7 +231,11 @@ public function testLoadProductCountCallsBulkMethodForLargeCategoryCount() $items = []; $categoryIds = []; for ($i = 1; $i <= $categoryCount; $i++) { - $category = $this->createMock(CategoryTestHelper::class); + $category = $this->getMockBuilder(Category::class) + ->addMethods(['getIsAnchor']) + ->onlyMethods(['getId', 'setProductCount']) + ->disableOriginalConstructor() + ->getMock(); $category->method('getId')->willReturn($i); $category->method('getIsAnchor')->willReturn(true); $category->expects($this->once())->method('setProductCount')->with(5); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Category/CollectionTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Category/CollectionTest.php index 7dff777192a33..304a5bf65d2f6 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Category/CollectionTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Category/CollectionTest.php @@ -82,7 +82,7 @@ public function testJoinUrlRewriteNotOnDefaultStore() #[ DataFixture ( - CategoryTreeWithProductsFixture ::class, + CategoryTreeWithProductsFixture::class, [ 'category_identifier' => 'bulk_test_123_cat', 'category_count' => 401, From 4fad9fbb70a3768d87e6f70a1ef070cca3de61cb Mon Sep 17 00:00:00 2001 From: Mohamed El Mrabet Date: Sat, 22 Nov 2025 09:24:06 +0100 Subject: [PATCH 11/14] fix static issue --- .../Catalog/Model/ResourceModel/Category/CollectionTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Category/CollectionTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Category/CollectionTest.php index 304a5bf65d2f6..8641a9ed7a12f 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Category/CollectionTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Category/CollectionTest.php @@ -81,7 +81,7 @@ public function testJoinUrlRewriteNotOnDefaultStore() } #[ - DataFixture ( + DataFixture( CategoryTreeWithProductsFixture::class, [ 'category_identifier' => 'bulk_test_123_cat', From 63b9932fa4b374c6940a0fcd9d05a25dddb1a678 Mon Sep 17 00:00:00 2001 From: Mohamed El Mrabet Date: Sat, 22 Nov 2025 09:29:13 +0100 Subject: [PATCH 12/14] fix static issue --- .../Model/ResourceModel/Category/CollectionTest.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Category/CollectionTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Category/CollectionTest.php index 8641a9ed7a12f..c1ea8fafe8ba5 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Category/CollectionTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Category/CollectionTest.php @@ -8,7 +8,6 @@ namespace Magento\Catalog\Model\ResourceModel\Category; use Magento\Catalog\Test\Fixture\CategoryTreeWithProducts as CategoryTreeWithProductsFixture; -use Magento\Catalog\Test\Fixture\CategoryTree as CategoryTreeFixture; use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Fixture\AppArea; use Magento\TestFramework\Fixture\AppIsolation; @@ -17,11 +16,17 @@ use Magento\TestFramework\Helper\Bootstrap; use Magento\Catalog\Model\ResourceModel\Category\Collection as CategoryCollection; use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory; +use PHPUnit\Framework\TestCase; -class CollectionTest extends \PHPUnit\Framework\TestCase +/** + * Tests collection category + * + * @see \Magento\Catalog\Model\ResourceModel\Category\Collection + */ +class CollectionTest extends TestCase { /** - * @var Collection|mixed + * @var Collection */ private Collection $collection; From 625881e8bb95a29b990112238339b76c8b852a37 Mon Sep 17 00:00:00 2001 From: Mohamed El Mrabet Date: Sun, 23 Nov 2025 21:29:53 +0100 Subject: [PATCH 13/14] Remove the BULK_PROCESSING_LIMIT --- .../ResourceModel/Category/Collection.php | 75 +---- .../Test/Fixture/CategoryTreeWithProducts.php | 290 ------------------ .../ResourceModel/Category/CollectionTest.php | 89 +++--- .../ResourceModel/Category/CollectionTest.php | 61 ++-- 4 files changed, 88 insertions(+), 427 deletions(-) delete mode 100644 app/code/Magento/Catalog/Test/Fixture/CategoryTreeWithProducts.php diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php index df841ae996ca6..53ddc8394949c 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php @@ -23,8 +23,6 @@ */ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\AbstractCollection { - private const BULK_PROCESSING_LIMIT = 400; - /** * Event prefix name * @@ -288,6 +286,7 @@ protected function _loadProductCount() * @SuppressWarnings(PHPMD.UnusedLocalVariable) * @SuppressWarnings(PHPMD.NPathComplexity) * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws \Zend_Db_Exception */ public function loadProductCount($items, $countRegular = true, $countAnchor = true) { @@ -340,27 +339,24 @@ public function loadProductCount($items, $countRegular = true, $countAnchor = tr if ($countAnchor) { // Retrieve Anchor categories product counts $categoryIds = array_keys($anchor); - $countSelect = $this->getProductsCountQuery($categoryIds, (bool)$websiteId); + $countSelect = $this->getProductsCountQuery($categoryIds, (bool) $websiteId); $categoryProductsCount = $this->_conn->fetchPairs($countSelect); - $countFromCategoryTable = []; - if (count($categoryIds) > self::BULK_PROCESSING_LIMIT) { - $countFromCategoryTable = $this->getCountFromCategoryTableBulk($categoryIds, (int)$websiteId); - } + + // Find categories missing from the SQL result + $categoriesIdsAlreadyIndexed = array_keys($categoryProductsCount); + $missingCategoryIds = array_diff($categoryIds, $categoriesIdsAlreadyIndexed); + + $countFromCategoryTable = $this->getCountFromCategoryTableBulk( + $missingCategoryIds, + (int) $websiteId + ); foreach ($anchor as $item) { - $productsCount = 0; - if (count($categoryIds) > self::BULK_PROCESSING_LIMIT) { - if (isset($categoryProductsCount[$item->getId()])) { - $productsCount = (int)$categoryProductsCount[$item->getId()]; - } elseif (isset($countFromCategoryTable[$item->getId()])) { - $productsCount = (int)$countFromCategoryTable[$item->getId()]; - } - } else { - $productsCount = isset($categoryProductsCount[$item->getId()]) - ? (int)$categoryProductsCount[$item->getId()] - : $this->getProductsCountFromCategoryTable($item, $websiteId); - } - $item->setProductCount($productsCount); + $id = $item->getId(); + $productsCount = $categoryProductsCount[$id] + ?? $countFromCategoryTable[$id] + ?? 0; + $item->setProductCount((int) $productsCount); } } return $this; @@ -455,45 +451,6 @@ private function getCountFromCategoryTableBulk( return $counts; } - /** - * Get products count using catalog_category_entity table - * - * @param Category $item - * @param string $websiteId - * @return int - */ - private function getProductsCountFromCategoryTable(Category $item, string $websiteId): int - { - $productCount = 0; - - if ($item->getAllChildren()) { - $bind = ['entity_id' => $item->getId(), 'c_path' => $item->getPath() . '/%']; - $select = $this->_conn->select(); - $select->from( - ['main_table' => $this->getProductTable()], - new \Zend_Db_Expr('COUNT(DISTINCT main_table.product_id)') - )->joinInner( - ['e' => $this->getTable('catalog_category_entity')], - 'main_table.category_id=e.entity_id', - [] - )->where( - '(e.entity_id = :entity_id OR e.path LIKE :c_path)' - ); - if ($websiteId) { - $select->join( - ['w' => $this->getProductWebsiteTable()], - 'main_table.product_id = w.product_id', - [] - )->where( - 'w.website_id = ?', - $websiteId - ); - } - $productCount = (int)$this->_conn->fetchOne($select, $bind); - } - return $productCount; - } - /** * Add category path filter * diff --git a/app/code/Magento/Catalog/Test/Fixture/CategoryTreeWithProducts.php b/app/code/Magento/Catalog/Test/Fixture/CategoryTreeWithProducts.php deleted file mode 100644 index bc28d7bcdd2ef..0000000000000 --- a/app/code/Magento/Catalog/Test/Fixture/CategoryTreeWithProducts.php +++ /dev/null @@ -1,290 +0,0 @@ - 'CategoryBulk%uniqid%', - 'category_count' => 10, - 'product_identifier' => 'ProductBulk%uniqid%', - 'product_count' => 0, - 'depth' => 1, - 'fanout' => [], - 'root_id' => null, - ]; - - /** - * @var AdapterInterface - */ - private AdapterInterface $connection; - - /** - * @var string - */ - private string $categoryProductTable; - - public function __construct( - private readonly ProcessorInterface $dataProcessor, - private readonly DataMerger $dataMerger, - private readonly CategoryRepositoryInterface $categoryRepository, - private readonly CategoryFactory $categoryFactory, - private readonly StoreManagerInterface $storeManager, - private readonly ProductFactory $productFactory, - private readonly ProductResource $productResource, - ResourceConnection $resource - ) { - $this->connection = $resource->getConnection(); - $this->categoryProductTable = $resource->getTableName('catalog_category_product'); - } - - /** - * Execute fixture. - */ - public function apply(array $data = []): ?DataObject - { - $data = $this->prepareData($data); - $productIdentifier = $data['product_identifier']; - $productsCount = (int)$data['product_count']; - $categoryIdentifier = $data['category_identifier']; - $categoriesCount = (int)$data['category_count']; - $depth = (int)$data['depth']; - $fanoutInput = $data['fanout']; - $requestedRootId = $data['root_id']; - - if ($depth < 0 || $depth > 5) { - throw new \RuntimeException("Parameter 'depth' must be between 0 and 5."); - } - - /** - * Resolve root_id - */ - if ($requestedRootId === null) { - $rootId = (int)$this->storeManager->getStore()->getRootCategoryId(); - } else { - try { - $root = $this->categoryRepository->get((int)$requestedRootId); - $rootId = (int)$root->getId(); - } catch (\Exception $e) { - throw new \RuntimeException("Invalid root_id '{$requestedRootId}': " . $e->getMessage()); - } - } - - /** Compute fanout */ - $fanout = $this->computeFanout($categoriesCount, $depth, $fanoutInput); - - /** Create products */ - $products = $productsCount > 0 - ? $this->createProducts($productsCount, $productIdentifier) - : []; - - $leafCategories = []; - $parentCategories = []; - $levelParents = []; - - /* ---------------- LEVEL 0 ---------------- */ - $levelParents[0] = []; - foreach (range(1, $fanout[0]) as $i) { - $levelParents[0][] = $this->createCategoryNode( - "{$categoryIdentifier}_l0_{$rootId}_{$i}", - $rootId, - ($depth === 0), - $products, - $leafCategories, - $parentCategories - ); - } - if (count($fanout) > 1) { - /* ---------------- LEVELS 1 → depth ---------------- */ - for ($level = 1; $level <= $depth; $level++) { - $levelParents[$level] = []; - foreach ($levelParents[$level - 1] as $parentId) { - foreach (range(1, $fanout[$level]) as $i) { - $levelParents[$level][] = $this->createCategoryNode( - "{$categoryIdentifier}_l{$level}_{$parentId}_{$i}", - $parentId, - ($level === $depth), - $products, - $leafCategories, - $parentCategories - ); - } - } - } - } - return $this->finalize( - $categoriesCount, - $categoryIdentifier, - $products, - $leafCategories, - $parentCategories - ); - } - - /** - * Compute fanout - */ - private function computeFanout(int $total, int $depth, array $fanout): array - { - $computed = []; - if (count($fanout) && array_sum($fanout) > $total) { - $computed[] = $total; - return $computed; - } - $levels = $depth + 1; - for ($i = 0; $i < $levels; $i++) { - if (isset($fanout[$i]) && $fanout[$i] > 0) { - $computed[$i] = (int)$fanout[$i]; - continue; - } - // AUTO distribute - $computed[$i] = max(1, (int)floor(pow($total, 1 / $levels))); - } - - return $computed; - } - - private function finalize( - int $categoriesCount, - string $identifier, - array $products, - array $leafCategories, - array $parentCategories - ): DataObject { - - $total = count($parentCategories) + count($leafCategories); - $missing = max(0, $categoriesCount - $total); - - for ($i = 1; $i <= $missing; $i++) { - $randomParentId = $parentCategories[random_int(0, count($parentCategories) - 1)]; - - $this->createCategoryNode( - "{$identifier}_extra_{$randomParentId}_{$i}", - $randomParentId, - true, - $products, - $leafCategories, - $parentCategories - ); - } - - return new DataObject([ - 'products' => $products, - 'leaf_categories' => $leafCategories, - 'all_categories' => array_merge($parentCategories, $leafCategories), - ]); - } - - /** Create products */ - private function createProducts(int $count, string $prefix): array - { - $ids = []; - - for ($i = 1; $i <= $count; $i++) { - $product = $this->productFactory->create(); - $product->setTypeId('simple') - ->setAttributeSetId(4) - ->setSku("{$prefix}_{$i}") - ->setName("Bulk Test Product {$i}") - ->setPrice(10 + $i) - ->setVisibility(4) - ->setStatus(1); - - $this->productResource->save($product); - $ids[] = (int)$product->getId(); - } - - return $ids; - } - - /** Create category node */ - private function createCategoryNode( - string $name, - int $parentId, - bool $isLeaf, - array $products, - array &$leafCategories, - array &$parentCategories - ): int { - - $cat = $this->categoryFactory->create(); - $cat->setName($name) - ->setIsActive(true) - ->setIsAnchor(1) - ->setParentId($parentId); - - $this->categoryRepository->save($cat); - - $id = (int)$cat->getId(); - - if ($isLeaf && count($products)) { - $this->assignRandomProductsToLeaf($id, $products); - $leafCategories[] = $id; - } else { - $parentCategories[] = $id; - } - - return $id; - } - - /** Assign products to leaf category */ - private function assignRandomProductsToLeaf(int $catId, array $products): void - { - $count = random_int(1, 5); - $selected = []; - - for ($i = 0; $i < $count; $i++) { - $selected[] = $products[random_int(0, count($products) - 1)]; - } - - $selected = array_unique($selected); - - $rows = []; - foreach ($selected as $pid) { - $rows[] = [ - 'category_id' => $catId, - 'product_id' => $pid, - 'position' => 0 - ]; - } - - if ($rows) { - $this->connection->insertMultiple($this->categoryProductTable, $rows); - } - } - - private function prepareData(array $data): array - { - return $this->dataProcessor->process( - $this, - $this->dataMerger->merge(self::DEFAULT_DATA, $data) - ); - } -} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Category/CollectionTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Category/CollectionTest.php index 7aca9f966241b..8d09026d384e7 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Category/CollectionTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Category/CollectionTest.php @@ -21,6 +21,7 @@ use Magento\Framework\Data\Collection\Db\FetchStrategyInterface; use Magento\Framework\Data\Collection\EntityFactory; use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Ddl\Table; use Magento\Framework\DB\Select; use Magento\Framework\Event\ManagerInterface; use Magento\Framework\Validator\UniversalFactory; @@ -220,75 +221,65 @@ public function testLoadProductCount() : void $this->collection->loadProductCount([]); } - /** - * Test that loadProductCount calls getCountFromCategoryTableBulk - */ - public function testLoadProductCountCallsBulkMethodForLargeCategoryCount() + public function testLoadProductCountWithAnchors() { $websiteId = 1; $storeId = 1; - $categoryCount = 401; $items = []; - $categoryIds = []; - for ($i = 1; $i <= $categoryCount; $i++) { + $categoryIds = range(1, 10); + foreach ($categoryIds as $id) { $category = $this->getMockBuilder(Category::class) ->addMethods(['getIsAnchor']) ->onlyMethods(['getId', 'setProductCount']) ->disableOriginalConstructor() ->getMock(); - $category->method('getId')->willReturn($i); + $category->method('getId')->willReturn($id); $category->method('getIsAnchor')->willReturn(true); - $category->expects($this->once())->method('setProductCount')->with(5); - $items[$i] = $category; - $categoryIds[] = $i; + $category + ->expects($this->once()) + ->method('setProductCount')->with(5); + $items[$id] = $category; } - $storeMock = $this->createMock(Store::class); - $storeMock->method('getWebsiteId')->willReturn($websiteId); - $this->storeManager->method('getStore')->with($storeId)->willReturn($storeMock); - $this->connection->method('select')->willReturn($this->select); - $counts = array_fill_keys($categoryIds, 5); - $tableMock = $this->createMock(\Magento\Framework\DB\Ddl\Table::class); + + $store = $this->createMock(Store::class); + $store->method('getWebsiteId')->willReturn($websiteId); + $this->storeManager->method('getStore')->with($storeId)->willReturn($store); + + $indexedIds = array_slice($categoryIds, 0, 5); + $firstCounts = array_fill_keys($indexedIds, 5); + + $missingIds = array_diff($categoryIds, $indexedIds); + $fallbackCounts = array_fill_keys($missingIds, 5); + + $this->connection->method('fetchPairs') + ->willReturnOnConsecutiveCalls($firstCounts, $fallbackCounts); + + $tableMock = $this->createMock(Table::class); $tableMock->method('addColumn')->willReturnSelf(); $tableMock->method('addIndex')->willReturnSelf(); - $this->connection->method('newTable') - ->with($this->stringContains('temp_category_descendants_')) - ->willReturn($tableMock); + + $this->connection->method('newTable')->willReturn($tableMock); $this->connection->expects($this->once())->method('createTemporaryTable')->with($tableMock); - $this->connection->expects($this->once())->method('dropTemporaryTable') - ->with($this->stringContains('temp_category_descendants_')); - $this->select->method('from')->willReturnSelf(); - $this->select->expects($this->once())->method('joinInner') - ->with( - ['ce2' => null], - 'ce2.path LIKE CONCAT(ce.path, \'/%\')', - [] - )->willReturnSelf(); - $this->select->method('where')->willReturnSelf(); - $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('dropTemporaryTable'); + + $this->connection->method('insertFromSelect')->willReturn('SQL'); + $this->connection->method('query')->with('SQL'); + + $expectedData = []; + foreach ($missingIds as $id) { + $expectedData[] = ['category_id' => $id, 'descendant_id' => $id]; } - $this->connection - ->expects($this->once()) - ->method('insertMultiple') - ->with( - $this->stringContains('temp_category_descendants_'), - $withs - ); + $this->connection->expects($this->once())->method('insertMultiple') + ->with($this->stringContains('temp_category_descendants_'), $expectedData); + + $this->connection->method('select')->willReturn($this->select); $this->select->method('from')->willReturnSelf(); + $this->select->method('joinInner')->willReturnSelf(); $this->select->method('joinLeft')->willReturnSelf(); $this->select->method('join')->willReturnSelf(); $this->select->method('where')->willReturnSelf(); $this->select->method('group')->willReturnSelf(); - $this->connection->method('fetchPairs') - ->with($this->isInstanceOf(Select::class)) - ->willReturn($counts); + $this->collection->setProductStoreId($storeId); $this->collection->loadProductCount($items, false, true); } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Category/CollectionTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Category/CollectionTest.php index c1ea8fafe8ba5..b4b166b678e74 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Category/CollectionTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Category/CollectionTest.php @@ -7,7 +7,9 @@ namespace Magento\Catalog\Model\ResourceModel\Category; +use Magento\Catalog\Test\Fixture\Category as CategoryFixture; use Magento\Catalog\Test\Fixture\CategoryTreeWithProducts as CategoryTreeWithProductsFixture; +use Magento\Catalog\Test\Fixture\Product as ProductFixture; use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Fixture\AppArea; use Magento\TestFramework\Fixture\AppIsolation; @@ -86,46 +88,47 @@ public function testJoinUrlRewriteNotOnDefaultStore() } #[ - DataFixture( - CategoryTreeWithProductsFixture::class, - [ - 'category_identifier' => 'bulk_test_123_cat', - 'category_count' => 401, - 'product_identifier' => 'bulk_test_123_prd', - 'product_count' => 20, - 'depth' => 3 - ], - 'cats' - ), + DataFixture(CategoryFixture::class, ['name' => 'TC L1 Root', 'parent_id' => '2', 'is_anchor' => 1], 'c1'), + DataFixture(CategoryFixture::class, ['name' => 'TC L2 A', 'parent_id' => '$c1.id$', 'is_anchor' => 1], 'c11'), + DataFixture(CategoryFixture::class, ['name' => 'TC L2 B', 'parent_id' => '$c1.id$', 'is_anchor' => 1], 'c12'), + DataFixture(CategoryFixture::class, ['name' => 'TC L2 C', 'parent_id' => '$c1.id$', 'is_anchor' => 0], 'c13'), + DataFixture(CategoryFixture::class, ['name' => 'TC L3 A1', 'parent_id' => '$c11.id$', 'is_anchor' => 1], 'c1111'), + DataFixture(CategoryFixture::class, ['name' => 'TC L3 A2', 'parent_id' => '$c11.id$', 'is_anchor' => 1], 'c1112'), + DataFixture(CategoryFixture::class, ['name' => 'TC L3 C1', 'parent_id' => '$c13.id$', 'is_anchor' => 0], 'c1113'), + + DataFixture(ProductFixture::class, ['sku' => 'TP-1A', 'category_ids' => ['$c12.id$']], as: 'p1'), + DataFixture(ProductFixture::class, ['sku' => 'TP-2A', 'category_ids' => ['$c1111.id$']], as: 'p2'), + DataFixture(ProductFixture::class, ['sku' => 'TP-3B', 'category_ids' => ['$c1112.id$', '$c1113.id$']], as: 'p3'), + DataFixture(ProductFixture::class, ['sku' => 'TP-4B', 'category_ids' => ['$c1112.id$', '$c1113.id$']], as: 'p4'), + AppArea('adminhtml'), DbIsolation(true), AppIsolation(true) ] - public function testBulkProcessingModeIsTriggered() + public function testLoadProductCountWithoutIndex() { - /** @var CategoryCollection $collection */ $collection = $this->categoryCollectionFactory->create(); - $collection->addAttributeToSelect('*'); - $collection->addAttributeToFilter('name', ['like' => 'bulk_test_123_cat%']); + $collection->addAttributeToSelect(['name', 'is_anchor']); + $collection->addAttributeToFilter('name', ['like' => 'TC L%']); $collection->setLoadProductCount(true); $collection->load(); - $this->assertGreaterThan( - 400, - $collection->getSize(), - 'Bulk limit path not triggered.' - ); + $expected = [ + 'TC L1 Root' => 4, + 'TC L2 A' => 3, + 'TC L2 B' => 1, + 'TC L2 C' => 0, + 'TC L3 A1' => 1, + 'TC L3 A2' => 2, + 'TC L3 C1' => 2 + ]; 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()) + $name = $category->getName(); + $this->assertEquals( + $expected[$name], + (int)$category->getProductCount(), + "Product count incorrect for category $name" ); } } From 4d90a39a0d9f12e4f2d2e6f77afaff28449fed30 Mon Sep 17 00:00:00 2001 From: Mohamed El Mrabet Date: Tue, 25 Nov 2025 19:48:22 +0100 Subject: [PATCH 14/14] fix issue check --- .../ResourceModel/Category/Collection.php | 13 ++++--- .../ResourceModel/Category/CollectionTest.php | 2 - .../ResourceModel/Category/CollectionTest.php | 38 ++++++++++++------- 3 files changed, 31 insertions(+), 22 deletions(-) diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php index 53ddc8394949c..68b242420ef43 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php @@ -345,12 +345,13 @@ public function loadProductCount($items, $countRegular = true, $countAnchor = tr // Find categories missing from the SQL result $categoriesIdsAlreadyIndexed = array_keys($categoryProductsCount); $missingCategoryIds = array_diff($categoryIds, $categoriesIdsAlreadyIndexed); - - $countFromCategoryTable = $this->getCountFromCategoryTableBulk( - $missingCategoryIds, - (int) $websiteId - ); - + $countFromCategoryTable = []; + if (count($missingCategoryIds)) { + $countFromCategoryTable = $this->getCountFromCategoryTableBulk( + $missingCategoryIds, + (int) $websiteId + ); + } foreach ($anchor as $item) { $id = $item->getId(); $productsCount = $categoryProductsCount[$id] diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Category/CollectionTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Category/CollectionTest.php index 8d09026d384e7..76b86976ded02 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Category/CollectionTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Category/CollectionTest.php @@ -11,9 +11,7 @@ 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\Eav\Model\Entity\Attribute\AttributeInterface; use Magento\Eav\Model\EntityFactory as EavEntityFactory; use Magento\Eav\Model\ResourceModel\Helper; use Magento\Framework\App\Config\ScopeConfigInterface; diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Category/CollectionTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Category/CollectionTest.php index b4b166b678e74..10e2494b89943 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Category/CollectionTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Category/CollectionTest.php @@ -7,8 +7,8 @@ namespace Magento\Catalog\Model\ResourceModel\Category; +use Magento\Catalog\Model\Category; use Magento\Catalog\Test\Fixture\Category as CategoryFixture; -use Magento\Catalog\Test\Fixture\CategoryTreeWithProducts as CategoryTreeWithProductsFixture; use Magento\Catalog\Test\Fixture\Product as ProductFixture; use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Fixture\AppArea; @@ -28,7 +28,7 @@ class CollectionTest extends TestCase { /** - * @var Collection + * @var CategoryCollection */ private Collection $collection; @@ -44,7 +44,7 @@ class CollectionTest extends TestCase protected function setUp(): void { $objectManager = Bootstrap::getObjectManager(); - $this->collection = Bootstrap::getObjectManager()->create(Collection::class); + $this->collection = Bootstrap::getObjectManager()->create(CategoryCollection::class); $this->categoryCollectionFactory = $objectManager->get(CollectionFactory::class); } @@ -65,7 +65,7 @@ public function testJoinUrlRewriteOnDefault() { $categories = $this->collection->joinUrlRewrite()->addPathFilter('1/2/3'); $this->assertCount(1, $categories); - /** @var $category \Magento\Catalog\Model\Category */ + /** @var $category Category */ $category = $categories->getFirstItem(); $this->assertStringEndsWith('category.html', $category->getUrl()); } @@ -82,7 +82,7 @@ public function testJoinUrlRewriteNotOnDefaultStore() $storeId = $store->load('second_category_store', 'code')->getId(); $categories = $this->collection->setStoreId($storeId)->joinUrlRewrite()->addPathFilter('1/2/3'); $this->assertCount(1, $categories); - /** @var $category \Magento\Catalog\Model\Category */ + /** @var $category Category */ $category = $categories->getFirstItem(); $this->assertStringEndsWith('category-3-on-2.html', $category->getUrl()); } @@ -92,15 +92,25 @@ public function testJoinUrlRewriteNotOnDefaultStore() DataFixture(CategoryFixture::class, ['name' => 'TC L2 A', 'parent_id' => '$c1.id$', 'is_anchor' => 1], 'c11'), DataFixture(CategoryFixture::class, ['name' => 'TC L2 B', 'parent_id' => '$c1.id$', 'is_anchor' => 1], 'c12'), DataFixture(CategoryFixture::class, ['name' => 'TC L2 C', 'parent_id' => '$c1.id$', 'is_anchor' => 0], 'c13'), - DataFixture(CategoryFixture::class, ['name' => 'TC L3 A1', 'parent_id' => '$c11.id$', 'is_anchor' => 1], 'c1111'), - DataFixture(CategoryFixture::class, ['name' => 'TC L3 A2', 'parent_id' => '$c11.id$', 'is_anchor' => 1], 'c1112'), - DataFixture(CategoryFixture::class, ['name' => 'TC L3 C1', 'parent_id' => '$c13.id$', 'is_anchor' => 0], 'c1113'), - - DataFixture(ProductFixture::class, ['sku' => 'TP-1A', 'category_ids' => ['$c12.id$']], as: 'p1'), - DataFixture(ProductFixture::class, ['sku' => 'TP-2A', 'category_ids' => ['$c1111.id$']], as: 'p2'), - DataFixture(ProductFixture::class, ['sku' => 'TP-3B', 'category_ids' => ['$c1112.id$', '$c1113.id$']], as: 'p3'), - DataFixture(ProductFixture::class, ['sku' => 'TP-4B', 'category_ids' => ['$c1112.id$', '$c1113.id$']], as: 'p4'), - + DataFixture( + CategoryFixture::class, + ['name' => 'TC L3 A1', 'parent_id' => '$c11.id$', 'is_anchor' => 1], + 'c1111' + ), + DataFixture( + CategoryFixture::class, + ['name' => 'TC L3 A2', 'parent_id' => '$c11.id$', 'is_anchor' => 1], + 'c1112' + ), + DataFixture( + CategoryFixture::class, + ['name' => 'TC L3 C1', 'parent_id' => '$c13.id$', 'is_anchor' => 0], + 'c1113' + ), + DataFixture(ProductFixture::class, ['sku' => 'TP-1A', 'category_ids' => ['$c12.id$']], 'p1'), + DataFixture(ProductFixture::class, ['sku' => 'TP-2A', 'category_ids' => ['$c1111.id$']], 'p2'), + DataFixture(ProductFixture::class, ['sku' => 'TP-3B', 'category_ids' => ['$c1112.id$', '$c1113.id$']], 'p3'), + DataFixture(ProductFixture::class, ['sku' => 'TP-4B', 'category_ids' => ['$c1112.id$', '$c1113.id$']], 'p4'), AppArea('adminhtml'), DbIsolation(true), AppIsolation(true)