diff --git a/app/code/Magento/ConfigurableProduct/Model/Plugin/Frontend/UsedProductsCache.php b/app/code/Magento/ConfigurableProduct/Model/Plugin/Frontend/UsedProductsCache.php
new file mode 100644
index 0000000000000..19a1b8d3ca17f
--- /dev/null
+++ b/app/code/Magento/ConfigurableProduct/Model/Plugin/Frontend/UsedProductsCache.php
@@ -0,0 +1,190 @@
+metadataPool = $metadataPool;
+ $this->cache = $cache;
+ $this->serializer = $serializer;
+ $this->productFactory = $productFactory;
+ $this->customerSession = $customerSession;
+ }
+
+ /**
+ * Retrieve used products for configurable product
+ *
+ * @param Configurable $subject
+ * @param callable $proceed
+ * @param Product $product
+ * @param array|null $requiredAttributeIds
+ * @return ProductInterface[]
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function aroundGetUsedProducts(
+ Configurable $subject,
+ callable $proceed,
+ $product,
+ $requiredAttributeIds = null
+ ) {
+ $cacheKey = $this->getCacheKey($product, $requiredAttributeIds);
+ $usedProducts = $this->readUsedProductsCacheData($cacheKey);
+ if ($usedProducts === null) {
+ $usedProducts = $proceed($product, $requiredAttributeIds);
+ $this->saveUsedProductsCacheData($product, $usedProducts, $cacheKey);
+ }
+
+ return $usedProducts;
+ }
+
+ /**
+ * Generate cache key for product
+ *
+ * @param Product $product
+ * @param array|null $requiredAttributeIds
+ * @return string
+ */
+ private function getCacheKey($product, $requiredAttributeIds = null): string
+ {
+ $metadata = $this->metadataPool->getMetadata(ProductInterface::class);
+ $keyParts = [
+ 'getUsedProducts',
+ $product->getData($metadata->getLinkField()),
+ $product->getStoreId(),
+ $this->customerSession->getCustomerGroupId(),
+ ];
+ if ($requiredAttributeIds !== null) {
+ sort($requiredAttributeIds);
+ $keyParts[] = implode('', $requiredAttributeIds);
+ }
+ $cacheKey = sha1(implode('_', $keyParts));
+
+ return $cacheKey;
+ }
+
+ /**
+ * Read used products data from cache
+ *
+ * Looking for cache record stored under provided $cacheKey
+ * In case data exists turns it into array of products
+ *
+ * @param string $cacheKey
+ * @return ProductInterface[]|null
+ */
+ private function readUsedProductsCacheData(string $cacheKey): ?array
+ {
+ $data = $this->cache->load($cacheKey);
+ if (!$data) {
+ return null;
+ }
+
+ $items = $this->serializer->unserialize($data);
+ if (!$items) {
+ return null;
+ }
+
+ $usedProducts = [];
+ foreach ($items as $item) {
+ /** @var Product $productItem */
+ $productItem = $this->productFactory->create();
+ $productItem->setData($item);
+ $usedProducts[] = $productItem;
+ }
+
+ return $usedProducts;
+ }
+
+ /**
+ * Save $subProducts to cache record identified with provided $cacheKey
+ *
+ * Cached data will be tagged with combined list of product tags and data specific tags i.e. 'price' etc.
+ *
+ * @param Product $product
+ * @param ProductInterface[] $subProducts
+ * @param string $cacheKey
+ * @return bool
+ */
+ private function saveUsedProductsCacheData(Product $product, array $subProducts, string $cacheKey): bool
+ {
+ $metadata = $this->metadataPool->getMetadata(ProductInterface::class);
+ $data = $this->serializer->serialize(
+ array_map(
+ function ($item) {
+ return $item->getData();
+ },
+ $subProducts
+ )
+ );
+ $tags = array_merge(
+ $product->getIdentities(),
+ [
+ Category::CACHE_TAG,
+ Product::CACHE_TAG,
+ 'price',
+ Configurable::TYPE_CODE . '_' . $product->getData($metadata->getLinkField()),
+ ]
+ );
+ $result = $this->cache->save($data, $cacheKey, $tags);
+
+ return (bool) $result;
+ }
+}
diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php
index a849d964eaed5..c60953e33e9eb 100644
--- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php
+++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php
@@ -1233,28 +1233,22 @@ public function isPossibleBuyFromList($product)
* Returns array of sub-products for specified configurable product
*
* $requiredAttributeIds - one dimensional array, if provided
- *
* Result array contains all children for specified configurable product
*
- * @param \Magento\Catalog\Model\Product $product
- * @param array $requiredAttributeIds
+ * @param \Magento\Catalog\Model\Product $product
+ * @param array $requiredAttributeIds
* @return ProductInterface[]
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function getUsedProducts($product, $requiredAttributeIds = null)
{
- $metadata = $this->getMetadataPool()->getMetadata(ProductInterface::class);
- $keyParts = [
- __METHOD__,
- $product->getData($metadata->getLinkField()),
- $product->getStoreId(),
- $this->getCustomerSession()->getCustomerGroupId()
- ];
- if ($requiredAttributeIds !== null) {
- sort($requiredAttributeIds);
- $keyParts[] = implode('', $requiredAttributeIds);
+ if (!$product->hasData($this->_usedProducts)) {
+ $collection = $this->getConfiguredUsedProductCollection($product, false);
+ $usedProducts = array_values($collection->getItems());
+ $product->setData($this->_usedProducts, $usedProducts);
}
- $cacheKey = $this->getUsedProductsCacheKey($keyParts);
- return $this->loadUsedProducts($product, $cacheKey);
+
+ return $product->getData($this->_usedProducts);
}
/**
diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/ConfigurableTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/ConfigurableTest.php
index c351d12fa813d..165e479d99348 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/ConfigurableTest.php
+++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/ConfigurableTest.php
@@ -266,10 +266,12 @@ public function testSave()
->with('_cache_instance_used_product_attribute_ids')
->willReturn(true);
$extensionAttributes = $this->getMockBuilder(ProductExtensionInterface::class)
- ->setMethods([
- 'getConfigurableProductOptions',
- 'getConfigurableProductLinks'
- ])
+ ->setMethods(
+ [
+ 'getConfigurableProductOptions',
+ 'getConfigurableProductLinks'
+ ]
+ )
->getMockForAbstractClass();
$this->entityMetadata->expects($this->any())
->method('getLinkField')
@@ -344,25 +346,13 @@ public function testCanUseAttribute()
public function testGetUsedProducts()
{
- $productCollectionItemData = ['array'];
+ $productCollectionItem = $this->createMock(\Magento\Catalog\Model\Product::class);
+ $attributeCollection = $this->createMock(Collection::class);
+ $product = $this->createMock(\Magento\Catalog\Model\Product::class);
+ $productCollection = $this->createMock(ProductCollection::class);
- $productCollectionItem = $this->getMockBuilder(\Magento\Catalog\Model\Product::class)
- ->disableOriginalConstructor()
- ->getMock();
- $attributeCollection = $this->getMockBuilder(Collection::class)
- ->disableOriginalConstructor()
- ->getMock();
- $product = $this->getMockBuilder(\Magento\Catalog\Model\Product::class)
- ->disableOriginalConstructor()
- ->getMock();
- $productCollection = $this->getMockBuilder(ProductCollection::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- $productCollectionItem->expects($this->once())->method('getData')->willReturn($productCollectionItemData);
$attributeCollection->expects($this->any())->method('setProductFilter')->willReturnSelf();
$product->expects($this->atLeastOnce())->method('getStoreId')->willReturn(5);
- $product->expects($this->once())->method('getIdentities')->willReturn(['123']);
$product->expects($this->exactly(2))
->method('hasData')
@@ -388,59 +378,10 @@ public function testGetUsedProducts()
$productCollection->expects($this->once())->method('setStoreId')->with(5)->willReturn([]);
$productCollection->expects($this->once())->method('getItems')->willReturn([$productCollectionItem]);
- $this->serializer->expects($this->once())
- ->method('serialize')
- ->with([$productCollectionItemData])
- ->willReturn('result');
-
$this->productCollectionFactory->expects($this->any())->method('create')->willReturn($productCollection);
$this->model->getUsedProducts($product);
}
- public function testGetUsedProductsWithDataInCache()
- {
- $product = $this->getMockBuilder(\Magento\Catalog\Model\Product::class)
- ->disableOriginalConstructor()
- ->getMock();
- $childProduct = $this->getMockBuilder(\Magento\Catalog\Model\Product::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- $dataKey = '_cache_instance_products';
- $usedProductsData = [['first']];
- $usedProducts = [$childProduct];
-
- $product->expects($this->once())
- ->method('hasData')
- ->with($dataKey)
- ->willReturn(false);
- $product->expects($this->once())
- ->method('setData')
- ->with($dataKey, $usedProducts);
- $product->expects($this->any())
- ->method('getData')
- ->willReturnOnConsecutiveCalls(1, $usedProducts);
-
- $childProduct->expects($this->once())
- ->method('setData')
- ->with($usedProductsData[0]);
-
- $this->productFactory->expects($this->once())
- ->method('create')
- ->willReturn($childProduct);
-
- $this->cache->expects($this->once())
- ->method('load')
- ->willReturn($usedProductsData);
-
- $this->serializer->expects($this->once())
- ->method('unserialize')
- ->with($usedProductsData)
- ->willReturn($usedProductsData);
-
- $this->assertEquals($usedProducts, $this->model->getUsedProducts($product));
- }
-
/**
* @param int $productStore
*
@@ -878,12 +819,12 @@ public function testSetImageFromChildProduct()
->method('getLinkField')
->willReturn('link');
$productMock->expects($this->any())->method('hasData')
- ->withConsecutive(['store_id'], ['_cache_instance_products'])
- ->willReturnOnConsecutiveCalls(true, true);
+ ->withConsecutive(['_cache_instance_products'])
+ ->willReturnOnConsecutiveCalls(true);
$productMock->expects($this->any())->method('getData')
- ->withConsecutive(['image'], ['image'], ['link'], ['store_id'], ['_cache_instance_products'])
- ->willReturnOnConsecutiveCalls('no_selection', 'no_selection', 1, 1, [$childProductMock]);
+ ->withConsecutive(['image'], ['image'], ['_cache_instance_products'])
+ ->willReturnOnConsecutiveCalls('no_selection', 'no_selection', [$childProductMock]);
$childProductMock->expects($this->any())->method('getData')->with('image')->willReturn('image_data');
$productMock->expects($this->once())->method('setImage')->with('image_data')->willReturnSelf();
diff --git a/app/code/Magento/ConfigurableProduct/etc/di.xml b/app/code/Magento/ConfigurableProduct/etc/di.xml
index b8f7ed67a9868..c8a278df92dc6 100644
--- a/app/code/Magento/ConfigurableProduct/etc/di.xml
+++ b/app/code/Magento/ConfigurableProduct/etc/di.xml
@@ -256,4 +256,12 @@
+
+
+ Magento\Framework\App\Cache\Type\Collection
+
+
+ Magento\Framework\Serialize\Serializer\Json
+
+
diff --git a/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml b/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml
index df96829b354c8..b2d50f54f5334 100644
--- a/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml
+++ b/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml
@@ -13,4 +13,7 @@
+
+
+