Skip to content

Commit 0998eaf

Browse files
committed
Merge remote-tracking branch 'origin/AC-15208' into spartans_pr_26082025
2 parents b7d87d0 + f8240a5 commit 0998eaf

File tree

4 files changed

+203
-17
lines changed

4 files changed

+203
-17
lines changed

app/code/Magento/Eav/Model/Entity/Attribute/OptionManagement.php

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?php
22
/**
3-
* Copyright © Magento, Inc. All rights reserved.
4-
* See COPYING.txt for license details.
3+
* Copyright 2014 Adobe
4+
* All Rights Reserved.
55
*/
66
declare(strict_types=1);
77

@@ -13,6 +13,8 @@
1313
use Magento\Eav\Api\Data\AttributeOptionInterface;
1414
use Magento\Eav\Model\AttributeRepository;
1515
use Magento\Eav\Model\ResourceModel\Entity\Attribute;
16+
use Magento\Eav\Model\ResourceModel\Entity\Attribute\Option as AttributeOptionResource;
17+
use Magento\Framework\App\ObjectManager;
1618
use Magento\Framework\Exception\InputException;
1719
use Magento\Framework\Exception\NoSuchEntityException;
1820
use Magento\Framework\Exception\StateException;
@@ -32,17 +34,25 @@ class OptionManagement implements AttributeOptionManagementInterface, AttributeO
3234
*/
3335
protected $resourceModel;
3436

37+
/**
38+
* @var AttributeOptionResource
39+
*/
40+
protected $optionResource;
41+
3542
/**
3643
* @param AttributeRepository $attributeRepository
3744
* @param Attribute $resourceModel
45+
* @param AttributeOptionResource|null $optionResource
3846
* @codeCoverageIgnore
3947
*/
4048
public function __construct(
4149
AttributeRepository $attributeRepository,
42-
Attribute $resourceModel
50+
Attribute $resourceModel,
51+
?AttributeOptionResource $optionResource = null
4352
) {
4453
$this->attributeRepository = $attributeRepository;
4554
$this->resourceModel = $resourceModel;
55+
$this->optionResource = $optionResource ?: ObjectManager::getInstance()->get(AttributeOptionResource::class);
4656
}
4757

4858
/**
@@ -140,6 +150,12 @@ private function saveOption(
140150
$options['value'][$optionId][0] = $optionLabel;
141151
$options['order'][$optionId] = $option->getSortOrder();
142152
$options['is_default'][$optionId] = $option->getIsDefault();
153+
154+
$existingLabels = $this->optionResource->getStoreLabelsByOptionId((int)$optionId);
155+
foreach ($existingLabels as $storeId => $labelText) {
156+
$options['value'][$optionId][$storeId] ??= $labelText;
157+
}
158+
143159
if (is_array($option->getStoreLabels())) {
144160
foreach ($option->getStoreLabels() as $label) {
145161
$options['value'][$optionId][$label->getStoreId()] = $label->getLabel();

app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute/Option.php

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
<?php
22
/**
3-
* Copyright © Magento, Inc. All rights reserved.
4-
* See COPYING.txt for license details.
3+
* Copyright 2011 Adobe
4+
* All Rights Reserved.
55
*/
66
namespace Magento\Eav\Model\ResourceModel\Entity\Attribute;
77

88
/**
99
* Entity attribute option resource model
1010
*
11-
* @author Magento Core Team <[email protected]>
1211
*/
1312
class Option extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
1413
{
@@ -161,4 +160,22 @@ public function getFlatUpdateSelect(
161160

162161
return $select;
163162
}
163+
164+
/**
165+
* Get all store labels for a given option ID
166+
*
167+
* @param int $optionId
168+
* @return array<int, string> [store_id => label]
169+
*/
170+
public function getStoreLabelsByOptionId(int $optionId): array
171+
{
172+
$connection = $this->getConnection();
173+
$table = $this->getTable('eav_attribute_option_value');
174+
175+
$select = $connection->select()
176+
->from($table, ['store_id', 'value'])
177+
->where('option_id = ?', $optionId);
178+
179+
return $connection->fetchPairs($select);
180+
}
164181
}

app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/OptionManagementTest.php

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?php
22
/**
3-
* Copyright © Magento, Inc. All rights reserved.
4-
* See COPYING.txt for license details.
3+
* Copyright 2014 Adobe
4+
* All Rights Reserved.
55
*/
66
declare(strict_types=1);
77

@@ -16,6 +16,7 @@
1616
use Magento\Eav\Model\Entity\Attribute\Source\SourceInterface;
1717
use Magento\Eav\Model\Entity\Attribute\Source\Table as EavAttributeSource;
1818
use Magento\Eav\Model\ResourceModel\Entity\Attribute;
19+
use Magento\Eav\Model\ResourceModel\Entity\Attribute\Option as AttributeOptionResource;
1920
use Magento\Framework\Exception\InputException;
2021
use Magento\Framework\Exception\NoSuchEntityException;
2122
use Magento\Framework\Exception\StateException;
@@ -44,17 +45,23 @@ class OptionManagementTest extends TestCase
4445
*/
4546
protected $resourceModelMock;
4647

48+
/**
49+
* @var MockObject|AttributeOptionResource
50+
*/
51+
protected $optionResourceMock;
52+
4753
/**
4854
* @inheritdoc
4955
*/
5056
protected function setUp(): void
5157
{
5258
$this->attributeRepositoryMock = $this->createMock(AttributeRepository::class);
53-
$this->resourceModelMock =
54-
$this->createMock(Attribute::class);
59+
$this->resourceModelMock = $this->createMock(Attribute::class);
60+
$this->optionResourceMock = $this->createMock(AttributeOptionResource::class);
5561
$this->model = new OptionManagement(
5662
$this->attributeRepositoryMock,
57-
$this->resourceModelMock
63+
$this->resourceModelMock,
64+
$this->optionResourceMock
5865
);
5966
}
6067

@@ -255,6 +262,7 @@ public function testUpdate(string $label): void
255262
$optionId => [
256263
0 => $label,
257264
$storeId => $storeLabel,
265+
5 => 'otherLabelLabel'
258266
],
259267
],
260268
'order' => [
@@ -265,8 +273,17 @@ public function testUpdate(string $label): void
265273
]
266274
];
267275

276+
$this->optionResourceMock->expects($this->once())
277+
->method('getStoreLabelsByOptionId')
278+
->with($optionId)
279+
->willReturn([
280+
4 => 'oldLabelLabel',
281+
5 => 'otherLabelLabel'
282+
]);
283+
268284
$optionMock = $this->getAttributeOption();
269-
$labelMock = $this->getAttributeOptionLabel();
285+
$labelMock1 = $this->getAttributeOptionLabel();
286+
$labelMock2 = $this->getAttributeOptionLabel();
270287
/** @var SourceInterface|MockObject $sourceMock */
271288
$sourceMock = $this->createMock(EavAttributeSource::class);
272289

@@ -297,9 +314,11 @@ public function testUpdate(string $label): void
297314
$optionMock->method('getLabel')->willReturn($label);
298315
$optionMock->method('getSortOrder')->willReturn($sortOder);
299316
$optionMock->method('getIsDefault')->willReturn(true);
300-
$optionMock->method('getStoreLabels')->willReturn([$labelMock]);
301-
$labelMock->method('getStoreId')->willReturn($storeId);
302-
$labelMock->method('getLabel')->willReturn($storeLabel);
317+
$optionMock->method('getStoreLabels')->willReturn([$labelMock1, $labelMock2]);
318+
$labelMock1->method('getStoreId')->willReturn($storeId);
319+
$labelMock1->method('getLabel')->willReturn($storeLabel);
320+
$labelMock2->method('getStoreId')->willReturn(5);
321+
$labelMock2->method('getLabel')->willReturn('otherLabelLabel');
303322
$this->resourceModelMock->expects($this->once())->method('save')->with($attributeMock);
304323

305324
$this->assertEquals(

dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductAttributeOptionUpdateInterfaceTest.php

Lines changed: 136 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
11
<?php
22
/**
3-
* Copyright © Magento, Inc. All rights reserved.
4-
* See COPYING.txt for license details.
3+
* Copyright 2020 Adobe
4+
* All Rights Reserved.
55
*/
6+
67
namespace Magento\Catalog\Api;
78

9+
use Magento\Catalog\Test\Fixture\SelectAttribute as SelectAttributeFixture;
810
use Magento\Eav\Api\Data\AttributeOptionInterface;
911
use Magento\Eav\Api\Data\AttributeOptionLabelInterface;
1012
use Magento\Framework\Webapi\Rest\Request;
13+
use Magento\Store\Test\Fixture\Group as StoreGroupFixture;
14+
use Magento\Store\Test\Fixture\Store as StoreFixture;
15+
use Magento\Store\Test\Fixture\Website as WebsiteFixture;
16+
use Magento\TestFramework\Fixture\DataFixture;
17+
use Magento\TestFramework\Fixture\DataFixtureStorage;
18+
use Magento\TestFramework\Fixture\DataFixtureStorageManager;
19+
use Magento\TestFramework\Helper\Bootstrap;
20+
use Magento\TestFramework\ObjectManager;
1121
use Magento\TestFramework\TestCase\WebapiAbstract;
1222

1323
/**
@@ -20,6 +30,26 @@ class ProductAttributeOptionUpdateInterfaceTest extends WebapiAbstract
2030
private const SERVICE_VERSION = 'V1';
2131
private const RESOURCE_PATH = '/V1/products/attributes';
2232

33+
/**
34+
* @var ObjectManager
35+
*/
36+
private $objectManager;
37+
38+
/**
39+
* @var DataFixtureStorage
40+
*/
41+
private $fixtures;
42+
43+
/**
44+
* @inheritdoc
45+
*/
46+
protected function setUp(): void
47+
{
48+
parent::setUp();
49+
$this->objectManager = Bootstrap::getObjectManager();
50+
$this->fixtures = $this->objectManager->get(DataFixtureStorageManager::class)->getStorage();
51+
}
52+
2353
/**
2454
* Test to update attribute option
2555
*
@@ -67,6 +97,110 @@ public function testUpdate()
6797
}
6898
}
6999

100+
#[
101+
DataFixture(WebsiteFixture::class, as: 'website'),
102+
DataFixture(StoreGroupFixture::class, ['website_id' => '$website.id$'], 'store_group'),
103+
DataFixture(StoreFixture::class, ['store_group_id' => '$store_group.id$'], 'store1'),
104+
DataFixture(StoreFixture::class, ['store_group_id' => '$store_group.id$'], 'store2'),
105+
DataFixture(SelectAttributeFixture::class, ['default_frontend_label' => 'CustomAttr'], 'multi_store_attr'),
106+
]
107+
public function testUpdateMultistorePreservingOtherStoreLabel()
108+
{
109+
$this->_markTestAsRestOnly('Fix inconsistencies in WSDL and Data interfaces');
110+
$store1 = $this->fixtures->get('store1');
111+
$store2 = $this->fixtures->get('store2');
112+
$attributeCode = $this->fixtures->get('multi_store_attr')->getAttributeCode();
113+
$attributeLabel = 'Multi Store Option';
114+
$store1Label = 'Store 1 Label';
115+
$store2Label = 'Store 2 Label';
116+
$store1LabelUpdated = 'Store 1 Label Updated';
117+
118+
// First, create an option with multiple store labels and add the option
119+
$initialOptionData = [
120+
AttributeOptionInterface::LABEL => $attributeLabel,
121+
AttributeOptionInterface::STORE_LABELS => [
122+
[
123+
AttributeOptionLabelInterface::LABEL => $store1Label,
124+
AttributeOptionLabelInterface::STORE_ID => $store1->getId(),
125+
],
126+
[
127+
AttributeOptionLabelInterface::LABEL => $store2Label,
128+
AttributeOptionLabelInterface::STORE_ID => $store2->getId(),
129+
],
130+
],
131+
];
132+
$newOptionId = $this->webApiCallAttributeOptions(
133+
$attributeCode,
134+
Request::HTTP_METHOD_POST,
135+
'add',
136+
[
137+
'attributeCode' => $attributeCode,
138+
'option' => $initialOptionData,
139+
]
140+
);
141+
$this->assertNotNull($newOptionId, 'Option should be created successfully');
142+
143+
// Now update only the first store label
144+
$updateOptionData = [
145+
AttributeOptionInterface::LABEL => $attributeLabel,
146+
AttributeOptionInterface::VALUE => $newOptionId,
147+
AttributeOptionInterface::STORE_LABELS => [
148+
[
149+
AttributeOptionLabelInterface::LABEL => $store1LabelUpdated,
150+
AttributeOptionLabelInterface::STORE_ID => $store1->getId(),
151+
],
152+
],
153+
];
154+
$response = $this->webApiCallAttributeOptions(
155+
$attributeCode,
156+
Request::HTTP_METHOD_PUT,
157+
'update',
158+
[
159+
'attributeCode' => $attributeCode,
160+
'optionId' => $newOptionId,
161+
'option' => $updateOptionData,
162+
],
163+
$newOptionId
164+
);
165+
$this->assertTrue($response, 'Update should be successful');
166+
167+
$store1Options = $this->getAttributeOptions($attributeCode, $store1->getCode());
168+
$store2Options = $this->getAttributeOptions($attributeCode, $store2->getCode());
169+
170+
// Find the option in store1 context
171+
$store1Option = null;
172+
foreach ($store1Options as $option) {
173+
if ($option['value'] === $newOptionId) {
174+
$store1Option = $option;
175+
break;
176+
}
177+
}
178+
// Find the option in store2 context
179+
$store2Option = null;
180+
foreach ($store2Options as $option) {
181+
if ($option['value'] === $newOptionId) {
182+
$store2Option = $option;
183+
break;
184+
}
185+
}
186+
187+
// Verify that store1 label was updated
188+
$this->assertNotNull($store1Option, 'Option should exist in store1 context');
189+
$this->assertEquals(
190+
$store1LabelUpdated,
191+
$store1Option['label'],
192+
'Store1 label should be updated'
193+
);
194+
195+
// Verify that store2 label was preserved
196+
$this->assertNotNull($store2Option, 'Option should exist in store2 context');
197+
$this->assertEquals(
198+
$store2Label,
199+
$store2Option['label'],
200+
'Store2 label should be preserved'
201+
);
202+
}
203+
70204
/**
71205
* Test to update option with already exist exception
72206
*

0 commit comments

Comments
 (0)