Skip to content

Commit 178d22f

Browse files
ENGCOM-8838: Adding required custom options to simple product removes it from parent composite products without warning #31645
2 parents 457ec61 + c601f1d commit 178d22f

File tree

3 files changed

+109
-13
lines changed

3 files changed

+109
-13
lines changed

app/code/Magento/Catalog/Model/Product/Option/SaveHandler.php

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,34 +7,55 @@
77

88
namespace Magento\Catalog\Model\Product\Option;
99

10+
use Magento\Catalog\Api\Data\ProductCustomOptionInterface;
11+
use Magento\Catalog\Api\Data\ProductInterface;
1012
use Magento\Catalog\Api\ProductCustomOptionRepositoryInterface as OptionRepository;
13+
use Magento\Catalog\Model\Product\Option;
14+
use Magento\Framework\App\ObjectManager;
1115
use Magento\Framework\EntityManager\Operation\ExtensionInterface;
16+
use Magento\Catalog\Model\ResourceModel\Product\Relation;
17+
use Magento\Framework\Exception\CouldNotSaveException;
1218

1319
/**
14-
* Class SaveHandler
20+
* SaveHandler for product option
21+
*
22+
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
1523
*/
1624
class SaveHandler implements ExtensionInterface
1725
{
26+
/**
27+
* @var string[]
28+
*/
29+
private $compositeProductTypes = ['grouped', 'configurable', 'bundle'];
30+
1831
/**
1932
* @var OptionRepository
2033
*/
2134
protected $optionRepository;
2235

36+
/**
37+
* @var Relation
38+
*/
39+
private $relation;
40+
2341
/**
2442
* @param OptionRepository $optionRepository
43+
* @param Relation|null $relation
2544
*/
2645
public function __construct(
27-
OptionRepository $optionRepository
46+
OptionRepository $optionRepository,
47+
?Relation $relation = null
2848
) {
2949
$this->optionRepository = $optionRepository;
50+
$this->relation = $relation ?: ObjectManager::getInstance()->get(Relation::class);
3051
}
3152

3253
/**
3354
* Perform action on relation/extension attribute
3455
*
3556
* @param object $entity
3657
* @param array $arguments
37-
* @return \Magento\Catalog\Api\Data\ProductInterface|object
58+
* @return ProductInterface|object
3859
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
3960
*/
4061
public function execute($entity, $arguments = [])
@@ -47,20 +68,19 @@ public function execute($entity, $arguments = [])
4768
$optionIds = [];
4869

4970
if ($options) {
50-
$optionIds = array_map(function ($option) {
51-
/** @var \Magento\Catalog\Model\Product\Option $option */
71+
$optionIds = array_map(function (Option $option) {
5272
return $option->getOptionId();
5373
}, $options);
5474
}
5575

56-
/** @var \Magento\Catalog\Api\Data\ProductInterface $entity */
76+
/** @var ProductInterface $entity */
5777
foreach ($this->optionRepository->getProductOptions($entity) as $option) {
5878
if (!in_array($option->getOptionId(), $optionIds)) {
5979
$this->optionRepository->delete($option);
6080
}
6181
}
6282
if ($options) {
63-
$this->processOptionsSaving($options, (bool)$entity->dataHasChangedFor('sku'), (string)$entity->getSku());
83+
$this->processOptionsSaving($options, (bool)$entity->dataHasChangedFor('sku'), $entity);
6484
}
6585

6686
return $entity;
@@ -71,15 +91,43 @@ public function execute($entity, $arguments = [])
7191
*
7292
* @param array $options
7393
* @param bool $hasChangedSku
74-
* @param string $newSku
94+
* @param ProductInterface $product
95+
* @return void
96+
* @throws CouldNotSaveException
7597
*/
76-
private function processOptionsSaving(array $options, bool $hasChangedSku, string $newSku)
98+
private function processOptionsSaving(array $options, bool $hasChangedSku, ProductInterface $product): void
7799
{
100+
$isProductHasRelations = $this->isProductHasRelations($product);
101+
/** @var ProductCustomOptionInterface $option */
78102
foreach ($options as $option) {
103+
if (!$isProductHasRelations && $option->getIsRequire()) {
104+
$message = 'Required custom options cannot be added to a simple product'
105+
. ' that is a part of a composite product.';
106+
throw new CouldNotSaveException(__($message));
107+
}
108+
79109
if ($hasChangedSku && $option->hasData('product_sku')) {
80-
$option->setProductSku($newSku);
110+
$option->setProductSku($product->getSku());
81111
}
82112
$this->optionRepository->save($option);
83113
}
84114
}
115+
116+
/**
117+
* Check if product doesn't belong to composite product
118+
*
119+
* @param ProductInterface $product
120+
* @return bool
121+
*/
122+
private function isProductHasRelations(ProductInterface $product): bool
123+
{
124+
$result = true;
125+
if (!in_array($product->getId(), $this->compositeProductTypes)
126+
&& $this->relation->getRelationsByChildren([$product->getId()])
127+
) {
128+
$result = false;
129+
}
130+
131+
return $result;
132+
}
85133
}

app/code/Magento/Catalog/Test/Unit/Model/Product/Option/SaveHandlerTest.php

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,13 @@
1111
use Magento\Catalog\Model\Product\Option;
1212
use Magento\Catalog\Model\Product\Option\Repository;
1313
use Magento\Catalog\Model\Product\Option\SaveHandler;
14+
use Magento\Catalog\Model\ResourceModel\Product\Relation;
1415
use PHPUnit\Framework\MockObject\MockObject;
1516
use PHPUnit\Framework\TestCase;
1617

18+
/**
19+
* Test for \Magento\Catalog\Model\Product\Option\SaveHandler.
20+
*/
1721
class SaveHandlerTest extends TestCase
1822
{
1923
/**
@@ -36,6 +40,14 @@ class SaveHandlerTest extends TestCase
3640
*/
3741
protected $optionRepository;
3842

43+
/**
44+
* @var Relation|MockObject
45+
*/
46+
private $relationMock;
47+
48+
/**
49+
* @inheridoc
50+
*/
3951
protected function setUp(): void
4052
{
4153
$this->entity = $this->getMockBuilder(Product::class)
@@ -47,11 +59,19 @@ protected function setUp(): void
4759
$this->optionRepository = $this->getMockBuilder(Repository::class)
4860
->disableOriginalConstructor()
4961
->getMock();
62+
$this->relationMock = $this->getMockBuilder(Relation::class)
63+
->disableOriginalConstructor()
64+
->getMock();
5065

51-
$this->model = new SaveHandler($this->optionRepository);
66+
$this->model = new SaveHandler($this->optionRepository, $this->relationMock);
5267
}
5368

54-
public function testExecute()
69+
/**
70+
* Test for execute
71+
*
72+
* @return void
73+
*/
74+
public function testExecute(): void
5575
{
5676
$this->optionMock->expects($this->any())->method('getOptionId')->willReturn(5);
5777
$this->entity->expects($this->once())->method('getOptions')->willReturn([$this->optionMock]);

dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/Type/ConfigurableTest.php

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@
1212
use Magento\Catalog\Api\ProductRepositoryInterface;
1313
use Magento\Catalog\Model\Product;
1414
use Magento\TestFramework\Helper\Bootstrap;
15+
use Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory;
16+
use PHPUnit\Framework\TestCase;
1517

1618
/**
1719
* Class ConfigurableTest
1820
*
1921
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
2022
*/
21-
class ConfigurableTest extends \PHPUnit\Framework\TestCase
23+
class ConfigurableTest extends TestCase
2224
{
2325
/**
2426
* Object under test
@@ -645,4 +647,30 @@ protected function getUsedProducts()
645647
$product->load(1);
646648
return $this->model->getUsedProducts($product);
647649
}
650+
651+
/**
652+
* Unable to save product required option to product which is a part of configurable product
653+
*
654+
* @magentoDataFixture Magento/ConfigurableProduct/_files/configurable_product_with_two_child_products.php
655+
* @return void
656+
*/
657+
public function testAddCustomOptionToConfigurableChildProduct(): void
658+
{
659+
$this->expectErrorMessage(
660+
'Required custom options cannot be added to a simple product that is a part of a composite product.'
661+
);
662+
663+
$sku = 'Simple option 1';
664+
$product = $this->productRepository->get($sku);
665+
$optionRepository = Bootstrap::getObjectManager()->get(ProductCustomOptionInterfaceFactory::class);
666+
$createdOption = $optionRepository->create(
667+
['data' => ['title' => 'drop_down option', 'type' => 'drop_down', 'sort_order' => 4, 'is_require' => 1]]
668+
);
669+
$createdOption->setProductSku($product->getSku());
670+
$product->setOptions([$createdOption]);
671+
$this->productRepository->save($product);
672+
673+
$product = $this->productRepository->get($sku);
674+
$this->assertEmpty($product->getOptions());
675+
}
648676
}

0 commit comments

Comments
 (0)