Skip to content

Commit fdab6cf

Browse files
committed
ACP2E-3410: Configurable product edit form load causes timeout and memory exhaustion
1 parent 3304583 commit fdab6cf

File tree

4 files changed

+246
-4
lines changed

4 files changed

+246
-4
lines changed

app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Edit/Tab/Variations/Config/Matrix.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,7 @@ protected function prepareVariations()
440440
{
441441
$productMatrix = $attributes = [];
442442
$variants = $this->getVariantAttributeComposition();
443-
foreach ($this->getAssociatedProducts() as $product) {
443+
foreach (array_reverse($this->getAssociatedProducts()) as $product) {
444444
$childProductOptions = [];
445445
foreach ($variants[$product->getId()] as $attributeComposition) {
446446
$childProductOptions[] = $this->buildChildProductOption($attributeComposition);

app/code/Magento/ConfigurableProduct/Test/Unit/Block/Adminhtml/Product/Edit/Tab/Variations/Config/MatrixTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030
use PHPUnit\Framework\MockObject\MockObject;
3131
use PHPUnit\Framework\TestCase;
3232

33+
/**
34+
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
35+
*/
3336
class MatrixTest extends TestCase
3437
{
3538
/**
@@ -199,6 +202,7 @@ public static function getVariationWizardDataProvider(): array
199202
/**
200203
* @return void
201204
* @throws Exception
205+
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
202206
*/
203207
public function testGetProductMatrix(): void
204208
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
<?php
2+
/**
3+
* Copyright 2024 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\ConfigurableProduct\Test\Unit\Ui\DataProvider\Product\Form\Modifier\Data;
9+
10+
use Magento\Catalog\Api\ProductRepositoryInterface;
11+
use Magento\Catalog\Helper\Image;
12+
use Magento\Catalog\Helper\Image as ImageHelper;
13+
use Magento\Catalog\Model\Locator\LocatorInterface;
14+
use Magento\Catalog\Model\Product;
15+
use Magento\CatalogInventory\Api\Data\StockItemInterface;
16+
use Magento\CatalogInventory\Api\StockRegistryInterface;
17+
use Magento\ConfigurableProduct\Model\Product\Type\Configurable as ConfigurableType;
18+
use Magento\ConfigurableProduct\Model\Product\Type\VariationMatrix;
19+
use Magento\ConfigurableProduct\Ui\DataProvider\Product\Form\Modifier\Data\AssociatedProducts;
20+
use Magento\Eav\Api\Data\AttributeOptionInterface;
21+
use Magento\Eav\Model\Entity\Attribute\AbstractAttribute;
22+
use Magento\Framework\Currency;
23+
use Magento\Framework\Escaper;
24+
use Magento\Framework\Json\Helper\Data as JsonHelper;
25+
use Magento\Framework\Locale\CurrencyInterface;
26+
use Magento\Framework\UrlInterface;
27+
use Magento\Store\Model\Store;
28+
use PHPUnit\Framework\MockObject\Exception;
29+
use PHPUnit\Framework\MockObject\MockObject;
30+
use PHPUnit\Framework\TestCase;
31+
32+
/**
33+
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
34+
*/
35+
class AssociatedProductsTest extends TestCase
36+
{
37+
/**
38+
* @var LocatorInterface|MockObject
39+
*/
40+
private LocatorInterface $locator;
41+
42+
/**
43+
* @var ConfigurableType|MockObject
44+
*/
45+
private ConfigurableType $configurableType;
46+
47+
/**
48+
* @var ProductRepositoryInterface|MockObject
49+
*/
50+
private ProductRepositoryInterface $productRepository;
51+
52+
/**
53+
* @var StockRegistryInterface|MockObject
54+
*/
55+
private StockRegistryInterface $stockRegistry;
56+
57+
/**
58+
* @var VariationMatrix|MockObject
59+
*/
60+
private VariationMatrix $variationMatrix;
61+
62+
/**
63+
* @var UrlInterface|MockObject
64+
*/
65+
private UrlInterface $urlBuilder;
66+
67+
/**
68+
* @var CurrencyInterface|MockObject
69+
*/
70+
private CurrencyInterface $localeCurrency;
71+
72+
/**
73+
* @var JsonHelper|MockObject
74+
*/
75+
private JsonHelper $jsonHelper;
76+
77+
/**
78+
* @var ImageHelper|MockObject
79+
*/
80+
private ImageHelper $imageHelper;
81+
82+
/**
83+
* @var Escaper|MockObject
84+
*/
85+
private Escaper $escaper;
86+
87+
/**
88+
* @inhertidoc
89+
*/
90+
protected function setUp(): void
91+
{
92+
parent::setUp();
93+
94+
$this->locator = $this->createMock(LocatorInterface::class);
95+
$this->configurableType = $this->createMock(ConfigurableType::class);
96+
$this->productRepository = $this->createMock(ProductRepositoryInterface::class);
97+
$this->stockRegistry = $this->createMock(StockRegistryInterface::class);
98+
$this->variationMatrix = $this->createMock(VariationMatrix::class);
99+
$this->urlBuilder = $this->createMock(UrlInterface::class);
100+
$this->localeCurrency = $this->createMock(CurrencyInterface::class);
101+
$this->jsonHelper = $this->createMock(JsonHelper::class);
102+
$this->imageHelper = $this->createMock(ImageHelper::class);
103+
$this->escaper = $this->createMock(Escaper::class);
104+
}
105+
106+
/**
107+
* @return void
108+
* @throws Exception
109+
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
110+
*/
111+
public function testGetProductMatrix(): void
112+
{
113+
$productId = 1;
114+
$product = $this->createMock(Product::class);
115+
$product->expects($this->any())
116+
->method('__call')
117+
->with('getAssociatedProductIds')
118+
->willReturn([$productId]);
119+
$product->expects($this->any())->method('getData')->with('attribute_code')->willReturn('attribute_value');
120+
$product->expects($this->any())->method('getId')->willReturn($productId);
121+
$product->expects($this->any())->method('getSku')->willReturn('sku');
122+
$product->expects($this->any())->method('getName')->willReturn('name');
123+
$product->expects($this->any())->method('getPrice')->willReturn(100.00);
124+
$product->expects($this->any())->method('getWeight')->willReturn(1);
125+
$product->expects($this->any())->method('getStatus')->willReturn(1);
126+
$baseCurrency = $this->createMock(\Magento\Directory\Model\Currency::class);
127+
$baseCurrency->expects($this->once())->method('getCurrencySymbol')->willReturn('$');
128+
$store = $this->createMock(Store::class);
129+
$store->expects($this->once())->method('getBaseCurrency')->willReturn($baseCurrency);
130+
$product->expects($this->once())->method('getStore')->willReturn($store);
131+
132+
$stockItem = $this->createMock(StockItemInterface::class);
133+
$stockItem->expects($this->once())->method('getQty')->willReturn(1);
134+
$this->stockRegistry->expects($this->once())->method('getStockItem')->willReturn($stockItem);
135+
$this->productRepository->expects($this->exactly(2))
136+
->method('getById')
137+
->with($productId)
138+
->willReturn($product);
139+
$this->locator->expects($this->any())
140+
->method('getProduct')
141+
->willReturn($product);
142+
$attribute = $this->createMock(AbstractAttribute::class);
143+
$attribute->expects($this->any())->method('getAttributeCode')->willReturn('attribute_code');
144+
$attribute->expects($this->any())->method('getAttributeId')->willReturn('1');
145+
$option = $this->createMock(AttributeOptionInterface::class);
146+
$option->expects($this->any())->method('getValue')->willReturn('attribute_value');
147+
$option->expects($this->any())->method('getLabel')->willReturn('attribute_label');
148+
$attribute->expects($this->any())->method('getOptions')->willReturn([$option]);
149+
$this->configurableType->expects($this->exactly(2))
150+
->method('getUsedProductAttributes')
151+
->with($product)
152+
->willReturn([$attribute]);
153+
$this->configurableType->expects($this->once())->method('getConfigurableAttributesAsArray')
154+
->willReturn(
155+
[
156+
1 => [
157+
'id' => 1,
158+
'label' => 'attribute_label',
159+
'use_default' => '0',
160+
'position' => '0',
161+
'values' => [
162+
0 => [
163+
'value_index' => 'attribute_value',
164+
'label' => 'attribute_label',
165+
'product_super_attribute_id' => '10',
166+
'default_label' => 'attribute_label',
167+
'store_label' => 'attribute_label',
168+
'use_default_value' => true
169+
]
170+
],
171+
'attribute_id' => '1',
172+
'attribute_code' => 'attribute_code',
173+
'frontend_label' => 'attribute_label',
174+
'store_label' => 'attribute_label',
175+
'options' => [
176+
0 => [
177+
'label' => 'attribute_label',
178+
'value' => 'attribute_value'
179+
]
180+
]
181+
]
182+
]
183+
);
184+
$image = $this->createMock(Image::class);
185+
$image->expects($this->once())->method('getUrl')->willReturn('image_url');
186+
$this->imageHelper->expects($this->once())
187+
->method('init')
188+
->with($product, 'product_thumbnail_image')
189+
->willReturn($image);
190+
$this->locator->expects($this->once())->method('getBaseCurrencyCode')->willReturn('USD');
191+
$this->locator->expects($this->once())->method('getStore')->willReturn($store);
192+
$currency = $this->createMock(Currency::class);
193+
$currency->expects($this->once())->method('toCurrency')->with(100.00)->willReturn('100.00$');
194+
$this->localeCurrency->expects($this->once())
195+
->method('getCurrency')
196+
->with('USD')
197+
->willReturn($currency);
198+
$this->jsonHelper->expects($this->once())
199+
->method('jsonEncode')
200+
->with(['attribute_code' => 'attribute_value'])
201+
->willReturn('{"attribute_code":"attribute_value"}');
202+
203+
$associatedProducts = new AssociatedProducts(
204+
$this->locator,
205+
$this->urlBuilder,
206+
$this->configurableType,
207+
$this->productRepository,
208+
$this->stockRegistry,
209+
$this->variationMatrix,
210+
$this->localeCurrency,
211+
$this->jsonHelper,
212+
$this->imageHelper,
213+
$this->escaper
214+
);
215+
216+
$expected = [
217+
0 => [
218+
'id' => 1,
219+
'product_link' => '<a href="" target="_blank"></a>',
220+
'sku' => 'sku',
221+
'name' => 'name',
222+
'qty' => 1,
223+
'price' => 100.0,
224+
'price_string' => '100.00$',
225+
'price_currency' => '$',
226+
'configurable_attribute' => '{"attribute_code":"attribute_value"}',
227+
'weight' => 1,
228+
'status' => 1,
229+
'variationKey' => 'attribute_value',
230+
'canEdit' => 0,
231+
'newProduct' => 0,
232+
'attributes' => ': attribute_label',
233+
'thumbnail_image' => 'image_url',
234+
]
235+
];
236+
$this->assertSame($expected, $associatedProducts->getProductMatrix());
237+
}
238+
}

app/code/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/Data/AssociatedProducts.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ protected function prepareVariations(): void
239239
$productMatrix = $attributes = [];
240240
$variants = $this->getVariantAttributeComposition();
241241
$productIds = [];
242-
foreach (array_reverse($this->getAssociatedProducts()) as $product) {
242+
foreach ($this->getAssociatedProducts() as $product) {
243243
$childProductOptions = [];
244244
$productIds[] = $product->getId();
245245
foreach ($variants[$product->getId()] as $attributeComposition) {
@@ -453,10 +453,10 @@ private function buildChildProductOption(array $attributeDetails): array
453453
* Get label for a specific value of an attribute.
454454
*
455455
* @param AbstractAttribute $attribute
456-
* @param int $valueId
456+
* @param mixed $valueId
457457
* @return string
458458
*/
459-
private function extractAttributeValueLabel(AbstractAttribute $attribute, int $valueId): string
459+
private function extractAttributeValueLabel(AbstractAttribute $attribute, mixed $valueId): string
460460
{
461461
foreach ($attribute->getOptions() as $attributeOption) {
462462
if ($attributeOption->getValue() == $valueId) {

0 commit comments

Comments
 (0)