Skip to content

Commit 605aedc

Browse files
committed
ACP2E-2843: Products on the frontend use store specific data when Single-Store Mode is enabled
- solution with test coverage
1 parent a98a64a commit 605aedc

File tree

4 files changed

+338
-0
lines changed

4 files changed

+338
-0
lines changed
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
<?php
2+
/************************************************************************
3+
*
4+
* ADOBE CONFIDENTIAL
5+
* ___________________
6+
*
7+
* Copyright 2024 Adobe
8+
* All Rights Reserved.
9+
*
10+
* NOTICE: All information contained herein is, and remains
11+
* the property of Adobe and its suppliers, if any. The intellectual
12+
* and technical concepts contained herein are proprietary to Adobe
13+
* and its suppliers and are protected by all applicable intellectual
14+
* property laws, including trade secret and copyright laws.
15+
* Dissemination of this information or reproduction of this material
16+
* is strictly forbidden unless prior written permission is obtained
17+
* from Adobe.
18+
* ************************************************************************
19+
*/
20+
declare(strict_types=1);
21+
22+
namespace Magento\Catalog\Model\Product;
23+
24+
use Exception;
25+
use Magento\Framework\App\ResourceConnection;
26+
use Magento\Framework\EntityManager\MetadataPool;
27+
use Magento\Framework\Exception\CouldNotSaveException;
28+
use Magento\Framework\Exception\LocalizedException;
29+
use Magento\Store\Model\Store;
30+
31+
/**
32+
* Migrate related catalog category and product tables for single store view mode
33+
*/
34+
class CatalogCategoryAndProductResolverOnSingleStoreMode
35+
{
36+
/**
37+
* @param MetadataPool $metadataPool
38+
* @param ResourceConnection $resourceConnection
39+
*/
40+
public function __construct(
41+
private readonly MetadataPool $metadataPool,
42+
private readonly ResourceConnection $resourceConnection
43+
) {
44+
}
45+
46+
/**
47+
* Process the Catalog and Product tables and migrate to single store view mode
48+
*
49+
* @param int $storeId
50+
* @param string $table
51+
* @return void
52+
* @throws CouldNotSaveException
53+
*/
54+
private function process(int $storeId, string $table): void
55+
{
56+
$connection = $this->resourceConnection->getConnection();
57+
$catalogProductTable = $this->resourceConnection->getTableName($table);
58+
$select = $connection->select()
59+
->from($catalogProductTable, ['value_id', 'attribute_id', 'row_id'])
60+
->where('store_id = ?', $storeId);
61+
$catalogProducts = $connection->fetchAll($select);
62+
try {
63+
if ($catalogProducts) {
64+
foreach ($catalogProducts as $catalogProduct) {
65+
$connection->delete(
66+
$table,
67+
[
68+
'store_id = ?' => Store::DEFAULT_STORE_ID,
69+
'attribute_id = ?' => $catalogProduct['attribute_id'],
70+
'row_id = ?' => $catalogProduct['row_id']
71+
]
72+
);
73+
$connection->update(
74+
$table,
75+
['store_id' => Store::DEFAULT_STORE_ID],
76+
['value_id = ?' => $catalogProduct['value_id']]
77+
);
78+
}
79+
}
80+
} catch (LocalizedException $e) {
81+
throw new CouldNotSaveException(
82+
__($e->getMessage()),
83+
$e
84+
);
85+
}
86+
}
87+
88+
/**
89+
* Migrate catalog category and product tables
90+
*
91+
* @param int $storeId
92+
* @throws Exception
93+
*/
94+
public function migrateCatalogCategoryAndProductTables(int $storeId): void
95+
{
96+
$connection = $this->resourceConnection->getConnection();
97+
$tables = [
98+
'catalog_category_entity_datetime',
99+
'catalog_category_entity_decimal',
100+
'catalog_category_entity_int',
101+
'catalog_category_entity_text',
102+
'catalog_category_entity_varchar',
103+
'catalog_product_entity_datetime',
104+
'catalog_product_entity_decimal',
105+
'catalog_product_entity_gallery',
106+
'catalog_product_entity_int',
107+
'catalog_product_entity_text',
108+
'catalog_product_entity_varchar',
109+
];
110+
try {
111+
$connection->beginTransaction();
112+
foreach ($tables as $table) {
113+
$this->process($storeId, $table);
114+
}
115+
$connection->commit();
116+
} catch (Exception $exception) {
117+
$connection->rollBack();
118+
}
119+
}
120+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php
2+
/************************************************************************
3+
*
4+
* Copyright 2024 Adobe
5+
* All Rights Reserved.
6+
*
7+
* NOTICE: All information contained herein is, and remains
8+
* the property of Adobe and its suppliers, if any. The intellectual
9+
* and technical concepts contained herein are proprietary to Adobe
10+
* and its suppliers and are protected by all applicable intellectual
11+
* property laws, including trade secret and copyright laws.
12+
* Dissemination of this information or reproduction of this material
13+
* is strictly forbidden unless prior written permission is obtained
14+
* from Adobe.
15+
* ************************************************************************
16+
*/
17+
declare(strict_types=1);
18+
19+
namespace Magento\Catalog\Observer;
20+
21+
use Magento\Catalog\Model\Indexer\Category\Product;
22+
use Magento\Catalog\Model\Indexer\Product\Category as ProductCategoryIndexer;
23+
use Magento\CatalogRule\Model\Indexer\Rule\RuleProductProcessor;
24+
use Magento\Catalog\Model\Indexer\Product\Price\Processor as PriceIndexProcessor;
25+
use Magento\Catalog\Model\Product\CatalogCategoryAndProductResolverOnSingleStoreMode as Resolver;
26+
use Magento\Framework\App\Config\ScopeConfigInterface;
27+
use Magento\Framework\Event\Observer;
28+
use Magento\Framework\Event\ObserverInterface;
29+
use Magento\Framework\Indexer\IndexerRegistry;
30+
use Magento\Store\Model\StoreManager;
31+
use Magento\Store\Model\StoreManagerInterface;
32+
33+
/**
34+
* Move and migrate store level catalog product and category to website level
35+
*/
36+
class MoveStoreLevelCatalogDataToWebsiteScopeOnSingleStoreMode implements ObserverInterface
37+
{
38+
/**
39+
* @param IndexerRegistry $indexerRegistry
40+
* @param ScopeConfigInterface $scopeConfig
41+
* @param StoreManagerInterface $storeManager
42+
* @param Resolver $categoryAndProductResolver
43+
*/
44+
public function __construct(
45+
private readonly IndexerRegistry $indexerRegistry,
46+
private readonly ScopeConfigInterface $scopeConfig,
47+
private readonly StoreManagerInterface $storeManager,
48+
private readonly Resolver $categoryAndProductResolver
49+
) {
50+
}
51+
52+
/**
53+
* @inheritDoc
54+
*/
55+
public function execute(Observer $observer)
56+
{
57+
$changedPaths = (array)$observer->getEvent()->getChangedPaths();
58+
if (in_array(StoreManager::XML_PATH_SINGLE_STORE_MODE_ENABLED, $changedPaths, true)
59+
&& $this->scopeConfig->getValue(StoreManager::XML_PATH_SINGLE_STORE_MODE_ENABLED)
60+
) {
61+
$store = $this->storeManager->getDefaultStoreView();
62+
if ($store) {
63+
$storeId = $store->getId();
64+
$this->categoryAndProductResolver->migrateCatalogCategoryAndProductTables((int) $storeId);
65+
$this->invalidateIndexer();
66+
}
67+
}
68+
}
69+
70+
/**
71+
* Invalidate related indexer
72+
*/
73+
private function invalidateIndexer(): void
74+
{
75+
$productIndexer = $this->indexerRegistry->get(Product::INDEXER_ID);
76+
$categoryProductIndexer = $this->indexerRegistry->get(ProductCategoryIndexer::INDEXER_ID);
77+
$priceIndexer = $this->indexerRegistry->get(PriceIndexProcessor::INDEXER_ID);
78+
$ruleIndexer = $this->indexerRegistry->get(RuleProductProcessor::INDEXER_ID);
79+
$productIndexer->invalidate();
80+
$categoryProductIndexer->invalidate();
81+
$priceIndexer->invalidate();
82+
$ruleIndexer->invalidate();
83+
}
84+
}

app/code/Magento/Catalog/etc/events.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,7 @@
6767
<event name="catalog_category_prepare_save">
6868
<observer name="additional_authorization" instance="Magento\Catalog\Observer\CategoryDesignAuthorization" />
6969
</event>
70+
<event name="admin_system_config_changed_section_general">
71+
<observer name="move_store_level_catalog_data_to_website_scope_on_single_store_mode" instance="Magento\Catalog\Observer\MoveStoreLevelCatalogDataToWebsiteScopeOnSingleStoreMode" />
72+
</event>
7073
</config>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
<?php
2+
/************************************************************************
3+
*
4+
* Copyright 2024 Adobe
5+
* All Rights Reserved.
6+
*
7+
* NOTICE: All information contained herein is, and remains
8+
* the property of Adobe and its suppliers, if any. The intellectual
9+
* and technical concepts contained herein are proprietary to Adobe
10+
* and its suppliers and are protected by all applicable intellectual
11+
* property laws, including trade secret and copyright laws.
12+
* Dissemination of this information or reproduction of this material
13+
* is strictly forbidden unless prior written permission is obtained
14+
* from Adobe.
15+
* ************************************************************************
16+
*/
17+
declare(strict_types=1);
18+
19+
namespace Magento\Catalog\Observer;
20+
21+
use Magento\Catalog\Api\ProductRepositoryInterface;
22+
use Magento\Catalog\Test\Fixture\Category as CategoryFixture;
23+
use Magento\Catalog\Test\Fixture\Product as ProductFixture;
24+
use Magento\Framework\App\Config\ReinitableConfigInterface;
25+
use Magento\Framework\Event\ManagerInterface;
26+
use Magento\Framework\ObjectManagerInterface;
27+
use Magento\Store\Model\StoreManager;
28+
use Magento\TestFramework\Fixture\AppArea;
29+
use Magento\TestFramework\Fixture\DataFixture;
30+
use Magento\TestFramework\Fixture\DataFixtureStorage;
31+
use Magento\TestFramework\Fixture\DataFixtureStorageManager;
32+
use Magento\TestFramework\Fixture\DbIsolation;
33+
use Magento\TestFramework\Helper\Bootstrap;
34+
use Magento\TestFramework\Indexer\TestCase;
35+
use Magento\Framework\App\Config\ConfigResource\ConfigInterface;
36+
37+
/**
38+
* Test class for checking migrate store level catalog product to website level
39+
*/
40+
class MoveStoreLevelCatalogDataToWebsiteScopeOnSingleStoreModeTest extends TestCase
41+
{
42+
/**
43+
* @var ObjectManagerInterface
44+
*/
45+
private $objectManager;
46+
47+
/**
48+
* @var ProductRepositoryInterface
49+
*/
50+
private $productRepository;
51+
52+
/**
53+
* @var ConfigInterface
54+
*/
55+
private $config;
56+
57+
/**
58+
* @var DataFixtureStorage
59+
*/
60+
private $fixtures;
61+
62+
/**
63+
* @inheritdoc
64+
*/
65+
protected function setUp(): void
66+
{
67+
$this->objectManager = Bootstrap::getObjectManager();
68+
$this->productRepository = $this->objectManager->create(ProductRepositoryInterface::class);
69+
$this->fixtures = $this->objectManager->get(DataFixtureStorageManager::class)->getStorage();
70+
$this->config = $this->objectManager->get(ConfigInterface::class);
71+
parent::setUp();
72+
}
73+
74+
/**
75+
* Test class for checking migration of product from store level scope to website scope in
76+
* single store mode.
77+
*/
78+
#[
79+
DbIsolation(true),
80+
DataFixture(CategoryFixture::class, ['name' => 'Category1', 'parent_id' => '2'], 'c11'),
81+
DataFixture(
82+
ProductFixture::class,
83+
[
84+
'sku' => 'simple_product',
85+
'name' => 'simple product for all store view',
86+
'price' => 35,
87+
'website_ids' => [1],
88+
'category_ids' => ['$c11.id$']
89+
],
90+
'simple product for all store view'
91+
),
92+
AppArea('adminhtml')
93+
]
94+
public function testExecute(): void
95+
{
96+
$eventManager = $this->objectManager->get(ManagerInterface::class);
97+
$scopeConfig = $this->objectManager->get(ReinitableConfigInterface::class);
98+
$productFromFixture = $this->fixtures->get('simple product for all store view');
99+
100+
$product = $this->productRepository->get($productFromFixture->getSku());
101+
$this->assertEquals($productFromFixture->getName(), $product->getName());
102+
$this->assertEquals($productFromFixture->getPrice(), $product->getPrice());
103+
104+
$eventManager->dispatch(
105+
'admin_system_config_changed_section_general',
106+
[
107+
'website' => '',
108+
'store' => '',
109+
'changed_paths' => [
110+
StoreManager::XML_PATH_SINGLE_STORE_MODE_ENABLED
111+
],
112+
]
113+
);
114+
115+
$product->setName('simple product for default store view')->setStoreId(0);
116+
$this->productRepository->save($product);
117+
118+
$product = $this->productRepository->get($productFromFixture->getSku());
119+
$this->assertEquals('simple product for default store view', $product->getName());
120+
121+
$this->config->saveConfig('StoreManager::XML_PATH_SINGLE_STORE_MODE_ENABLED', 1);
122+
$scopeConfig->reinit();
123+
124+
$product = $this->productRepository->get($productFromFixture->getSku());
125+
$this->assertEquals('simple product for default store view', $product->getName());
126+
$this->assertEquals($productFromFixture->getPrice(), $product->getPrice());
127+
128+
$this->config->saveConfig('StoreManager::XML_PATH_SINGLE_STORE_MODE_ENABLED', 0);
129+
$scopeConfig->reinit();
130+
}
131+
}

0 commit comments

Comments
 (0)