From 239ef1494e2c25d7cdd84e82072cdf71a9f6a5b5 Mon Sep 17 00:00:00 2001 From: Dmytro Horytskyi Date: Thu, 29 Aug 2019 18:22:31 -0500 Subject: [PATCH 1/4] MC-19689: Simple product disappearing in the configurable grid after qty set to 0 --- .../Model/Product/Type/Configurable.php | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php index a849d964eaed5..77450748f7eba 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php @@ -1304,15 +1304,11 @@ private function loadUsedProducts(\Magento\Catalog\Model\Product $product, $cach { $dataFieldName = $salableOnly ? $this->usedSalableProducts : $this->_usedProducts; if (!$product->hasData($dataFieldName)) { - $usedProducts = $this->readUsedProductsCacheData($cacheKey); - if ($usedProducts === null) { - $collection = $this->getConfiguredUsedProductCollection($product, false); - if ($salableOnly) { - $collection = $this->salableProcessor->process($collection); - } - $usedProducts = array_values($collection->getItems()); - $this->saveUsedProductsCacheData($product, $usedProducts, $cacheKey); + $collection = $this->getConfiguredUsedProductCollection($product, false); + if ($salableOnly) { + $collection = $this->salableProcessor->process($collection); } + $usedProducts = array_values($collection->getItems()); $product->setData($dataFieldName, $usedProducts); } From 2487db1b537c9d14f534d8628acf60581f5cbb1b Mon Sep 17 00:00:00 2001 From: Dmytro Horytskyi Date: Fri, 30 Aug 2019 12:15:50 -0500 Subject: [PATCH 2/4] MC-19689: Simple product disappearing in the configurable grid after qty set to 0 --- .../Plugin/Frontend/UsedProductsCache.php | 185 ++++++++++++++++++ .../Model/Product/Type/Configurable.php | 36 ++-- .../Magento/ConfigurableProduct/etc/di.xml | 8 + .../ConfigurableProduct/etc/frontend/di.xml | 3 + 4 files changed, 213 insertions(+), 19 deletions(-) create mode 100644 app/code/Magento/ConfigurableProduct/Model/Plugin/Frontend/UsedProductsCache.php 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..69c0a5f6000f0 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Model/Plugin/Frontend/UsedProductsCache.php @@ -0,0 +1,185 @@ +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[] + */ + 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 77450748f7eba..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); } /** @@ -1304,11 +1298,15 @@ private function loadUsedProducts(\Magento\Catalog\Model\Product $product, $cach { $dataFieldName = $salableOnly ? $this->usedSalableProducts : $this->_usedProducts; if (!$product->hasData($dataFieldName)) { - $collection = $this->getConfiguredUsedProductCollection($product, false); - if ($salableOnly) { - $collection = $this->salableProcessor->process($collection); + $usedProducts = $this->readUsedProductsCacheData($cacheKey); + if ($usedProducts === null) { + $collection = $this->getConfiguredUsedProductCollection($product, false); + if ($salableOnly) { + $collection = $this->salableProcessor->process($collection); + } + $usedProducts = array_values($collection->getItems()); + $this->saveUsedProductsCacheData($product, $usedProducts, $cacheKey); } - $usedProducts = array_values($collection->getItems()); $product->setData($dataFieldName, $usedProducts); } 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 @@ + + + From 3051ff6096a6b1eb58e67ac7ce00fce333730bbc Mon Sep 17 00:00:00 2001 From: Dmytro Horytskyi Date: Fri, 30 Aug 2019 15:49:20 -0500 Subject: [PATCH 3/4] MC-19689: Simple product disappearing in the configurable grid after qty set to 0 --- .../Plugin/Frontend/UsedProductsCache.php | 19 +++-- .../Model/Product/Type/ConfigurableTest.php | 77 ++----------------- 2 files changed, 20 insertions(+), 76 deletions(-) diff --git a/app/code/Magento/ConfigurableProduct/Model/Plugin/Frontend/UsedProductsCache.php b/app/code/Magento/ConfigurableProduct/Model/Plugin/Frontend/UsedProductsCache.php index 69c0a5f6000f0..19a1b8d3ca17f 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Plugin/Frontend/UsedProductsCache.php +++ b/app/code/Magento/ConfigurableProduct/Model/Plugin/Frontend/UsedProductsCache.php @@ -19,6 +19,8 @@ /** * Cache of used products for configurable product + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class UsedProductsCache { @@ -76,6 +78,7 @@ public function __construct( * @param Product $product * @param array|null $requiredAttributeIds * @return ProductInterface[] + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function aroundGetUsedProducts( Configurable $subject, @@ -163,19 +166,21 @@ private function readUsedProductsCacheData(string $cacheKey): ?array 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 - )); + $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()) + Configurable::TYPE_CODE . '_' . $product->getData($metadata->getLinkField()), ] ); $result = $this->cache->save($data, $cacheKey, $tags); 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..bf8357b5181d7 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 @@ -344,25 +344,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 +376,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 +817,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(); From 689a0b9719aa286e57858020077e6bb270ef42d9 Mon Sep 17 00:00:00 2001 From: Dmytro Horytskyi Date: Fri, 30 Aug 2019 16:21:47 -0500 Subject: [PATCH 4/4] MC-19689: Simple product disappearing in the configurable grid after qty set to 0 --- .../Test/Unit/Model/Product/Type/ConfigurableTest.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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 bf8357b5181d7..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')