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 @@ + + +