Skip to content

Commit 3a1f3ef

Browse files
committed
Merge remote-tracking branch 'tango/MC-19689' into Chaika-2019-09-12-3
2 parents 4808977 + d37b4b7 commit 3a1f3ef

File tree

5 files changed

+224
-88
lines changed

5 files changed

+224
-88
lines changed
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\ConfigurableProduct\Model\Plugin\Frontend;
9+
10+
use Magento\Catalog\Api\Data\ProductInterface;
11+
use Magento\Catalog\Api\Data\ProductInterfaceFactory;
12+
use Magento\Catalog\Model\Category;
13+
use Magento\Catalog\Model\Product;
14+
use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
15+
use Magento\Customer\Model\Session;
16+
use Magento\Framework\Cache\FrontendInterface;
17+
use Magento\Framework\EntityManager\MetadataPool;
18+
use Magento\Framework\Serialize\SerializerInterface;
19+
20+
/**
21+
* Cache of used products for configurable product
22+
*
23+
* @SuppressWarnings(PHPMD.CookieAndSessionMisuse)
24+
*/
25+
class UsedProductsCache
26+
{
27+
/**
28+
* @var MetadataPool
29+
*/
30+
private $metadataPool;
31+
32+
/**
33+
* @var FrontendInterface
34+
*/
35+
private $cache;
36+
37+
/**
38+
* @var SerializerInterface
39+
*/
40+
private $serializer;
41+
42+
/**
43+
* @var ProductInterfaceFactory
44+
*/
45+
private $productFactory;
46+
47+
/**
48+
* @var Session
49+
*/
50+
private $customerSession;
51+
52+
/**
53+
* @param MetadataPool $metadataPool
54+
* @param FrontendInterface $cache
55+
* @param SerializerInterface $serializer
56+
* @param ProductInterfaceFactory $productFactory
57+
* @param Session $customerSession
58+
*/
59+
public function __construct(
60+
MetadataPool $metadataPool,
61+
FrontendInterface $cache,
62+
SerializerInterface $serializer,
63+
ProductInterfaceFactory $productFactory,
64+
Session $customerSession
65+
) {
66+
$this->metadataPool = $metadataPool;
67+
$this->cache = $cache;
68+
$this->serializer = $serializer;
69+
$this->productFactory = $productFactory;
70+
$this->customerSession = $customerSession;
71+
}
72+
73+
/**
74+
* Retrieve used products for configurable product
75+
*
76+
* @param Configurable $subject
77+
* @param callable $proceed
78+
* @param Product $product
79+
* @param array|null $requiredAttributeIds
80+
* @return ProductInterface[]
81+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
82+
*/
83+
public function aroundGetUsedProducts(
84+
Configurable $subject,
85+
callable $proceed,
86+
$product,
87+
$requiredAttributeIds = null
88+
) {
89+
$cacheKey = $this->getCacheKey($product, $requiredAttributeIds);
90+
$usedProducts = $this->readUsedProductsCacheData($cacheKey);
91+
if ($usedProducts === null) {
92+
$usedProducts = $proceed($product, $requiredAttributeIds);
93+
$this->saveUsedProductsCacheData($product, $usedProducts, $cacheKey);
94+
}
95+
96+
return $usedProducts;
97+
}
98+
99+
/**
100+
* Generate cache key for product
101+
*
102+
* @param Product $product
103+
* @param array|null $requiredAttributeIds
104+
* @return string
105+
*/
106+
private function getCacheKey($product, $requiredAttributeIds = null): string
107+
{
108+
$metadata = $this->metadataPool->getMetadata(ProductInterface::class);
109+
$keyParts = [
110+
'getUsedProducts',
111+
$product->getData($metadata->getLinkField()),
112+
$product->getStoreId(),
113+
$this->customerSession->getCustomerGroupId(),
114+
];
115+
if ($requiredAttributeIds !== null) {
116+
sort($requiredAttributeIds);
117+
$keyParts[] = implode('', $requiredAttributeIds);
118+
}
119+
$cacheKey = sha1(implode('_', $keyParts));
120+
121+
return $cacheKey;
122+
}
123+
124+
/**
125+
* Read used products data from cache
126+
*
127+
* Looking for cache record stored under provided $cacheKey
128+
* In case data exists turns it into array of products
129+
*
130+
* @param string $cacheKey
131+
* @return ProductInterface[]|null
132+
*/
133+
private function readUsedProductsCacheData(string $cacheKey): ?array
134+
{
135+
$data = $this->cache->load($cacheKey);
136+
if (!$data) {
137+
return null;
138+
}
139+
140+
$items = $this->serializer->unserialize($data);
141+
if (!$items) {
142+
return null;
143+
}
144+
145+
$usedProducts = [];
146+
foreach ($items as $item) {
147+
/** @var Product $productItem */
148+
$productItem = $this->productFactory->create();
149+
$productItem->setData($item);
150+
$usedProducts[] = $productItem;
151+
}
152+
153+
return $usedProducts;
154+
}
155+
156+
/**
157+
* Save $subProducts to cache record identified with provided $cacheKey
158+
*
159+
* Cached data will be tagged with combined list of product tags and data specific tags i.e. 'price' etc.
160+
*
161+
* @param Product $product
162+
* @param ProductInterface[] $subProducts
163+
* @param string $cacheKey
164+
* @return bool
165+
*/
166+
private function saveUsedProductsCacheData(Product $product, array $subProducts, string $cacheKey): bool
167+
{
168+
$metadata = $this->metadataPool->getMetadata(ProductInterface::class);
169+
$data = $this->serializer->serialize(
170+
array_map(
171+
function ($item) {
172+
return $item->getData();
173+
},
174+
$subProducts
175+
)
176+
);
177+
$tags = array_merge(
178+
$product->getIdentities(),
179+
[
180+
Category::CACHE_TAG,
181+
Product::CACHE_TAG,
182+
'price',
183+
Configurable::TYPE_CODE . '_' . $product->getData($metadata->getLinkField()),
184+
]
185+
);
186+
$result = $this->cache->save($data, $cacheKey, $tags);
187+
188+
return (bool) $result;
189+
}
190+
}

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

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1233,28 +1233,22 @@ public function isPossibleBuyFromList($product)
12331233
* Returns array of sub-products for specified configurable product
12341234
*
12351235
* $requiredAttributeIds - one dimensional array, if provided
1236-
*
12371236
* Result array contains all children for specified configurable product
12381237
*
1239-
* @param \Magento\Catalog\Model\Product $product
1240-
* @param array $requiredAttributeIds
1238+
* @param \Magento\Catalog\Model\Product $product
1239+
* @param array $requiredAttributeIds
12411240
* @return ProductInterface[]
1241+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
12421242
*/
12431243
public function getUsedProducts($product, $requiredAttributeIds = null)
12441244
{
1245-
$metadata = $this->getMetadataPool()->getMetadata(ProductInterface::class);
1246-
$keyParts = [
1247-
__METHOD__,
1248-
$product->getData($metadata->getLinkField()),
1249-
$product->getStoreId(),
1250-
$this->getCustomerSession()->getCustomerGroupId()
1251-
];
1252-
if ($requiredAttributeIds !== null) {
1253-
sort($requiredAttributeIds);
1254-
$keyParts[] = implode('', $requiredAttributeIds);
1245+
if (!$product->hasData($this->_usedProducts)) {
1246+
$collection = $this->getConfiguredUsedProductCollection($product, false);
1247+
$usedProducts = array_values($collection->getItems());
1248+
$product->setData($this->_usedProducts, $usedProducts);
12551249
}
1256-
$cacheKey = $this->getUsedProductsCacheKey($keyParts);
1257-
return $this->loadUsedProducts($product, $cacheKey);
1250+
1251+
return $product->getData($this->_usedProducts);
12581252
}
12591253

12601254
/**

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

Lines changed: 14 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -266,10 +266,12 @@ public function testSave()
266266
->with('_cache_instance_used_product_attribute_ids')
267267
->willReturn(true);
268268
$extensionAttributes = $this->getMockBuilder(ProductExtensionInterface::class)
269-
->setMethods([
270-
'getConfigurableProductOptions',
271-
'getConfigurableProductLinks'
272-
])
269+
->setMethods(
270+
[
271+
'getConfigurableProductOptions',
272+
'getConfigurableProductLinks'
273+
]
274+
)
273275
->getMockForAbstractClass();
274276
$this->entityMetadata->expects($this->any())
275277
->method('getLinkField')
@@ -344,25 +346,13 @@ public function testCanUseAttribute()
344346

345347
public function testGetUsedProducts()
346348
{
347-
$productCollectionItemData = ['array'];
349+
$productCollectionItem = $this->createMock(\Magento\Catalog\Model\Product::class);
350+
$attributeCollection = $this->createMock(Collection::class);
351+
$product = $this->createMock(\Magento\Catalog\Model\Product::class);
352+
$productCollection = $this->createMock(ProductCollection::class);
348353

349-
$productCollectionItem = $this->getMockBuilder(\Magento\Catalog\Model\Product::class)
350-
->disableOriginalConstructor()
351-
->getMock();
352-
$attributeCollection = $this->getMockBuilder(Collection::class)
353-
->disableOriginalConstructor()
354-
->getMock();
355-
$product = $this->getMockBuilder(\Magento\Catalog\Model\Product::class)
356-
->disableOriginalConstructor()
357-
->getMock();
358-
$productCollection = $this->getMockBuilder(ProductCollection::class)
359-
->disableOriginalConstructor()
360-
->getMock();
361-
362-
$productCollectionItem->expects($this->once())->method('getData')->willReturn($productCollectionItemData);
363354
$attributeCollection->expects($this->any())->method('setProductFilter')->willReturnSelf();
364355
$product->expects($this->atLeastOnce())->method('getStoreId')->willReturn(5);
365-
$product->expects($this->once())->method('getIdentities')->willReturn(['123']);
366356

367357
$product->expects($this->exactly(2))
368358
->method('hasData')
@@ -388,59 +378,10 @@ public function testGetUsedProducts()
388378
$productCollection->expects($this->once())->method('setStoreId')->with(5)->willReturn([]);
389379
$productCollection->expects($this->once())->method('getItems')->willReturn([$productCollectionItem]);
390380

391-
$this->serializer->expects($this->once())
392-
->method('serialize')
393-
->with([$productCollectionItemData])
394-
->willReturn('result');
395-
396381
$this->productCollectionFactory->expects($this->any())->method('create')->willReturn($productCollection);
397382
$this->model->getUsedProducts($product);
398383
}
399384

400-
public function testGetUsedProductsWithDataInCache()
401-
{
402-
$product = $this->getMockBuilder(\Magento\Catalog\Model\Product::class)
403-
->disableOriginalConstructor()
404-
->getMock();
405-
$childProduct = $this->getMockBuilder(\Magento\Catalog\Model\Product::class)
406-
->disableOriginalConstructor()
407-
->getMock();
408-
409-
$dataKey = '_cache_instance_products';
410-
$usedProductsData = [['first']];
411-
$usedProducts = [$childProduct];
412-
413-
$product->expects($this->once())
414-
->method('hasData')
415-
->with($dataKey)
416-
->willReturn(false);
417-
$product->expects($this->once())
418-
->method('setData')
419-
->with($dataKey, $usedProducts);
420-
$product->expects($this->any())
421-
->method('getData')
422-
->willReturnOnConsecutiveCalls(1, $usedProducts);
423-
424-
$childProduct->expects($this->once())
425-
->method('setData')
426-
->with($usedProductsData[0]);
427-
428-
$this->productFactory->expects($this->once())
429-
->method('create')
430-
->willReturn($childProduct);
431-
432-
$this->cache->expects($this->once())
433-
->method('load')
434-
->willReturn($usedProductsData);
435-
436-
$this->serializer->expects($this->once())
437-
->method('unserialize')
438-
->with($usedProductsData)
439-
->willReturn($usedProductsData);
440-
441-
$this->assertEquals($usedProducts, $this->model->getUsedProducts($product));
442-
}
443-
444385
/**
445386
* @param int $productStore
446387
*
@@ -878,12 +819,12 @@ public function testSetImageFromChildProduct()
878819
->method('getLinkField')
879820
->willReturn('link');
880821
$productMock->expects($this->any())->method('hasData')
881-
->withConsecutive(['store_id'], ['_cache_instance_products'])
882-
->willReturnOnConsecutiveCalls(true, true);
822+
->withConsecutive(['_cache_instance_products'])
823+
->willReturnOnConsecutiveCalls(true);
883824

884825
$productMock->expects($this->any())->method('getData')
885-
->withConsecutive(['image'], ['image'], ['link'], ['store_id'], ['_cache_instance_products'])
886-
->willReturnOnConsecutiveCalls('no_selection', 'no_selection', 1, 1, [$childProductMock]);
826+
->withConsecutive(['image'], ['image'], ['_cache_instance_products'])
827+
->willReturnOnConsecutiveCalls('no_selection', 'no_selection', [$childProductMock]);
887828

888829
$childProductMock->expects($this->any())->method('getData')->with('image')->willReturn('image_data');
889830
$productMock->expects($this->once())->method('setImage')->with('image_data')->willReturnSelf();

app/code/Magento/ConfigurableProduct/etc/di.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,4 +256,12 @@
256256
</argument>
257257
</arguments>
258258
</type>
259+
<type name="Magento\ConfigurableProduct\Model\Plugin\Frontend\UsedProductsCache">
260+
<arguments>
261+
<argument name="cache" xsi:type="object">Magento\Framework\App\Cache\Type\Collection</argument>
262+
</arguments>
263+
<arguments>
264+
<argument name="serializer" xsi:type="object">Magento\Framework\Serialize\Serializer\Json</argument>
265+
</arguments>
266+
</type>
259267
</config>

app/code/Magento/ConfigurableProduct/etc/frontend/di.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,7 @@
1313
<type name="Magento\Catalog\Model\Product">
1414
<plugin name="product_identities_extender" type="Magento\ConfigurableProduct\Model\Plugin\Frontend\ProductIdentitiesExtender" />
1515
</type>
16+
<type name="Magento\ConfigurableProduct\Model\Product\Type\Configurable">
17+
<plugin name="used_products_cache" type="Magento\ConfigurableProduct\Model\Plugin\Frontend\UsedProductsCache" />
18+
</type>
1619
</config>

0 commit comments

Comments
 (0)