Skip to content

Commit 9be517f

Browse files
committed
Merge remote-tracking branch 'adobe-commerce-tier-4/ACP2E-3879' into PR_2025_06_25_muntianu
2 parents 91720aa + 9aa1d4e commit 9be517f

File tree

5 files changed

+339
-3
lines changed

5 files changed

+339
-3
lines changed
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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+
return $connection->fetchCol($select);
86+
}
87+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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\Catalog\Api\ProductRepositoryInterface;
11+
use Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider;
12+
use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
13+
use Magento\ConfigurableProduct\Model\ResourceModel\Product\GetStoreSpecificProductChildIds;
14+
use Magento\Framework\Exception\NoSuchEntityException;
15+
use Magento\Store\Model\StoreManagerInterface;
16+
17+
/**
18+
* Filter out store specific for configurable product.
19+
*/
20+
class GetProductChildIds
21+
{
22+
/**
23+
* @var StoreManagerInterface
24+
*/
25+
private $storeManager;
26+
27+
/**
28+
* @var GetStoreSpecificProductChildIds
29+
*/
30+
private $getChildProductFromStoreId;
31+
32+
/**
33+
* @var ProductRepositoryInterface
34+
*/
35+
private $productRepository;
36+
37+
/**
38+
* @param StoreManagerInterface $storeManager
39+
* @param GetStoreSpecificProductChildIds $getChildProductFromStoreId
40+
* @param ProductRepositoryInterface $productRepository
41+
*/
42+
public function __construct(
43+
StoreManagerInterface $storeManager,
44+
GetStoreSpecificProductChildIds $getChildProductFromStoreId,
45+
ProductRepositoryInterface $productRepository
46+
) {
47+
$this->storeManager = $storeManager;
48+
$this->getChildProductFromStoreId = $getChildProductFromStoreId;
49+
$this->productRepository = $productRepository;
50+
}
51+
52+
/**
53+
* Filter out store specific for configurable product.
54+
*
55+
* @param DataProvider $dataProvider
56+
* @param array $indexData
57+
* @param array $productData
58+
* @param int $storeId
59+
* @return array
60+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
61+
* @throws NoSuchEntityException
62+
*/
63+
public function beforePrepareProductIndex(
64+
DataProvider $dataProvider,
65+
array $indexData,
66+
array $productData,
67+
int $storeId
68+
) {
69+
if (Configurable::TYPE_CODE === $productData['type_id']) {
70+
$websiteId = $this->storeManager->getStore($storeId)->getWebsiteId();
71+
$product = $this->productRepository->getById($productData['entity_id']);
72+
73+
if ($product->isVisibleInSiteVisibility()) {
74+
$childProductIds = $this->getChildProductFromStoreId->process(
75+
$product->getData(),
76+
(int) $websiteId
77+
);
78+
if (!empty($childProductIds)) {
79+
$childProductIds[] = $productData['entity_id'];
80+
$indexData = array_intersect_key($indexData, array_flip($childProductIds));
81+
}
82+
}
83+
}
84+
85+
return [
86+
$indexData,
87+
$productData,
88+
$storeId,
89+
];
90+
}
91+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
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\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\ConfigurableProduct\Plugin\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider\GetProductChildIds;
16+
use Magento\Framework\Exception\NoSuchEntityException;
17+
use Magento\Store\Api\Data\StoreInterface;
18+
use Magento\Store\Model\StoreManagerInterface;
19+
use PHPUnit\Framework\MockObject\Exception;
20+
use PHPUnit\Framework\MockObject\MockObject;
21+
use PHPUnit\Framework\TestCase;
22+
23+
class GetProductChildIdsTest extends TestCase
24+
{
25+
/**
26+
* @var StoreManagerInterface|MockObject
27+
*/
28+
private $storeManagerMock;
29+
30+
/**
31+
* @var GetStoreSpecificProductChildIds|MockObject
32+
*/
33+
private $getChildProductFromStoreIdMock;
34+
35+
/**
36+
* @var ProductRepositoryInterface|MockObject
37+
*/
38+
private $productRepositoryMock;
39+
40+
/**
41+
* @var GetProductChildIds
42+
*/
43+
private $plugin;
44+
45+
protected function setUp(): void
46+
{
47+
$this->storeManagerMock = $this->createMock(StoreManagerInterface::class);
48+
$this->getChildProductFromStoreIdMock = $this->createMock(GetStoreSpecificProductChildIds::class);
49+
$this->productRepositoryMock = $this->getMockBuilder(ProductRepositoryInterface::class)
50+
->disableOriginalConstructor()
51+
->onlyMethods(['getById'])
52+
->getMockForAbstractClass();
53+
54+
$this->plugin = new GetProductChildIds(
55+
$this->storeManagerMock,
56+
$this->getChildProductFromStoreIdMock,
57+
$this->productRepositoryMock
58+
);
59+
}
60+
61+
/**
62+
* Test case for beforePrepareProductIndex method with child product visibility and website check.
63+
*
64+
* @return void
65+
* @throws NoSuchEntityException
66+
* @throws Exception
67+
*/
68+
public function testBeforePrepareProductIndexWithChildProductVisibilityAndWebsiteCheck(): void
69+
{
70+
$dataProviderMock = $this->createMock(DataProvider::class);
71+
$indexData = [
72+
1 => ['data'],
73+
2 => ['data']
74+
];
75+
$productData = [
76+
'entity_id' => '1',
77+
'type_id' => Configurable::TYPE_CODE,
78+
];
79+
$storeId = 1;
80+
$websiteId = 2;
81+
82+
$storeMock = $this->createMock(StoreInterface::class);
83+
$storeMock->expects($this->once())
84+
->method('getWebsiteId')
85+
->willReturn($websiteId);
86+
87+
$this->storeManagerMock->expects($this->once())
88+
->method('getStore')
89+
->with($storeId)
90+
->willReturn($storeMock);
91+
92+
$productMock = $this->createMock(Product::class);
93+
$productMock->expects($this->any())
94+
->method('isVisibleInSiteVisibility')
95+
->willReturn(true);
96+
$productMock->expects($this->once())
97+
->method('getData')
98+
->willReturn(['entity_id' => 1]);
99+
$productMock->expects($this->any())
100+
->method('getWebsiteIds')
101+
->willReturn([2]);
102+
103+
$this->productRepositoryMock->expects($this->once())
104+
->method('getById')
105+
->with($productData['entity_id'])
106+
->willReturn($productMock);
107+
108+
$this->getChildProductFromStoreIdMock->expects($this->once())
109+
->method('process')
110+
->with(['entity_id' => 1], $websiteId)
111+
->willReturn([2, 3]);
112+
113+
$childProductMock1 = $this->getMockBuilder(Product::class)
114+
->disableOriginalConstructor()
115+
->onlyMethods(['isVisibleInSiteVisibility', 'getWebsiteIds'])
116+
->getMock();
117+
$childProductMock1->expects($this->any())
118+
->method('isVisibleInSiteVisibility')
119+
->willReturn(true);
120+
$childProductMock1->expects($this->any())
121+
->method('getWebsiteIds')
122+
->willReturn([2]);
123+
124+
$childProductMock2 = $this->getMockBuilder(Product::class)
125+
->disableOriginalConstructor()
126+
->onlyMethods(['isVisibleInSiteVisibility', 'getWebsiteIds'])
127+
->getMock();
128+
$childProductMock2->expects($this->any())
129+
->method('isVisibleInSiteVisibility')
130+
->willReturn(false);
131+
132+
$this->productRepositoryMock->expects($this->any())
133+
->method('getById')
134+
->will($this->returnCallback(
135+
function ($id) use ($childProductMock1, $childProductMock2) {
136+
return $id === 2 ? $childProductMock1 : $childProductMock2;
137+
}
138+
));
139+
140+
$result = $this->plugin->beforePrepareProductIndex(
141+
$dataProviderMock,
142+
$indexData,
143+
$productData,
144+
$storeId
145+
);
146+
147+
$expectedIndexData = [
148+
1 => ['data'],
149+
2 => ['data'],
150+
];
151+
152+
$this->assertEquals([$expectedIndexData, $productData, $storeId], $result);
153+
}
154+
}

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)