Skip to content

Commit a966bf7

Browse files
author
Yu Tang
committed
MAGETWO-28254: ConfigurableProduct Integration API
- Added validation when linking products to configurable product
1 parent e859bfc commit a966bf7

File tree

3 files changed

+269
-12
lines changed

3 files changed

+269
-12
lines changed

app/code/Magento/ConfigurableProduct/Model/Plugin/AroundProductRepositorySave.php

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,20 @@
77

88
namespace Magento\ConfigurableProduct\Model\Plugin;
99

10+
use Magento\Framework\Exception\InputException;
11+
1012
class AroundProductRepositorySave
1113
{
1214
/**
1315
* @var \Magento\ConfigurableProduct\Api\OptionRepositoryInterface
1416
*/
1517
protected $optionRepository;
1618

19+
/**
20+
* @var \Magento\Catalog\Model\ProductFactory
21+
*/
22+
protected $productFactory;
23+
1724
/**
1825
* Type configurable factory
1926
*
@@ -28,15 +35,18 @@ class AroundProductRepositorySave
2835

2936
/**
3037
* @param \Magento\ConfigurableProduct\Api\OptionRepositoryInterface $optionRepository
38+
* @param \Magento\Catalog\Model\ProductFactory $productFactory
3139
* @param \Magento\ConfigurableProduct\Model\Resource\Product\Type\Configurable\Attribute\Price\Data $priceData
3240
* @param \Magento\ConfigurableProduct\Model\Resource\Product\Type\ConfigurableFactory $typeConfigurableFactory
3341
*/
3442
public function __construct(
3543
\Magento\ConfigurableProduct\Api\OptionRepositoryInterface $optionRepository,
44+
\Magento\Catalog\Model\ProductFactory $productFactory,
3645
\Magento\ConfigurableProduct\Model\Resource\Product\Type\Configurable\Attribute\Price\Data $priceData,
3746
\Magento\ConfigurableProduct\Model\Resource\Product\Type\ConfigurableFactory $typeConfigurableFactory
3847
) {
3948
$this->optionRepository = $optionRepository;
49+
$this->productFactory = $productFactory;
4050
$this->priceData = $priceData;
4151
$this->typeConfigurableFactory = $typeConfigurableFactory;
4252
}
@@ -119,14 +129,53 @@ protected function saveConfigurableProductOptions(
119129

120130
/**
121131
* @param \Magento\Catalog\Api\Data\ProductInterface $product
122-
* @param int[] $links
132+
* @param int[] $linkIds
123133
* @return $this
124134
*/
125135
protected function saveConfigurableProductLinks(
126136
\Magento\Catalog\Api\Data\ProductInterface $product,
127-
array $links
137+
array $linkIds
128138
) {
129-
$this->typeConfigurableFactory->create()->saveProducts($product, $links);
139+
$configurableProductTypeResource = $this->typeConfigurableFactory->create();
140+
if (!empty($linkIds)) {
141+
/** @var \Magento\ConfigurableProduct\Model\Product\Type\Configurable $configurableProductType */
142+
$configurableProductType = $product->getTypeInstance();
143+
$configurableAttributes = $configurableProductType->getConfigurableAttributes($product);
144+
$attributeCodes = [];
145+
foreach ($configurableAttributes as $configurableAttribute) {
146+
/** @var \Magento\Catalog\Model\Resource\Eav\Attribute $productAttribute */
147+
$productAttribute = $configurableAttribute->getProductAttribute();
148+
$attributeCode = $productAttribute->getAttributeCode();
149+
$attributeCodes[] = $attributeCode;
150+
}
151+
$this->validateProductLinks($attributeCodes, $linkIds);
152+
}
153+
154+
$configurableProductTypeResource->saveProducts($product, $linkIds);
155+
return $this;
156+
}
157+
158+
/**
159+
* @param array $attributeCodes
160+
* @param array $linkIds
161+
* @throws InputException
162+
* @return $this
163+
*/
164+
protected function validateProductLinks(array $attributeCodes, array $linkIds)
165+
{
166+
foreach ($linkIds as $productId) {
167+
$variation = $this->productFactory->create()->load($productId);
168+
if (!$variation->getId()) {
169+
throw new InputException(__('Product with id "%1" does not exist.', $productId));
170+
}
171+
foreach ($attributeCodes as $attributeCode) {
172+
if (!$variation->getData($attributeCode)) {
173+
throw new InputException(
174+
__('Product with id "%1" does not contain required attribute "%2".', $productId, $attributeCode)
175+
);
176+
}
177+
}
178+
}
130179
return $this;
131180
}
132181
}

app/code/Magento/ConfigurableProduct/Test/Unit/Model/Plugin/AroundProductRepositorySaveTest.php

Lines changed: 198 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ class AroundProductRepositorySaveTest extends \PHPUnit_Framework_TestCase
2525
*/
2626
protected $productOptionRepositoryMock;
2727

28+
/**
29+
* @var \PHPUnit_Framework_MockObject_MockObject
30+
*/
31+
protected $productFactoryMock;
32+
2833
/**
2934
* @var \PHPUnit_Framework_MockObject_MockObject
3035
*/
@@ -70,19 +75,29 @@ protected function setUp()
7075
$this->productInterfaceMock = $this->getMock('\Magento\Catalog\Api\Data\ProductInterface');
7176
$this->productMock = $this->getMock(
7277
'Magento\Catalog\Model\Product',
73-
['getExtensionAttributes', 'getTypeId', 'getSku', 'getStoreId', 'getId'],
78+
['getExtensionAttributes', 'getTypeId', 'getSku', 'getStoreId', 'getId', 'getTypeInstance'],
7479
[],
7580
'',
7681
false
7782
);
7883
$this->closureMock = function () {
7984
return $this->productMock;
8085
};
81-
$this->plugin = new AroundProductRepositorySave(
82-
$this->productOptionRepositoryMock,
83-
$this->priceDataMock,
84-
$this->configurableTypeFactoryMock
86+
87+
$this->productFactoryMock = $this->getMockBuilder('\Magento\Catalog\Model\ProductFactory')
88+
->disableOriginalConstructor()
89+
->getMock();
90+
$objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
91+
$this->plugin = $objectManager->getObject(
92+
'Magento\ConfigurableProduct\Model\Plugin\AroundProductRepositorySave',
93+
[
94+
'optionRepository' => $this->productOptionRepositoryMock,
95+
'productFactory' => $this->productFactoryMock,
96+
'priceData' => $this->priceDataMock,
97+
'typeConfigurableFactory' => $this->configurableTypeFactoryMock
98+
]
8599
);
100+
86101
$this->productExtensionMock = $this->getMock(
87102
'Magento\Catalog\Api\Data\ProductExtension',
88103
[
@@ -108,7 +123,7 @@ public function testAroundSaveWhenProductIsSimple()
108123
);
109124
}
110125

111-
public function testAroundSaveWhenProductIsConfigurableWithoutOptions()
126+
public function testAroundSaveWithoutOptions()
112127
{
113128
$this->productInterfaceMock->expects($this->once())->method('getTypeId')
114129
->willReturn(\Magento\ConfigurableProduct\Model\Product\Type\Configurable::TYPE_CODE);
@@ -131,9 +146,89 @@ public function testAroundSaveWhenProductIsConfigurableWithoutOptions()
131146
);
132147
}
133148

134-
public function testAroundSaveWhenProductIsConfigurableWithLinks()
149+
protected function setupProducts($productIds, $attributeCode, $additionalProductId = null)
150+
{
151+
$count = 0;
152+
$products = [];
153+
foreach ($productIds as $productId) {
154+
$productMock = $this->getMockBuilder('\Magento\Catalog\Model\Product')
155+
->disableOriginalConstructor()
156+
->getMock();
157+
$productMock->expects($this->once())
158+
->method('load')
159+
->with($productId)
160+
->willReturnSelf();
161+
$productMock->expects($this->once())
162+
->method('getId')
163+
->willReturn($productId);
164+
$productMock->expects($this->once())
165+
->method('getData')
166+
->with($attributeCode)
167+
->willReturn('value');
168+
$this->productFactoryMock->expects($this->at($count))
169+
->method('create')
170+
->willReturn($productMock);
171+
$products[] = $productMock;
172+
$count++;
173+
}
174+
175+
if ($additionalProductId) {
176+
$nonExistingProductMock = $this->getMockBuilder('\Magento\Catalog\Model\Product')
177+
->disableOriginalConstructor()
178+
->getMock();
179+
$nonExistingProductMock->expects($this->once())
180+
->method('load')
181+
->with($additionalProductId)
182+
->willReturnSelf();
183+
$this->productFactoryMock->expects($this->at($count))
184+
->method('create')
185+
->willReturn($nonExistingProductMock);
186+
$products[] = $nonExistingProductMock;
187+
}
188+
return $products;
189+
}
190+
191+
protected function setupConfigurableProductAttributes($attributeCodes)
192+
{
193+
$configurableProductTypeMock = $this->getMockBuilder(
194+
'\Magento\ConfigurableProduct\Model\Product\Type\Configurable'
195+
)->disableOriginalConstructor()->getMock();
196+
197+
$this->productMock->expects($this->once())
198+
->method('getTypeInstance')
199+
->willReturn($configurableProductTypeMock);
200+
201+
$configurableAttributes = [];
202+
foreach ($attributeCodes as $attributeCode) {
203+
$configurableAttribute = $this->getMockBuilder(
204+
'\Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute'
205+
)->setMethods(['getProductAttribute'])
206+
->disableOriginalConstructor()
207+
->getMock();
208+
$productAttributeMock = $this->getMockBuilder('\Magento\Catalog\Model\Resource\Eav\Attribute')
209+
->disableOriginalConstructor()
210+
->getMock();
211+
$productAttributeMock->expects($this->once())
212+
->method('getAttributeCode')
213+
->willReturn($attributeCode);
214+
$configurableAttribute->expects($this->once())
215+
->method('getProductAttribute')
216+
->willReturn($productAttributeMock);
217+
$configurableAttributes[] = $configurableAttribute;
218+
}
219+
220+
$configurableProductTypeMock->expects($this->once())
221+
->method('getConfigurableAttributes')
222+
->with($this->productMock)
223+
->willReturn($configurableAttributes);
224+
225+
return $this;
226+
}
227+
228+
public function testAroundSaveWithLinks()
135229
{
136230
$links = [4, 5];
231+
$configurableAttributeCode = 'color';
137232
$this->productMock->expects($this->once())->method('getTypeId')
138233
->willReturn(\Magento\ConfigurableProduct\Model\Product\Type\Configurable::TYPE_CODE);
139234
$this->productMock->expects($this->once())
@@ -146,6 +241,9 @@ public function testAroundSaveWhenProductIsConfigurableWithLinks()
146241
->method('getConfigurableProductLinks')
147242
->willReturn($links);
148243

244+
$this->setupConfigurableProductAttributes([$configurableAttributeCode]);
245+
$this->setupProducts($links, $configurableAttributeCode);
246+
149247
$configurableTypeMock = $this->getMockBuilder(
150248
'\Magento\ConfigurableProduct\Model\Resource\Product\Type\Configurable'
151249
)->disableOriginalConstructor()->getMock();
@@ -157,7 +255,7 @@ public function testAroundSaveWhenProductIsConfigurableWithLinks()
157255
->with($this->productMock, $links);
158256

159257
$productId = 3;
160-
$this->productMock->expects($this->once())
258+
$this->productMock->expects($this->any())
161259
->method('getId')
162260
->willReturn($productId);
163261
$this->priceDataMock->expects($this->once())
@@ -176,7 +274,98 @@ public function testAroundSaveWhenProductIsConfigurableWithLinks()
176274
);
177275
}
178276

179-
public function testAroundSaveWhenProductIsConfigurableWithOptions()
277+
/**
278+
* @expectedException \Magento\Framework\Exception\InputException
279+
* @expectedExceptionMessage Product with id "6" does not exist.
280+
*/
281+
public function testAroundSaveWithNonExistingLinks()
282+
{
283+
$links = [4, 5];
284+
$nonExistingId = 6;
285+
$configurableAttributeCode = 'color';
286+
287+
$this->setupConfigurableProductAttributes([$configurableAttributeCode]);
288+
$productMocks = $this->setupProducts($links, $configurableAttributeCode, $nonExistingId);
289+
$nonExistingProductMock = $productMocks[2];
290+
$nonExistingProductMock->expects($this->once())
291+
->method('getId')
292+
->willReturn(null);
293+
$links[] = $nonExistingId;
294+
295+
$this->productMock->expects($this->once())->method('getTypeId')
296+
->willReturn(\Magento\ConfigurableProduct\Model\Product\Type\Configurable::TYPE_CODE);
297+
$this->productMock->expects($this->once())
298+
->method('getExtensionAttributes')
299+
->willReturn($this->productExtensionMock);
300+
$this->productExtensionMock->expects($this->once())
301+
->method('getConfigurableProductOptions')
302+
->willReturn(null);
303+
$this->productExtensionMock->expects($this->once())
304+
->method('getConfigurableProductLinks')
305+
->willReturn($links);
306+
307+
$configurableTypeMock = $this->getMockBuilder(
308+
'\Magento\ConfigurableProduct\Model\Resource\Product\Type\Configurable'
309+
)->disableOriginalConstructor()->getMock();
310+
$this->configurableTypeFactoryMock->expects($this->once())
311+
->method('create')
312+
->willReturn($configurableTypeMock);
313+
$configurableTypeMock->expects($this->never())
314+
->method('saveProducts')
315+
->with($this->productMock, $links);
316+
317+
$this->plugin->aroundSave($this->productRepositoryMock, $this->closureMock, $this->productMock);
318+
}
319+
320+
/**
321+
* @expectedException \Magento\Framework\Exception\InputException
322+
* @expectedExceptionMessage Product with id "6" does not contain required attribute "color".
323+
*/
324+
public function testAroundSaveWithLinksWithMissingAttribute()
325+
{
326+
$links = [4, 5];
327+
$simpleProductId = 6;
328+
$configurableAttributeCode = 'color';
329+
330+
$this->setupConfigurableProductAttributes([$configurableAttributeCode]);
331+
$productMocks = $this->setupProducts($links, $configurableAttributeCode, $simpleProductId);
332+
$simpleProductMock = $productMocks[2];
333+
$simpleProductMock->expects($this->once())
334+
->method('getId')
335+
->willReturn($simpleProductId);
336+
$simpleProductMock->expects($this->once())
337+
->method('getData')
338+
->with($configurableAttributeCode)
339+
->willReturn(null);
340+
341+
$links[] = $simpleProductId;
342+
343+
$this->productMock->expects($this->once())->method('getTypeId')
344+
->willReturn(\Magento\ConfigurableProduct\Model\Product\Type\Configurable::TYPE_CODE);
345+
$this->productMock->expects($this->once())
346+
->method('getExtensionAttributes')
347+
->willReturn($this->productExtensionMock);
348+
$this->productExtensionMock->expects($this->once())
349+
->method('getConfigurableProductOptions')
350+
->willReturn(null);
351+
$this->productExtensionMock->expects($this->once())
352+
->method('getConfigurableProductLinks')
353+
->willReturn($links);
354+
355+
$configurableTypeMock = $this->getMockBuilder(
356+
'\Magento\ConfigurableProduct\Model\Resource\Product\Type\Configurable'
357+
)->disableOriginalConstructor()->getMock();
358+
$this->configurableTypeFactoryMock->expects($this->once())
359+
->method('create')
360+
->willReturn($configurableTypeMock);
361+
$configurableTypeMock->expects($this->never())
362+
->method('saveProducts')
363+
->with($this->productMock, $links);
364+
365+
$this->plugin->aroundSave($this->productRepositoryMock, $this->closureMock, $this->productMock);
366+
}
367+
368+
public function testAroundSaveWithOptions()
180369
{
181370
$productSku = "configurable_sku";
182371
$this->productInterfaceMock->expects($this->once())->method('getTypeId')

dev/tests/api-functional/testsuite/Magento/ConfigurableProduct/Api/ProductRepositoryTest.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,25 @@ public function testUpdateConfigurableProductLinks()
269269
$this->assertEquals($options, $currentOptions);
270270
}
271271

272+
/**
273+
* @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable.php
274+
* @expectedException \Exception
275+
* @expectedExceptionMessage Product with id "%1" does not exist.
276+
*/
277+
public function testUpdateConfigurableProductLinksWithNonExistingProduct()
278+
{
279+
$productId1 = 10;
280+
$nonExistingId = 999;
281+
282+
$response = $this->createConfigurableProduct();
283+
//leave existing option untouched
284+
unset($response[ExtensibleDataInterface::EXTENSION_ATTRIBUTES_KEY]['configurable_product_options']);
285+
$response[ExtensibleDataInterface::EXTENSION_ATTRIBUTES_KEY]['configurable_product_links'] = [
286+
$productId1, $nonExistingId
287+
];
288+
$this->saveProduct($response);
289+
}
290+
272291
/**
273292
* Get product
274293
*

0 commit comments

Comments
 (0)