Skip to content

Commit 2487db1

Browse files
committed
MC-19689: Simple product disappearing in the configurable grid after qty set to 0
1 parent c243fc7 commit 2487db1

File tree

4 files changed

+213
-19
lines changed

4 files changed

+213
-19
lines changed
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\ConfigurableProduct\Model\Plugin\Frontend;
9+
10+
use Magento\Catalog\Api\Data\ProductInterface;
11+
use Magento\Catalog\Api\Data\ProductInterfaceFactory;
12+
use Magento\Catalog\Model\Category;
13+
use Magento\Catalog\Model\Product;
14+
use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
15+
use Magento\Customer\Model\Session;
16+
use Magento\Framework\Cache\FrontendInterface;
17+
use Magento\Framework\EntityManager\MetadataPool;
18+
use Magento\Framework\Serialize\SerializerInterface;
19+
20+
/**
21+
* Cache of used products for configurable product
22+
*/
23+
class UsedProductsCache
24+
{
25+
/**
26+
* @var MetadataPool
27+
*/
28+
private $metadataPool;
29+
30+
/**
31+
* @var FrontendInterface
32+
*/
33+
private $cache;
34+
35+
/**
36+
* @var SerializerInterface
37+
*/
38+
private $serializer;
39+
40+
/**
41+
* @var ProductInterfaceFactory
42+
*/
43+
private $productFactory;
44+
45+
/**
46+
* @var Session
47+
*/
48+
private $customerSession;
49+
50+
/**
51+
* @param MetadataPool $metadataPool
52+
* @param FrontendInterface $cache
53+
* @param SerializerInterface $serializer
54+
* @param ProductInterfaceFactory $productFactory
55+
* @param Session $customerSession
56+
*/
57+
public function __construct(
58+
MetadataPool $metadataPool,
59+
FrontendInterface $cache,
60+
SerializerInterface $serializer,
61+
ProductInterfaceFactory $productFactory,
62+
Session $customerSession
63+
) {
64+
$this->metadataPool = $metadataPool;
65+
$this->cache = $cache;
66+
$this->serializer = $serializer;
67+
$this->productFactory = $productFactory;
68+
$this->customerSession = $customerSession;
69+
}
70+
71+
/**
72+
* Retrieve used products for configurable product
73+
*
74+
* @param Configurable $subject
75+
* @param callable $proceed
76+
* @param Product $product
77+
* @param array|null $requiredAttributeIds
78+
* @return ProductInterface[]
79+
*/
80+
public function aroundGetUsedProducts(
81+
Configurable $subject,
82+
callable $proceed,
83+
$product,
84+
$requiredAttributeIds = null
85+
) {
86+
$cacheKey = $this->getCacheKey($product, $requiredAttributeIds);
87+
$usedProducts = $this->readUsedProductsCacheData($cacheKey);
88+
if ($usedProducts === null) {
89+
$usedProducts = $proceed($product, $requiredAttributeIds);
90+
$this->saveUsedProductsCacheData($product, $usedProducts, $cacheKey);
91+
}
92+
93+
return $usedProducts;
94+
}
95+
96+
/**
97+
* Generate cache key for product
98+
*
99+
* @param Product $product
100+
* @param array|null $requiredAttributeIds
101+
* @return string
102+
*/
103+
private function getCacheKey($product, $requiredAttributeIds = null): string
104+
{
105+
$metadata = $this->metadataPool->getMetadata(ProductInterface::class);
106+
$keyParts = [
107+
'getUsedProducts',
108+
$product->getData($metadata->getLinkField()),
109+
$product->getStoreId(),
110+
$this->customerSession->getCustomerGroupId(),
111+
];
112+
if ($requiredAttributeIds !== null) {
113+
sort($requiredAttributeIds);
114+
$keyParts[] = implode('', $requiredAttributeIds);
115+
}
116+
$cacheKey = sha1(implode('_', $keyParts));
117+
118+
return $cacheKey;
119+
}
120+
121+
/**
122+
* Read used products data from cache
123+
*
124+
* Looking for cache record stored under provided $cacheKey
125+
* In case data exists turns it into array of products
126+
*
127+
* @param string $cacheKey
128+
* @return ProductInterface[]|null
129+
*/
130+
private function readUsedProductsCacheData(string $cacheKey): ?array
131+
{
132+
$data = $this->cache->load($cacheKey);
133+
if (!$data) {
134+
return null;
135+
}
136+
137+
$items = $this->serializer->unserialize($data);
138+
if (!$items) {
139+
return null;
140+
}
141+
142+
$usedProducts = [];
143+
foreach ($items as $item) {
144+
/** @var Product $productItem */
145+
$productItem = $this->productFactory->create();
146+
$productItem->setData($item);
147+
$usedProducts[] = $productItem;
148+
}
149+
150+
return $usedProducts;
151+
}
152+
153+
/**
154+
* Save $subProducts to cache record identified with provided $cacheKey
155+
*
156+
* Cached data will be tagged with combined list of product tags and data specific tags i.e. 'price' etc.
157+
*
158+
* @param Product $product
159+
* @param ProductInterface[] $subProducts
160+
* @param string $cacheKey
161+
* @return bool
162+
*/
163+
private function saveUsedProductsCacheData(Product $product, array $subProducts, string $cacheKey): bool
164+
{
165+
$metadata = $this->metadataPool->getMetadata(ProductInterface::class);
166+
$data = $this->serializer->serialize(array_map(
167+
function ($item) {
168+
return $item->getData();
169+
},
170+
$subProducts
171+
));
172+
$tags = array_merge(
173+
$product->getIdentities(),
174+
[
175+
Category::CACHE_TAG,
176+
Product::CACHE_TAG,
177+
'price',
178+
Configurable::TYPE_CODE . '_' . $product->getData($metadata->getLinkField())
179+
]
180+
);
181+
$result = $this->cache->save($data, $cacheKey, $tags);
182+
183+
return (bool) $result;
184+
}
185+
}

app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1233,28 +1233,22 @@ public function isPossibleBuyFromList($product)
12331233
* Returns array of sub-products for specified configurable product
12341234
*
12351235
* $requiredAttributeIds - one dimensional array, if provided
1236-
*
12371236
* Result array contains all children for specified configurable product
12381237
*
1239-
* @param \Magento\Catalog\Model\Product $product
1240-
* @param array $requiredAttributeIds
1238+
* @param \Magento\Catalog\Model\Product $product
1239+
* @param array $requiredAttributeIds
12411240
* @return ProductInterface[]
1241+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
12421242
*/
12431243
public function getUsedProducts($product, $requiredAttributeIds = null)
12441244
{
1245-
$metadata = $this->getMetadataPool()->getMetadata(ProductInterface::class);
1246-
$keyParts = [
1247-
__METHOD__,
1248-
$product->getData($metadata->getLinkField()),
1249-
$product->getStoreId(),
1250-
$this->getCustomerSession()->getCustomerGroupId()
1251-
];
1252-
if ($requiredAttributeIds !== null) {
1253-
sort($requiredAttributeIds);
1254-
$keyParts[] = implode('', $requiredAttributeIds);
1245+
if (!$product->hasData($this->_usedProducts)) {
1246+
$collection = $this->getConfiguredUsedProductCollection($product, false);
1247+
$usedProducts = array_values($collection->getItems());
1248+
$product->setData($this->_usedProducts, $usedProducts);
12551249
}
1256-
$cacheKey = $this->getUsedProductsCacheKey($keyParts);
1257-
return $this->loadUsedProducts($product, $cacheKey);
1250+
1251+
return $product->getData($this->_usedProducts);
12581252
}
12591253

12601254
/**
@@ -1304,11 +1298,15 @@ private function loadUsedProducts(\Magento\Catalog\Model\Product $product, $cach
13041298
{
13051299
$dataFieldName = $salableOnly ? $this->usedSalableProducts : $this->_usedProducts;
13061300
if (!$product->hasData($dataFieldName)) {
1307-
$collection = $this->getConfiguredUsedProductCollection($product, false);
1308-
if ($salableOnly) {
1309-
$collection = $this->salableProcessor->process($collection);
1301+
$usedProducts = $this->readUsedProductsCacheData($cacheKey);
1302+
if ($usedProducts === null) {
1303+
$collection = $this->getConfiguredUsedProductCollection($product, false);
1304+
if ($salableOnly) {
1305+
$collection = $this->salableProcessor->process($collection);
1306+
}
1307+
$usedProducts = array_values($collection->getItems());
1308+
$this->saveUsedProductsCacheData($product, $usedProducts, $cacheKey);
13101309
}
1311-
$usedProducts = array_values($collection->getItems());
13121310
$product->setData($dataFieldName, $usedProducts);
13131311
}
13141312

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,4 +256,12 @@
256256
</argument>
257257
</arguments>
258258
</type>
259+
<type name="Magento\ConfigurableProduct\Model\Plugin\Frontend\UsedProductsCache">
260+
<arguments>
261+
<argument name="cache" xsi:type="object">Magento\Framework\App\Cache\Type\Collection</argument>
262+
</arguments>
263+
<arguments>
264+
<argument name="serializer" xsi:type="object">Magento\Framework\Serialize\Serializer\Json</argument>
265+
</arguments>
266+
</type>
259267
</config>

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,7 @@
1313
<type name="Magento\Catalog\Model\Product">
1414
<plugin name="product_identities_extender" type="Magento\ConfigurableProduct\Model\Plugin\Frontend\ProductIdentitiesExtender" />
1515
</type>
16+
<type name="Magento\ConfigurableProduct\Model\Product\Type\Configurable">
17+
<plugin name="used_products_cache" type="Magento\ConfigurableProduct\Model\Plugin\Frontend\UsedProductsCache" />
18+
</type>
1619
</config>

0 commit comments

Comments
 (0)