Skip to content

Commit 88f95ef

Browse files
author
Yaroslav Onischenko
committed
MAGETWO-66515: [Performance] Reduce queries to stock_status_index on Category Page
2 parents 7c19f03 + 6fe5199 commit 88f95ef

File tree

2 files changed

+209
-90
lines changed

2 files changed

+209
-90
lines changed

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

Lines changed: 199 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,13 @@ class Configurable extends \Magento\Catalog\Model\Product\Type\AbstractType
7373
*/
7474
protected $_usedProducts = '_cache_instance_products';
7575

76+
/**
77+
* Cache key for salable used products
78+
*
79+
* @var string
80+
*/
81+
private $usedSalableProducts = '_cache_instance_salable_products';
82+
7683
/**
7784
* Product is composite
7885
*
@@ -166,10 +173,19 @@ class Configurable extends \Magento\Catalog\Model\Product\Type\AbstractType
166173
private $customerSession;
167174

168175
/**
176+
* Product factory
177+
*
169178
* @var ProductInterfaceFactory
170179
*/
171180
private $productFactory;
172181

182+
/**
183+
* Collection salable processor
184+
*
185+
* @var SalableProcessor
186+
*/
187+
private $salableProcessor;
188+
173189
/**
174190
* @codingStandardsIgnoreStart/End
175191
*
@@ -192,6 +208,7 @@ class Configurable extends \Magento\Catalog\Model\Product\Type\AbstractType
192208
* @param \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface $extensionAttributesJoinProcessor
193209
* @param \Magento\Framework\Serialize\Serializer\Json $serializer
194210
* @param ProductInterfaceFactory $productFactory
211+
* @param SalableProcessor $salableProcessor
195212
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
196213
*/
197214
public function __construct(
@@ -513,88 +530,6 @@ public function getUsedProductIds($product)
513530
return $product->getData($this->_usedProductIds);
514531
}
515532

516-
/**
517-
* Retrieve array of "subproducts"
518-
*
519-
* @param \Magento\Catalog\Model\Product $product
520-
* @param array $requiredAttributeIds
521-
* @return array
522-
*/
523-
public function getUsedProducts($product, $requiredAttributeIds = null)
524-
{
525-
\Magento\Framework\Profiler::start(
526-
'CONFIGURABLE:' . __METHOD__,
527-
['group' => 'CONFIGURABLE', 'method' => __METHOD__]
528-
);
529-
if (!$product->hasData($this->_usedProducts)) {
530-
$metadata = $this->getMetadataPool()->getMetadata(ProductInterface::class);
531-
$productId = $product->getData($metadata->getLinkField());
532-
533-
$key = md5(
534-
implode(
535-
'_',
536-
[
537-
__METHOD__,
538-
$productId,
539-
$product->getStoreId(),
540-
$this->getCustomerSession()->getCustomerGroupId(),
541-
json_encode($requiredAttributeIds)
542-
]
543-
)
544-
);
545-
$data = $this->serializer->unserialize($this->getCache()->load($key));
546-
if (!empty($data)) {
547-
$usedProducts = [];
548-
foreach ($data as $item) {
549-
$productItem = $this->productFactory->create();
550-
$productItem->setData($item);
551-
$usedProducts[] = $productItem;
552-
}
553-
} else {
554-
$collection = $this->getUsedProductCollection($product);
555-
$collection
556-
->setFlag('has_stock_status_filter', true)
557-
->addAttributeToSelect($this->getCatalogConfig()->getProductAttributes())
558-
->addFilterByRequiredOptions()
559-
->setStoreId($product->getStoreId());
560-
561-
$requiredAttributes = ['name', 'price', 'weight', 'image', 'thumbnail', 'status', 'media_gallery'];
562-
foreach ($requiredAttributes as $attributeCode) {
563-
$collection->addAttributeToSelect($attributeCode);
564-
}
565-
foreach ($this->getUsedProductAttributes($product) as $usedProductAttribute) {
566-
$collection->addAttributeToSelect($usedProductAttribute->getAttributeCode());
567-
}
568-
$collection->addMediaGalleryData();
569-
$collection->addTierPriceData();
570-
$usedProducts = $collection->getItems();
571-
572-
$this->getCache()->save(
573-
$this->serializer->serialize(array_map(
574-
function ($item) {
575-
return $item->getData();
576-
},
577-
$usedProducts
578-
)),
579-
$key,
580-
array_merge(
581-
$product->getIdentities(),
582-
[
583-
\Magento\Catalog\Model\Category::CACHE_TAG,
584-
\Magento\Catalog\Model\Product::CACHE_TAG,
585-
'price',
586-
self::TYPE_CODE . '_' . $productId
587-
]
588-
)
589-
);
590-
}
591-
$product->setData($this->_usedProducts, $usedProducts);
592-
}
593-
\Magento\Framework\Profiler::stop('CONFIGURABLE:' . __METHOD__);
594-
$usedProducts = $product->getData($this->_usedProducts);
595-
return $usedProducts;
596-
}
597-
598533
/**
599534
* Retrieve GalleryReadHandler
600535
*
@@ -1273,4 +1208,186 @@ public function isPossibleBuyFromList($product)
12731208

12741209
return $isAllCustomOptionsDisplayed;
12751210
}
1211+
1212+
/**
1213+
* Returns array of sub-products for specified configurable product
1214+
*
1215+
* Result array contains all children for specified configurable product
1216+
*
1217+
* @param \Magento\Catalog\Model\Product $product
1218+
* @param array $requiredAttributeIds
1219+
* @return ProductInterface[]
1220+
*/
1221+
public function getUsedProducts($product, $requiredAttributeIds = null)
1222+
{
1223+
$metadata = $this->getMetadataPool()->getMetadata(ProductInterface::class);
1224+
$keyParts = [
1225+
__METHOD__,
1226+
$product->getData($metadata->getLinkField()),
1227+
$product->getStoreId(),
1228+
$this->getCustomerSession()->getCustomerGroupId(),
1229+
$requiredAttributeIds
1230+
];
1231+
$cacheKey = $this->getUsedProductsCacheKey($keyParts);
1232+
return $this->loadUsedProducts($product, $cacheKey);
1233+
}
1234+
1235+
/**
1236+
* Returns array of sub-products for specified configurable product filtered by salable status
1237+
*
1238+
* Result array contains only those children for specified configurable product which are salable on store front
1239+
*
1240+
* @deprecated Not used anymore. Keep it for backward compatibility.
1241+
*
1242+
* @param \Magento\Catalog\Model\Product $product
1243+
* @param array|null $requiredAttributeIds
1244+
* @return ProductInterface[]
1245+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
1246+
*/
1247+
public function getSalableUsedProducts(\Magento\Catalog\Model\Product $product, $requiredAttributeIds = null)
1248+
{
1249+
$metadata = $this->getMetadataPool()->getMetadata(ProductInterface::class);
1250+
$keyParts = [
1251+
__METHOD__,
1252+
$product->getData($metadata->getLinkField()),
1253+
$product->getStoreId(),
1254+
$this->getCustomerSession()->getCustomerGroupId()
1255+
];
1256+
$cacheKey = $this->getUsedProductsCacheKey($keyParts);
1257+
1258+
return $this->loadUsedProducts($product, $cacheKey, true);
1259+
}
1260+
1261+
/**
1262+
* Load collection on sub-products for specified configurable product
1263+
*
1264+
* Load collection of sub-products, apply result to specified configurable product and store result to cache
1265+
* Number of loaded sub-products depends on $salableOnly parameter
1266+
* $salableOnly = true - result array contains only salable sub-products
1267+
* $salableOnly = false - result array contains all sub-products
1268+
* $cacheKey - allow store result data in different cache records
1269+
*
1270+
* @param \Magento\Catalog\Model\Product $product
1271+
* @param string $cacheKey
1272+
* @param bool $salableOnly
1273+
* @return ProductInterface[]
1274+
*/
1275+
private function loadUsedProducts(\Magento\Catalog\Model\Product $product, $cacheKey, $salableOnly = false)
1276+
{
1277+
$dataFieldName = $salableOnly ? $this->usedSalableProducts : $this->_usedProducts;
1278+
if (!$product->hasData($dataFieldName)) {
1279+
$usedProducts = $this->readUsedProductsCacheData($cacheKey);
1280+
if ($usedProducts === null) {
1281+
$collection = $this->getConfiguredUsedProductCollection($product);
1282+
if ($salableOnly) {
1283+
$collection = $this->salableProcessor->process($collection);
1284+
}
1285+
$usedProducts = $collection->getItems();
1286+
$this->saveUsedProductsCacheData($product, $usedProducts, $cacheKey);
1287+
}
1288+
$product->setData($dataFieldName, $usedProducts);
1289+
}
1290+
1291+
return $product->getData($dataFieldName);
1292+
}
1293+
1294+
/**
1295+
* Read used products data from cache
1296+
*
1297+
* Looking for cache record stored under provided $cacheKey
1298+
* In case data exists turns it into array of products
1299+
*
1300+
* @param string $cacheKey
1301+
* @return ProductInterface[]|null
1302+
*/
1303+
private function readUsedProductsCacheData($cacheKey)
1304+
{
1305+
$usedProducts = null;
1306+
$data = $this->serializer->unserialize($this->getCache()->load($cacheKey));
1307+
if (!empty($data)) {
1308+
$usedProducts = [];
1309+
foreach ($data as $item) {
1310+
$productItem = $this->productFactory->create();
1311+
$productItem->setData($item);
1312+
$usedProducts[] = $productItem;
1313+
}
1314+
}
1315+
1316+
return $usedProducts;
1317+
}
1318+
1319+
/**
1320+
* Save $subProducts to cache record identified with provided $cacheKey
1321+
*
1322+
* Cached data will be tagged with combined list of product tags and data specific tags i.e. 'price' etc.
1323+
*
1324+
* @param \Magento\Catalog\Model\Product $product
1325+
* @param ProductInterface[] $subProducts
1326+
* @param string $cacheKey
1327+
* @return bool
1328+
*/
1329+
private function saveUsedProductsCacheData(\Magento\Catalog\Model\Product $product, array $subProducts, $cacheKey)
1330+
{
1331+
$metadata = $this->getMetadataPool()->getMetadata(ProductInterface::class);
1332+
return $this->getCache()->save(
1333+
$this->serializer->serialize(array_map(
1334+
function ($item) {
1335+
return $item->getData();
1336+
},
1337+
$subProducts
1338+
)),
1339+
$cacheKey,
1340+
array_merge(
1341+
$product->getIdentities(),
1342+
[
1343+
\Magento\Catalog\Model\Category::CACHE_TAG,
1344+
\Magento\Catalog\Model\Product::CACHE_TAG,
1345+
'price',
1346+
self::TYPE_CODE . '_' . $product->getData($metadata->getLinkField())
1347+
]
1348+
)
1349+
);
1350+
}
1351+
1352+
/**
1353+
* Create string key based on $keyParts
1354+
*
1355+
* $keyParts - one dimensional array of strings
1356+
*
1357+
* @param array $keyParts
1358+
* @return string
1359+
*/
1360+
private function getUsedProductsCacheKey($keyParts)
1361+
{
1362+
return md5(implode('_', $keyParts));
1363+
}
1364+
1365+
/**
1366+
* Prepare collection for retrieving sub-products of specified configurable product
1367+
*
1368+
* Retrieve related products collection with additional configuration
1369+
*
1370+
* @param \Magento\Catalog\Model\Product $product
1371+
* @return \Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\Collection
1372+
*/
1373+
private function getConfiguredUsedProductCollection(\Magento\Catalog\Model\Product $product)
1374+
{
1375+
$collection = $this->getUsedProductCollection($product);
1376+
$collection
1377+
->setFlag('has_stock_status_filter', true)
1378+
->addAttributeToSelect($this->getCatalogConfig()->getProductAttributes())
1379+
->addFilterByRequiredOptions()
1380+
->setStoreId($product->getStoreId());
1381+
1382+
$requiredAttributes = ['name', 'price', 'weight', 'image', 'thumbnail', 'status', 'media_gallery'];
1383+
foreach ($requiredAttributes as $attributeCode) {
1384+
$collection->addAttributeToSelect($attributeCode);
1385+
}
1386+
foreach ($this->getUsedProductAttributes($product) as $usedProductAttribute) {
1387+
$collection->addAttributeToSelect($usedProductAttribute->getAttributeCode());
1388+
}
1389+
$collection->addMediaGalleryData();
1390+
$collection->addTierPriceData();
1391+
return $collection;
1392+
}
12761393
}

app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/ConfigurableTest.php

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -871,18 +871,20 @@ public function testSetImageFromChildProduct()
871871
$childProductMock = $this->getMockBuilder(\Magento\Catalog\Model\Product::class)
872872
->disableOriginalConstructor()
873873
->getMock();
874+
$this->entityMetadata->expects($this->any())
875+
->method('getLinkField')
876+
->willReturn('link');
877+
$productMock->expects($this->any())->method('hasData')
878+
->withConsecutive(['store_id'], ['_cache_instance_products'])
879+
->willReturnOnConsecutiveCalls(true, true);
880+
881+
$productMock->expects($this->any())->method('getData')
882+
->withConsecutive(['image'], ['image'], ['link'], ['store_id'], ['_cache_instance_products'])
883+
->willReturnOnConsecutiveCalls('no_selection', 'no_selection', 1, 1, [$childProductMock]);
874884

875-
$productMock->expects($this->at(0))->method('getData')->with('image')->willReturn('no_selection');
876-
$productMock->expects($this->at(1))->method('getData')->with('image')->willReturn('no_selection');
877-
$productMock->expects($this->once())->method('hasData')->with('_cache_instance_products')->willReturn(true);
878-
$productMock->expects($this->at(3))
879-
->method('getData')
880-
->with('_cache_instance_products')
881-
->willReturn([$childProductMock]);
882885
$childProductMock->expects($this->any())->method('getData')->with('image')->willReturn('image_data');
883886
$productMock->expects($this->once())->method('setImage')->with('image_data')->willReturnSelf();
884887

885888
$this->_model->setImageFromChildProduct($productMock);
886889
}
887890
}
888-

0 commit comments

Comments
 (0)