Skip to content

Commit 4ced4a7

Browse files
committed
ACP2E-3879: [Mainline] Layered Navigation display options assigned to other stores (or not assigned)
- Initial commit with test cases
1 parent 4ca7360 commit 4ced4a7

File tree

5 files changed

+363
-3
lines changed

5 files changed

+363
-3
lines changed
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\ConfigurableProduct\Model\ResourceModel\Product;
9+
10+
use Exception;
11+
use Magento\Catalog\Api\Data\ProductInterface;
12+
use Magento\Framework\EntityManager\MetadataPool;
13+
use Magento\Framework\Model\ResourceModel\Db\AbstractDb;
14+
use Magento\Framework\Model\ResourceModel\Db\Context;
15+
16+
/**
17+
* Get child product ids from store id and parent product id
18+
*/
19+
class GetStoreSpecificProductChildIds extends AbstractDb
20+
{
21+
/**
22+
* @var MetadataPool
23+
*/
24+
private $metadataPool;
25+
26+
/**
27+
* Constructor
28+
*
29+
*
30+
* @param MetadataPool $metadataPool
31+
* @param Context $context
32+
* @param string $connectionName
33+
*/
34+
public function __construct(
35+
MetadataPool $metadataPool,
36+
Context $context,
37+
$connectionName = null
38+
) {
39+
$this->metadataPool = $metadataPool;
40+
parent::__construct($context, $connectionName);
41+
}
42+
43+
/**
44+
* Load catalog_product_entity model
45+
*
46+
* @return void
47+
*/
48+
public function _construct()
49+
{
50+
$this->_init('catalog_product_entity', 'entity_id');
51+
}
52+
53+
/**
54+
* Process the child product ids based on store id and parent product id
55+
*
56+
* @param array $productData
57+
* @param int $websiteId
58+
* @return array
59+
* @throws Exception
60+
*/
61+
public function process(array $productData, int $websiteId): array
62+
{
63+
$connection = $this->getConnection();
64+
$entityMetadata = $this->metadataPool->getMetadata(ProductInterface::class);
65+
$linkField = $entityMetadata->getLinkField();
66+
67+
$select = $connection->select()
68+
->from(
69+
['cpe' => $this->getTable('catalog_product_entity')],
70+
[]
71+
)
72+
->join(
73+
['cpw' => $this->getTable('catalog_product_website')],
74+
'cpe.entity_id = cpw.product_id',
75+
[]
76+
)
77+
->join(
78+
['cpsl' => $this->getTable('catalog_product_super_link')],
79+
'cpe.entity_id = cpsl.product_id',
80+
['product_id']
81+
)
82+
->where('cpsl.parent_id = ?', (int) $productData[$linkField])
83+
->where('cpw.website_id = ?', $websiteId);
84+
85+
$result = $connection->fetchAll($select);
86+
return array_column($result, 'product_id');
87+
}
88+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\ConfigurableProduct\Plugin\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider;
9+
10+
use Magento\CatalogInventory\Api\StockConfigurationInterface;
11+
use Magento\Catalog\Api\ProductRepositoryInterface;
12+
use Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider;
13+
use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
14+
use Magento\ConfigurableProduct\Model\ResourceModel\Product\GetStoreSpecificProductChildIds;
15+
use Magento\Framework\Exception\NoSuchEntityException;
16+
use Magento\Store\Model\StoreManagerInterface;
17+
18+
/**
19+
* Filter out store specific for configurable product.
20+
*/
21+
class GetProductChildIds
22+
{
23+
/**
24+
* @var StoreManagerInterface
25+
*/
26+
private $storeManager;
27+
28+
/**
29+
* @var StockConfigurationInterface
30+
*/
31+
private $stockConfiguration;
32+
33+
/**
34+
* @var GetStoreSpecificProductChildIds
35+
*/
36+
private $getChildProductFromStoreId;
37+
38+
/**
39+
* @var ProductRepositoryInterface
40+
*/
41+
private $productRepository;
42+
43+
/**
44+
* @param StoreManagerInterface $storeManager
45+
* @param StockConfigurationInterface $stockConfiguration
46+
* @param GetStoreSpecificProductChildIds $getChildProductFromStoreId
47+
* @param ProductRepositoryInterface $productRepository
48+
*/
49+
public function __construct(
50+
StoreManagerInterface $storeManager,
51+
StockConfigurationInterface $stockConfiguration,
52+
GetStoreSpecificProductChildIds $getChildProductFromStoreId,
53+
ProductRepositoryInterface $productRepository
54+
) {
55+
$this->storeManager = $storeManager;
56+
$this->stockConfiguration = $stockConfiguration;
57+
$this->getChildProductFromStoreId = $getChildProductFromStoreId;
58+
$this->productRepository = $productRepository;
59+
}
60+
61+
/**
62+
* Filter out store specific for configurable product.
63+
*
64+
* @param DataProvider $dataProvider
65+
* @param array $indexData
66+
* @param array $productData
67+
* @param int $storeId
68+
* @return array
69+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
70+
* @throws NoSuchEntityException
71+
*/
72+
public function beforePrepareProductIndex(
73+
DataProvider $dataProvider,
74+
array $indexData,
75+
array $productData,
76+
int $storeId
77+
) {
78+
if (!$this->stockConfiguration->isShowOutOfStock($storeId) &&
79+
Configurable::TYPE_CODE === $productData['type_id']) {
80+
$websiteId = $this->storeManager->getStore($storeId)->getWebsiteId();
81+
$product = $this->productRepository->getById($productData['entity_id']);
82+
83+
if ($product->isVisibleInSiteVisibility()) {
84+
$childProductIds = $this->getChildProductFromStoreId->process(
85+
$product->getData(),
86+
(int) $websiteId
87+
);
88+
if (!empty($childProductIds)) {
89+
$childProductIds[] = $productData['entity_id'];
90+
$indexData = array_intersect_key($indexData, array_flip($childProductIds));
91+
}
92+
}
93+
}
94+
95+
return [
96+
$indexData,
97+
$productData,
98+
$storeId,
99+
];
100+
}
101+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\ConfigurableProduct\Test\Unit\Model\Plugin\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider;
9+
10+
use Magento\Catalog\Api\ProductRepositoryInterface;
11+
use Magento\Catalog\Model\Product;
12+
use Magento\CatalogInventory\Api\StockConfigurationInterface;
13+
use Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider;
14+
use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
15+
use Magento\ConfigurableProduct\Model\ResourceModel\Product\GetStoreSpecificProductChildIds;
16+
use Magento\ConfigurableProduct\Plugin\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider\GetProductChildIds;
17+
use Magento\Framework\Exception\NoSuchEntityException;
18+
use Magento\Store\Api\Data\StoreInterface;
19+
use Magento\Store\Model\StoreManagerInterface;
20+
use PHPUnit\Framework\MockObject\Exception;
21+
use PHPUnit\Framework\MockObject\MockObject;
22+
use PHPUnit\Framework\TestCase;
23+
24+
class GetProductChildIdsTest extends TestCase
25+
{
26+
/**
27+
* @var StoreManagerInterface|MockObject
28+
*/
29+
private $storeManagerMock;
30+
31+
/**
32+
* @var StockConfigurationInterface|MockObject
33+
*/
34+
private $stockConfigurationMock;
35+
36+
/**
37+
* @var GetStoreSpecificProductChildIds|MockObject
38+
*/
39+
private $getChildProductFromStoreIdMock;
40+
41+
/**
42+
* @var ProductRepositoryInterface|MockObject
43+
*/
44+
private $productRepositoryMock;
45+
46+
/**
47+
* @var GetProductChildIds
48+
*/
49+
private $plugin;
50+
51+
protected function setUp(): void
52+
{
53+
$this->storeManagerMock = $this->createMock(StoreManagerInterface::class);
54+
$this->stockConfigurationMock = $this->createMock(StockConfigurationInterface::class);
55+
$this->getChildProductFromStoreIdMock = $this->createMock(GetStoreSpecificProductChildIds::class);
56+
$this->productRepositoryMock = $this->getMockBuilder(ProductRepositoryInterface::class)
57+
->disableOriginalConstructor()
58+
->onlyMethods(['getById'])
59+
->getMockForAbstractClass();
60+
61+
$this->plugin = new GetProductChildIds(
62+
$this->storeManagerMock,
63+
$this->stockConfigurationMock,
64+
$this->getChildProductFromStoreIdMock,
65+
$this->productRepositoryMock
66+
);
67+
}
68+
69+
/**
70+
* Test case for beforePrepareProductIndex method with child product visibility and website check.
71+
*
72+
* @return void
73+
* @throws NoSuchEntityException
74+
* @throws Exception
75+
*/
76+
public function testBeforePrepareProductIndexWithChildProductVisibilityAndWebsiteCheck(): void
77+
{
78+
$dataProviderMock = $this->createMock(DataProvider::class);
79+
$indexData = [
80+
1 => ['data'],
81+
2 => ['data']
82+
];
83+
$productData = [
84+
'entity_id' => '1',
85+
'type_id' => Configurable::TYPE_CODE,
86+
];
87+
$storeId = 1;
88+
$websiteId = 2;
89+
90+
$storeMock = $this->createMock(StoreInterface::class);
91+
$storeMock->expects($this->once())
92+
->method('getWebsiteId')
93+
->willReturn($websiteId);
94+
95+
$this->storeManagerMock->expects($this->once())
96+
->method('getStore')
97+
->with($storeId)
98+
->willReturn($storeMock);
99+
100+
$this->stockConfigurationMock->expects($this->once())
101+
->method('isShowOutOfStock')
102+
->with($storeId)
103+
->willReturn(false);
104+
105+
$productMock = $this->createMock(Product::class);
106+
$productMock->expects($this->any())
107+
->method('isVisibleInSiteVisibility')
108+
->willReturn(true);
109+
$productMock->expects($this->once())
110+
->method('getData')
111+
->willReturn(['entity_id' => 1]);
112+
$productMock->expects($this->any())
113+
->method('getWebsiteIds')
114+
->willReturn([2]);
115+
116+
$this->productRepositoryMock->expects($this->once())
117+
->method('getById')
118+
->with($productData['entity_id'])
119+
->willReturn($productMock);
120+
121+
$this->getChildProductFromStoreIdMock->expects($this->once())
122+
->method('process')
123+
->with(['entity_id' => 1], $websiteId)
124+
->willReturn([2, 3]);
125+
126+
$childProductMock1 = $this->getMockBuilder(Product::class)
127+
->disableOriginalConstructor()
128+
->onlyMethods(['isVisibleInSiteVisibility', 'getWebsiteIds'])
129+
->getMock();
130+
$childProductMock1->expects($this->any())
131+
->method('isVisibleInSiteVisibility')
132+
->willReturn(true);
133+
$childProductMock1->expects($this->any())
134+
->method('getWebsiteIds')
135+
->willReturn([2]);
136+
137+
$childProductMock2 = $this->getMockBuilder(Product::class)
138+
->disableOriginalConstructor()
139+
->onlyMethods(['isVisibleInSiteVisibility', 'getWebsiteIds'])
140+
->getMock();
141+
$childProductMock2->expects($this->any())
142+
->method('isVisibleInSiteVisibility')
143+
->willReturn(false);
144+
145+
$this->productRepositoryMock->expects($this->any())
146+
->method('getById')
147+
->will($this->returnCallback(
148+
function ($id) use ($childProductMock1, $childProductMock2) {
149+
return $id === 2 ? $childProductMock1 : $childProductMock2;
150+
}
151+
));
152+
153+
$result = $this->plugin->beforePrepareProductIndex(
154+
$dataProviderMock,
155+
$indexData,
156+
$productData,
157+
$storeId
158+
);
159+
160+
$expectedIndexData = [
161+
1 => ['data'],
162+
2 => ['data'],
163+
];
164+
165+
$this->assertEquals([$expectedIndexData, $productData, $storeId], $result);
166+
}
167+
}

app/code/Magento/ConfigurableProduct/composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
"magento/module-configurable-sample-data": "*",
2828
"magento/module-product-links-sample-data": "*",
2929
"magento/module-tax": "*",
30-
"magento/module-catalog-widget": "*"
30+
"magento/module-catalog-widget": "*",
31+
"magento/module-catalog-search": "*"
3132
},
3233
"type": "magento2-module",
3334
"license": [

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
<?xml version="1.0"?>
22
<!--
33
/**
4-
* Copyright © Magento, Inc. All rights reserved.
5-
* See COPYING.txt for license details.
4+
* Copyright 2014 Adobe
5+
* All Rights Reserved.
66
*/
77
-->
88
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
@@ -288,4 +288,7 @@
288288
<type name="Magento\Catalog\Model\Product\Attribute\Backend\TierPrice\UpdateHandler">
289289
<plugin name="tier_price_update_handler_plugin" type="Magento\ConfigurableProduct\Plugin\Catalog\Model\Product\Attribute\Backend\TierPrice\UpdateHandlerPlugin" sortOrder="10" disabled="false"/>
290290
</type>
291+
<type name="Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider">
292+
<plugin name="storeSpecificConfigurableProductFromParentId" type="Magento\ConfigurableProduct\Plugin\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider\GetProductChildIds"/>
293+
</type>
291294
</config>

0 commit comments

Comments
 (0)