Skip to content

Commit

Permalink
Merge pull request #4766 from magento-chaika/Chaika-2019-09-12-3
Browse files Browse the repository at this point in the history
Chaika-2019-09-12-3
  • Loading branch information
dhorytskyi authored Sep 17, 2019
2 parents 27985bc + bac0072 commit 9b2cefd
Show file tree
Hide file tree
Showing 5 changed files with 224 additions and 88 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);

namespace Magento\ConfigurableProduct\Model\Plugin\Frontend;

use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Api\Data\ProductInterfaceFactory;
use Magento\Catalog\Model\Category;
use Magento\Catalog\Model\Product;
use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
use Magento\Customer\Model\Session;
use Magento\Framework\Cache\FrontendInterface;
use Magento\Framework\EntityManager\MetadataPool;
use Magento\Framework\Serialize\SerializerInterface;

/**
* Cache of used products for configurable product
*
* @SuppressWarnings(PHPMD.CookieAndSessionMisuse)
*/
class UsedProductsCache
{
/**
* @var MetadataPool
*/
private $metadataPool;

/**
* @var FrontendInterface
*/
private $cache;

/**
* @var SerializerInterface
*/
private $serializer;

/**
* @var ProductInterfaceFactory
*/
private $productFactory;

/**
* @var Session
*/
private $customerSession;

/**
* @param MetadataPool $metadataPool
* @param FrontendInterface $cache
* @param SerializerInterface $serializer
* @param ProductInterfaceFactory $productFactory
* @param Session $customerSession
*/
public function __construct(
MetadataPool $metadataPool,
FrontendInterface $cache,
SerializerInterface $serializer,
ProductInterfaceFactory $productFactory,
Session $customerSession
) {
$this->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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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')
Expand All @@ -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
*
Expand Down Expand Up @@ -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();
Expand Down
8 changes: 8 additions & 0 deletions app/code/Magento/ConfigurableProduct/etc/di.xml
Original file line number Diff line number Diff line change
Expand Up @@ -256,4 +256,12 @@
</argument>
</arguments>
</type>
<type name="Magento\ConfigurableProduct\Model\Plugin\Frontend\UsedProductsCache">
<arguments>
<argument name="cache" xsi:type="object">Magento\Framework\App\Cache\Type\Collection</argument>
</arguments>
<arguments>
<argument name="serializer" xsi:type="object">Magento\Framework\Serialize\Serializer\Json</argument>
</arguments>
</type>
</config>
3 changes: 3 additions & 0 deletions app/code/Magento/ConfigurableProduct/etc/frontend/di.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,7 @@
<type name="Magento\Catalog\Model\Product">
<plugin name="product_identities_extender" type="Magento\ConfigurableProduct\Model\Plugin\Frontend\ProductIdentitiesExtender" />
</type>
<type name="Magento\ConfigurableProduct\Model\Product\Type\Configurable">
<plugin name="used_products_cache" type="Magento\ConfigurableProduct\Model\Plugin\Frontend\UsedProductsCache" />
</type>
</config>

0 comments on commit 9b2cefd

Please sign in to comment.