Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
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);
}