Skip to content

Commit 3845d9d

Browse files
committed
MAGETWO-73967: [2.2.x] - [Github]Can not save attribute #5907
1 parent 2b1bf75 commit 3845d9d

File tree

7 files changed

+445
-6
lines changed

7 files changed

+445
-6
lines changed

app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ public function execute()
115115
{
116116
$data = $this->getRequest()->getPostValue();
117117
if ($data) {
118+
$this->preprocessOptionsData($data);
118119
$setId = $this->getRequest()->getParam('set');
119120

120121
$attributeSet = null;
@@ -210,7 +211,7 @@ public function execute()
210211

211212
$data['attribute_code'] = $model->getAttributeCode();
212213
$data['is_user_defined'] = $model->getIsUserDefined();
213-
$data['frontend_input'] = $model->getFrontendInput();
214+
$data['frontend_input'] = $data['frontend_input'] ?? $model->getFrontendInput();
214215
} else {
215216
/**
216217
* @todo add to helper and specify all relations for properties
@@ -311,6 +312,28 @@ public function execute()
311312
return $this->returnResult('catalog/*/', [], ['error' => true]);
312313
}
313314

315+
/**
316+
* Extract options data from serialized options field and append to data array.
317+
*
318+
* This logic is required to overcome max_input_vars php limit
319+
* that may vary and/or be inaccessible to change on different instances.
320+
*
321+
* @param array $data
322+
* @return void
323+
*/
324+
private function preprocessOptionsData(&$data)
325+
{
326+
if (isset($data['serialized_options'])) {
327+
$serializedOptions = json_decode($data['serialized_options'], JSON_OBJECT_AS_ARRAY);
328+
foreach ($serializedOptions as $serializedOption) {
329+
$option = [];
330+
parse_str($serializedOption, $option);
331+
$data = array_replace_recursive($data, $option);
332+
}
333+
}
334+
unset($data['serialized_options']);
335+
}
336+
314337
/**
315338
* @param string $path
316339
* @param array $params

app/code/Magento/Catalog/view/adminhtml/web/js/options.js

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,16 @@ define([
1313
'jquery/ui',
1414
'prototype',
1515
'form',
16-
'validation'
16+
'validation',
17+
'mage/translate'
1718
], function (jQuery, mageTemplate, rg) {
1819
'use strict';
1920

2021
return function (config) {
21-
var attributeOption = {
22+
var optionPanel = jQuery('#manage-options-panel'),
23+
optionsValues = [],
24+
editForm = jQuery('#edit_form'),
25+
attributeOption = {
2226
table: $('attribute-options-table'),
2327
itemCount: 0,
2428
totalItems: 0,
@@ -150,7 +154,7 @@ define([
150154
attributeOption.remove(event);
151155
});
152156

153-
jQuery('#manage-options-panel').on('render', function () {
157+
optionPanel.on('render', function () {
154158
attributeOption.ignoreValidate();
155159

156160
if (attributeOption.rendered) {
@@ -176,7 +180,31 @@ define([
176180
});
177181
});
178182
}
183+
editForm.on('submit', function () {
184+
optionPanel.find('input')
185+
.each(function () {
186+
if (this.disabled) {
187+
return;
188+
}
179189

190+
if (this.type === 'checkbox' || this.type === 'radio') {
191+
if (this.checked) {
192+
optionsValues.push(this.name + '=' + jQuery(this).val());
193+
}
194+
} else {
195+
optionsValues.push(this.name + '=' + jQuery(this).val());
196+
}
197+
});
198+
jQuery('<input>')
199+
.attr({
200+
type: 'hidden',
201+
name: 'serialized_options'
202+
})
203+
.val(JSON.stringify(optionsValues))
204+
.prependTo(editForm);
205+
optionPanel.find('table')
206+
.replaceWith(jQuery('<div>').text(jQuery.mage.__('Sending attribute values as package.')));
207+
});
180208
window.attributeOption = attributeOption;
181209
window.optionDefaultInputType = attributeOption.getOptionInputType();
182210

app/code/Magento/Swatches/Controller/Adminhtml/Product/Attribute/Plugin/Save.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
use Magento\Swatches\Model\Swatch;
1212

1313
/**
14-
* Class Save
14+
* Plugin for product attribute save controller.
1515
*/
1616
class Save
1717
{
@@ -24,7 +24,17 @@ class Save
2424
public function beforeDispatch(Attribute\Save $subject, RequestInterface $request)
2525
{
2626
$data = $request->getPostValue();
27+
2728
if (isset($data['frontend_input'])) {
29+
//Data is serialized to overcome issues caused by max_input_vars value if it's modification is unavailable.
30+
//See subject controller code and comments for more info.
31+
if (isset($data['serialized_swatch_values'])
32+
&& in_array($data['frontend_input'], ['swatch_visual', 'swatch_text'])
33+
) {
34+
$data['serialized_options'] = $data['serialized_swatch_values'];
35+
unset($data['serialized_swatch_values']);
36+
}
37+
2838
switch ($data['frontend_input']) {
2939
case 'swatch_visual':
3040
$data[Swatch::SWATCH_INPUT_TYPE_KEY] = Swatch::SWATCH_INPUT_TYPE_VISUAL;

app/code/Magento/Swatches/view/adminhtml/web/js/product-attributes.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,8 @@ define([
413413
};
414414

415415
$(function () {
416+
var editForm = $('#edit_form');
417+
416418
$('#frontend_input').bind('change', function () {
417419
swatchProductAttributes.bindAttributeInputType();
418420
});
@@ -426,6 +428,32 @@ define([
426428
$('.attribute-popup .collapse, [data-role="advanced_fieldset-content"]')
427429
.collapsable()
428430
.collapse('hide');
431+
432+
editForm.on('submit', function () {
433+
var activePanel,
434+
swatchValues = [],
435+
swatchVisualPanel = $('#swatch-visual-options-panel'),
436+
swatchTextPanel = $('#swatch-text-options-panel');
437+
438+
activePanel = swatchTextPanel.is(':visible') ? swatchTextPanel : swatchVisualPanel;
439+
440+
activePanel.find('table input')
441+
.each(function () {
442+
swatchValues.push(this.name + '=' + $(this).val());
443+
});
444+
445+
$('<input>').attr({
446+
type: 'hidden',
447+
name: 'serialized_swatch_values'
448+
})
449+
.val(JSON.stringify(swatchValues))
450+
.prependTo(editForm);
451+
452+
[swatchVisualPanel, swatchTextPanel].forEach(function (el) {
453+
$(el).find('table')
454+
.replaceWith($('<div>').text($.mage.__('Sending swatch values as package.')));
455+
});
456+
});
429457
});
430458

431459
window.saveAttributeInNewSet = swatchProductAttributes.saveAttributeInNewSet;

dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/AttributeTest.php

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
*/
66
namespace Magento\Catalog\Controller\Adminhtml\Product;
77

8+
use Magento\Framework\Exception\LocalizedException;
9+
810
/**
911
* @magentoAppArea adminhtml
1012
* @magentoDbIsolation enabled
@@ -221,6 +223,110 @@ public function testSaveActionCleanAttributeLabelCache()
221223
$this->assertEquals('new string translation', $this->_translate('string to translate'));
222224
}
223225

226+
/**
227+
* Get attribute data preset.
228+
*
229+
* @return array
230+
*/
231+
private function getLargeOptionsSetAttributeData(): array
232+
{
233+
return [
234+
'frontend_label' => [
235+
0 => 'testdrop1',
236+
1 => '',
237+
2 => '',
238+
],
239+
'frontend_input' => 'select',
240+
'is_required' => '0',
241+
'update_product_preview_image' => '0',
242+
'use_product_image_for_swatch' => '0',
243+
'visual_swatch_validation' => '',
244+
'visual_swatch_validation_unique' => '',
245+
'text_swatch_validation' => '',
246+
'text_swatch_validation_unique' => '',
247+
'attribute_code' => 'test_many_options',
248+
'is_global' => '0',
249+
'default_value_text' => '',
250+
'default_value_yesno' => '0',
251+
'default_value_date' => '',
252+
'default_value_textarea' => '',
253+
'is_unique' => '0',
254+
'is_used_in_grid' => '1',
255+
'is_visible_in_grid' => '1',
256+
'is_filterable_in_grid' => '1',
257+
'is_searchable' => '0',
258+
'is_comparable' => '0',
259+
'is_filterable' => '0',
260+
'is_filterable_in_search' => '0',
261+
'is_used_for_promo_rules' => '0',
262+
'is_html_allowed_on_front' => '1',
263+
'is_visible_on_front' => '0',
264+
'used_in_product_listing' => '0',
265+
'used_for_sort_by' => '0',
266+
'swatch_input_type' => 'dropdown',
267+
];
268+
}
269+
270+
/**
271+
* Test attribute saving with large amount of options exceeding maximum allowed by max_input_vars limit.
272+
*
273+
* @return void
274+
*/
275+
public function testLargeOptionsDataSet()
276+
{
277+
$maxInputVars = ini_get('max_input_vars');
278+
// Each option is at least 4 variables array (order, admin value, first store view value, delete flag).
279+
// Set options count to exceed max_input_vars by 100 options (400 variables).
280+
$optionsCount = floor($maxInputVars / 4) + 100;
281+
$attributeData = $this->getLargeOptionsSetAttributeData();
282+
$optionsData = [];
283+
$expectedOptionsLabels = [];
284+
for ($i = 0; $i < $optionsCount; $i++) {
285+
$order = $i + 1;
286+
$expectedOptionLabelOnStoreView = "value_{$i}_store_1";
287+
$expectedOptionsLabels[$i+1] = $expectedOptionLabelOnStoreView;
288+
$optionsData []= "option[order][option_{$i}]={$order}";
289+
$optionsData []= "option[value][option_{$i}][0]=value_{$i}_admin";
290+
$optionsData []= "option[value][option_{$i}][1]={$expectedOptionLabelOnStoreView}";
291+
$optionsData []= "option[delete][option_{$i}=";
292+
}
293+
$attributeData['serialized_options'] = json_encode($optionsData);
294+
$this->getRequest()->setPostValue($attributeData);
295+
$this->dispatch('backend/catalog/product_attribute/save');
296+
$entityTypeId = $this->_objectManager->create(
297+
\Magento\Eav\Model\Entity::class
298+
)->setType(
299+
\Magento\Catalog\Model\Product::ENTITY
300+
)->getTypeId();
301+
302+
/** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */
303+
$attribute = $this->_objectManager->create(
304+
\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class
305+
)->setEntityTypeId(
306+
$entityTypeId
307+
);
308+
try {
309+
$attribute->loadByCode($entityTypeId, 'test_many_options');
310+
$options = $attribute->getOptions();
311+
// assert that all options are saved without truncation
312+
$this->assertEquals(
313+
$optionsCount + 1,
314+
count($options),
315+
'Expected options count does not match (regarding first empty option for non-required attribute)'
316+
);
317+
318+
foreach ($expectedOptionsLabels as $optionOrderNum => $label) {
319+
$this->assertEquals(
320+
$label,
321+
$options[$optionOrderNum]->getLabel(),
322+
"Label for option #{$optionOrderNum} does not match expected."
323+
);
324+
}
325+
} catch (LocalizedException $e) {
326+
$this->fail('Test failed with exception on attribute model load: ' . $e);
327+
}
328+
}
329+
224330
/**
225331
* Return translation for a string literal belonging to backend area
226332
*

0 commit comments

Comments
 (0)