Skip to content

Commit 86d30a5

Browse files
author
mvadim
committed
#30492 Adding required custom options to simple product removes it from parent composite products without warning
1 parent ea57677 commit 86d30a5

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,11 +7,22 @@
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\ConfigurableProduct\Model\Product\Type\Configurable;
15+
use Magento\Framework\App\ObjectManager;
16+
use Magento\Catalog\Model\Product\Type;
1117
use Magento\Framework\EntityManager\Operation\ExtensionInterface;
18+
use Magento\Catalog\Model\ResourceModel\Product\Relation;
19+
use Magento\Framework\Exception\CouldNotSaveException;
20+
use Magento\GroupedProduct\Model\Product\Type\Grouped;
1221

1322
/**
14-
* Class SaveHandler
23+
* SaveHandler for product option
24+
*
25+
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
1526
*/
1627
class SaveHandler implements ExtensionInterface
1728
{
@@ -20,21 +31,29 @@ class SaveHandler implements ExtensionInterface
2031
*/
2132
protected $optionRepository;
2233

34+
/**
35+
* @var Relation
36+
*/
37+
private $relation;
38+
2339
/**
2440
* @param OptionRepository $optionRepository
41+
* @param Relation|null $relation
2542
*/
2643
public function __construct(
27-
OptionRepository $optionRepository
44+
OptionRepository $optionRepository,
45+
?Relation $relation = null
2846
) {
2947
$this->optionRepository = $optionRepository;
48+
$this->relation = $relation ?: ObjectManager::getInstance()->get(Relation::class);
3049
}
3150

3251
/**
3352
* Perform action on relation/extension attribute
3453
*
3554
* @param object $entity
3655
* @param array $arguments
37-
* @return \Magento\Catalog\Api\Data\ProductInterface|object
56+
* @return ProductInterface|object
3857
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
3958
*/
4059
public function execute($entity, $arguments = [])
@@ -47,37 +66,66 @@ public function execute($entity, $arguments = [])
4766
$optionIds = [];
4867

4968
if ($options) {
50-
$optionIds = array_map(function ($option) {
51-
/** @var \Magento\Catalog\Model\Product\Option $option */
69+
$optionIds = array_map(function (Option $option) {
5270
return $option->getOptionId();
5371
}, $options);
5472
}
5573

56-
/** @var \Magento\Catalog\Api\Data\ProductInterface $entity */
74+
/** @var ProductInterface $entity */
5775
foreach ($this->optionRepository->getProductOptions($entity) as $option) {
5876
if (!in_array($option->getOptionId(), $optionIds)) {
5977
$this->optionRepository->delete($option);
6078
}
6179
}
6280
if ($options) {
63-
$this->processOptionsSaving($options, (bool)$entity->dataHasChangedFor('sku'), (string)$entity->getSku());
81+
$this->processOptionsSaving($options, (bool)$entity->dataHasChangedFor('sku'), $entity);
6482
}
6583

6684
return $entity;
6785
}
6886

87+
/**
88+
* Check if product doesn't belong to composite product
89+
*
90+
* @param ProductInterface $product
91+
* @return bool
92+
*/
93+
private function isProductValid(ProductInterface $product): bool
94+
{
95+
$result = true;
96+
if ($product->getTypeId() !== Type::TYPE_BUNDLE
97+
&& $product->getTypeId() !== Configurable::TYPE_CODE
98+
&& $product->getTypeId() !== Grouped::TYPE_CODE
99+
&& $this->relation->getRelationsByChildren([$product->getId()])
100+
) {
101+
$result = false;
102+
}
103+
104+
return $result;
105+
}
106+
69107
/**
70108
* Save custom options
71109
*
72110
* @param array $options
73111
* @param bool $hasChangedSku
74-
* @param string $newSku
112+
* @param ProductInterface $product
113+
* @return void
114+
* @throws CouldNotSaveException
75115
*/
76-
private function processOptionsSaving(array $options, bool $hasChangedSku, string $newSku)
116+
private function processOptionsSaving(array $options, bool $hasChangedSku, ProductInterface $product): void
77117
{
118+
$isProductValid = $this->isProductValid($product);
119+
/** @var ProductCustomOptionInterface $option */
78120
foreach ($options as $option) {
121+
if (!$isProductValid && $option->getIsRequire()) {
122+
$message = 'Required custom options cannot be added to a simple product'
123+
. ' that is a part of a composite product.';
124+
throw new CouldNotSaveException(__($message));
125+
}
126+
79127
if ($hasChangedSku && $option->hasData('product_sku')) {
80-
$option->setProductSku($newSku);
128+
$option->setProductSku($product->getSku());
81129
}
82130
$this->optionRepository->save($option);
83131
}

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)