77
88namespace Magento \Catalog \Test \Fixture ;
99
10+ use Magento \Catalog \Api \Data \ProductAttributeInterface ;
11+ use Magento \Catalog \Api \Data \ProductAttributeInterfaceFactory ;
1012use Magento \Catalog \Api \ProductAttributeManagementInterface ;
1113use Magento \Catalog \Api \ProductAttributeRepositoryInterface ;
1214use Magento \Catalog \Model \Product ;
13- use Magento \Catalog \Model \ResourceModel \Attribute as ResourceModelAttribute ;
14- use Magento \Catalog \Model \ResourceModel \Eav \Attribute as EavAttribute ;
15- use Magento \Eav \Model \AttributeFactory ;
1615use Magento \Eav \Setup \EavSetup ;
16+ use Magento \Framework \Api \DataObjectHelper ;
1717use Magento \Framework \DataObject ;
1818use Magento \TestFramework \Fixture \Api \DataMerger ;
1919use Magento \TestFramework \Fixture \Api \ServiceFactory ;
2020use Magento \TestFramework \Fixture \RevertibleDataFixtureInterface ;
2121use Magento \TestFramework \Fixture \Data \ProcessorInterface ;
2222
23+ /**
24+ * Product attribute fixture
25+ *
26+ * 1. Create an attribute with default data
27+ * <pre>
28+ * #[
29+ * DataFixture(AttributeFixture::class, as: 'attribute')
30+ * ]
31+ * </pre>
32+ * 2. Create an attribute with custom data
33+ * <pre>
34+ * #[
35+ * DataFixture(AttributeFixture::class, ['is_filterable' => true], 'attribute')
36+ * ]
37+ * </pre>
38+ * 3. Update an existing attribute
39+ * <pre>
40+ * #[
41+ * DataFixture(
42+ * AttributeFixture::class,
43+ * [
44+ * 'attribute_code' => 'price',
45+ * 'scope' => 'website',
46+ * '_update' => true
47+ * ]
48+ * )
49+ * ]
50+ * </pre>
51+ */
2352class Attribute implements RevertibleDataFixtureInterface
2453{
2554 private const DEFAULT_DATA = [
26- 'is_wysiwyg_enabled ' => false ,
27- 'is_html_allowed_on_front ' => true ,
28- 'used_for_sort_by ' => false ,
29- 'is_filterable ' => false ,
30- 'is_filterable_in_search ' => false ,
31- 'is_used_in_grid ' => true ,
32- 'is_visible_in_grid ' => true ,
33- 'is_filterable_in_grid ' => true ,
34- 'position ' => 0 ,
35- 'apply_to ' => [],
36- 'is_searchable ' => false ,
37- 'is_visible_in_advanced_search ' => false ,
38- 'is_comparable ' => false ,
39- 'is_used_for_promo_rules ' => false ,
40- 'is_visible_on_front ' => false ,
41- 'used_in_product_listing ' => false ,
42- 'is_visible ' => true ,
43- 'scope ' => 'store ' ,
44- 'attribute_code ' => 'product_attribute%uniqid% ' ,
45- 'frontend_input ' => 'text ' ,
46- 'entity_type_id ' => '4 ' ,
47- 'is_required ' => false ,
48- 'options ' => [],
49- 'is_user_defined ' => true ,
55+ ProductAttributeInterface::ATTRIBUTE_ID => null ,
56+ ProductAttributeInterface::ATTRIBUTE_CODE => 'product_attribute%uniqid% ' ,
57+ ProductAttributeInterface::ENTITY_TYPE_ID => '4 ' ,
58+ ProductAttributeInterface::SCOPE => ProductAttributeInterface::SCOPE_GLOBAL_TEXT ,
59+ ProductAttributeInterface::IS_USER_DEFINED => true ,
60+ ProductAttributeInterface::IS_SEARCHABLE => false ,
61+ ProductAttributeInterface::IS_FILTERABLE => false ,
62+ ProductAttributeInterface::IS_FILTERABLE_IN_SEARCH => false ,
63+ ProductAttributeInterface::IS_FILTERABLE_IN_GRID => true ,
64+ ProductAttributeInterface::IS_VISIBLE => true ,
65+ ProductAttributeInterface::IS_VISIBLE_IN_GRID => true ,
66+ ProductAttributeInterface::IS_VISIBLE_IN_ADVANCED_SEARCH => false ,
67+ ProductAttributeInterface::IS_VISIBLE_ON_FRONT => false ,
68+ ProductAttributeInterface::IS_USED_IN_GRID => true ,
69+ ProductAttributeInterface::IS_COMPARABLE => false ,
70+ ProductAttributeInterface::IS_USED_FOR_PROMO_RULES => false ,
71+ ProductAttributeInterface::IS_REQUIRED => false ,
72+ ProductAttributeInterface::IS_UNIQUE => false ,
73+ ProductAttributeInterface::IS_WYSIWYG_ENABLED => false ,
74+ ProductAttributeInterface::IS_HTML_ALLOWED_ON_FRONT => true ,
75+ ProductAttributeInterface::USED_IN_PRODUCT_LISTING => false ,
76+ ProductAttributeInterface::USED_FOR_SORT_BY => false ,
77+ ProductAttributeInterface::POSITION => 0 ,
78+ ProductAttributeInterface::APPLY_TO => [],
79+ ProductAttributeInterface::OPTIONS => [],
80+ ProductAttributeInterface::NOTE => null ,
81+ ProductAttributeInterface::BACKEND_TYPE => 'varchar ' ,
82+ ProductAttributeInterface::BACKEND_MODEL => null ,
83+ ProductAttributeInterface::FRONTEND_INPUT => 'text ' ,
84+ ProductAttributeInterface::FRONTEND_CLASS => null ,
85+ ProductAttributeInterface::SOURCE_MODEL => null ,
86+ ProductAttributeInterface::EXTENSION_ATTRIBUTES_KEY => [],
87+ ProductAttributeInterface::CUSTOM_ATTRIBUTES => [],
88+ ProductAttributeInterface::FRONTEND_LABELS => [],
5089 'default_frontend_label ' => 'Product Attribute%uniqid% ' ,
51- 'frontend_labels ' => [],
52- 'backend_type ' => 'varchar ' ,
53- 'is_unique ' => '0 ' ,
54- 'validation_rules ' => []
90+ 'validation_rules ' => [],
91+ "default_value " => null ,
5592 ];
5693
5794 private const DEFAULT_ATTRIBUTE_SET_DATA = [
@@ -60,103 +97,75 @@ class Attribute implements RevertibleDataFixtureInterface
6097 '_sort_order ' => 0 ,
6198 ];
6299
63- /**
64- * @var ServiceFactory
65- */
66- private $ serviceFactory ;
67-
68- /**
69- * @var ProcessorInterface
70- */
71- private $ dataProcessor ;
72-
73- /**
74- * @var EavSetup
75- */
76- private $ eavSetup ;
77-
78- /**
79- * @var ProductAttributeManagementInterface
80- */
81- private $ productAttributeManagement ;
82-
83- /**
84- * @var AttributeFactory
85- */
86- private AttributeFactory $ attributeFactory ;
87-
88- /**
89- * @var DataMerger
90- */
91- private DataMerger $ dataMerger ;
92-
93- /**
94- * @var ResourceModelAttribute
95- */
96- private ResourceModelAttribute $ resourceModelAttribute ;
97-
98- /**
99- * @var ProductAttributeRepositoryInterface
100- */
101- private ProductAttributeRepositoryInterface $ productAttributeRepository ;
102-
103100 /**
104101 * @param ServiceFactory $serviceFactory
105102 * @param ProcessorInterface $dataProcessor
106103 * @param EavSetup $eavSetup
107104 * @param ProductAttributeManagementInterface $productAttributeManagement
108- * @param AttributeFactory $attributeFactory
109- * @param DataMerger $dataMerger
110- * @param ResourceModelAttribute $resourceModelAttribute
105+ * @param ProductAttributeInterfaceFactory $attributeFactory
111106 * @param ProductAttributeRepositoryInterface $productAttributeRepository
107+ * @param DataObjectHelper $dataObjectHelper
108+ * @param DataMerger $dataMerger
112109 */
113110 public function __construct (
114- ServiceFactory $ serviceFactory ,
115- ProcessorInterface $ dataProcessor ,
116- EavSetup $ eavSetup ,
117- ProductAttributeManagementInterface $ productAttributeManagement ,
118- AttributeFactory $ attributeFactory ,
119- DataMerger $ dataMerger ,
120- ResourceModelAttribute $ resourceModelAttribute ,
121- ProductAttributeRepositoryInterface $ productAttributeRepository
111+ private readonly ServiceFactory $ serviceFactory ,
112+ private readonly ProcessorInterface $ dataProcessor ,
113+ private readonly EavSetup $ eavSetup ,
114+ private readonly ProductAttributeManagementInterface $ productAttributeManagement ,
115+ private readonly ProductAttributeInterfaceFactory $ attributeFactory ,
116+ private readonly ProductAttributeRepositoryInterface $ productAttributeRepository ,
117+ private readonly DataObjectHelper $ dataObjectHelper ,
118+ private readonly DataMerger $ dataMerger
122119 ) {
123- $ this ->serviceFactory = $ serviceFactory ;
124- $ this ->dataProcessor = $ dataProcessor ;
125- $ this ->eavSetup = $ eavSetup ;
126- $ this ->productAttributeManagement = $ productAttributeManagement ;
127- $ this ->attributeFactory = $ attributeFactory ;
128- $ this ->dataMerger = $ dataMerger ;
129- $ this ->resourceModelAttribute = $ resourceModelAttribute ;
130- $ this ->productAttributeRepository = $ productAttributeRepository ;
131120 }
132121
133122 /**
134123 * {@inheritdoc}
135124 * @param array $data Parameters. Same format as Attribute::DEFAULT_DATA.
125+ *
126+ * Additional fields:
127+ * - `_update`: boolean - whether to update attribute instead of creating a new one
128+ * - `_set_id`: int - attribute set ID to assign the attribute to
129+ * - `_group_id`: int - attribute group ID to assign the attribute to
130+ * - `_sort_order`: int - sort order within the attribute group
131+ *
136132 * @return DataObject|null
137133 */
138134 public function apply (array $ data = []): ?DataObject
139135 {
140- if (array_key_exists ('additional_data ' , $ data )) {
141- return $ this ->applyAttributeWithAdditionalData ($ data );
142- }
143-
144- $ attribute = $ this ->attributeFactory ->createAttribute (
145- EavAttribute::class,
146- $ this ->prepareData (array_diff_key ($ data , self ::DEFAULT_ATTRIBUTE_SET_DATA ))
147- );
148- $ attribute = $ this ->productAttributeRepository ->save ($ attribute );
149-
136+ $ attributeData = array_diff_key ($ data , self ::DEFAULT_ATTRIBUTE_SET_DATA );
150137 $ attributeSetData = $ this ->prepareAttributeSetData (
151138 array_intersect_key ($ data , self ::DEFAULT_ATTRIBUTE_SET_DATA )
152139 );
140+ if (!empty ($ data ['_update ' ])) {
141+ $ attribute = $ this ->productAttributeRepository ->get ($ data ['attribute_code ' ]);
142+ unset($ attributeData ['_update ' ], $ attributeData ['attribute_code ' ]);
143+ } else {
144+ $ attribute = $ this ->attributeFactory ->create ();
145+ $ attributeData = $ this ->prepareData ($ attributeData );
146+ }
153147
154- $ this ->productAttributeManagement ->assign (
155- $ attributeSetData ['_set_id ' ],
156- $ attributeSetData ['_group_id ' ],
157- $ attribute ->getAttributeCode (),
158- $ attributeSetData ['_sort_order ' ]
148+ $ this ->dataObjectHelper ->populateWithArray (
149+ $ attribute ,
150+ $ attributeData ,
151+ ProductAttributeInterface::class
159152 );
153+ // Add data that are not part of the interface
154+ $ attribute ->addData (array_diff_key ($ attributeData , self ::DEFAULT_DATA ));
155+ if (isset ($ attributeData ['scope ' ])) {
156+ $ attribute ->setScope ($ attributeData ['scope ' ]);
157+ }
158+ $ attribute = $ this ->productAttributeRepository ->save ($ attribute );
159+
160+ // Do not assign attribute if both set_id and group_id are not provided during update
161+ if (empty ($ data ['_update ' ]) || isset ($ data ['_set_id ' ], $ data ['_group_id ' ])) {
162+ $ this ->productAttributeManagement ->assign (
163+ $ attributeSetData ['_set_id ' ],
164+ $ attributeSetData ['_group_id ' ],
165+ $ attribute ->getAttributeCode (),
166+ $ attributeSetData ['_sort_order ' ]
167+ );
168+ }
160169
161170 return $ attribute ;
162171 }
@@ -166,6 +175,9 @@ public function apply(array $data = []): ?DataObject
166175 */
167176 public function revert (DataObject $ data ): void
168177 {
178+ if (!$ data ->getIsUserDefined ()) {
179+ return ;
180+ }
169181 $ service = $ this ->serviceFactory ->create (ProductAttributeRepositoryInterface::class, 'deleteById ' );
170182 $ service ->execute (
171183 [
@@ -174,26 +186,6 @@ public function revert(DataObject $data): void
174186 );
175187 }
176188
177- /**
178- * @param array $data Parameters. Same format as Attribute::DEFAULT_DATA.
179- * @return DataObject|null
180- */
181- private function applyAttributeWithAdditionalData (array $ data = []): ?DataObject
182- {
183- $ defaultData = array_merge (self ::DEFAULT_DATA , ['additional_data ' => null ]);
184- /** @var EavAttribute $attr */
185- $ attr = $ this ->attributeFactory ->createAttribute (EavAttribute::class, $ defaultData );
186- $ mergedData = $ this ->dataProcessor ->process ($ this , $ this ->dataMerger ->merge ($ defaultData , $ data ));
187-
188- $ attributeSetData = $ this ->prepareAttributeSetData (
189- array_intersect_key ($ data , self ::DEFAULT_ATTRIBUTE_SET_DATA )
190- );
191-
192- $ attr ->setData (array_merge ($ mergedData , $ attributeSetData ));
193- $ this ->resourceModelAttribute ->save ($ attr );
194- return $ attr ;
195- }
196-
197189 /**
198190 * Prepare attribute data
199191 *
@@ -202,7 +194,7 @@ private function applyAttributeWithAdditionalData(array $data = []): ?DataObject
202194 */
203195 private function prepareData (array $ data ): array
204196 {
205- $ data = array_merge (self ::DEFAULT_DATA , $ data );
197+ $ data = $ this -> dataMerger -> merge (self ::DEFAULT_DATA , $ data, false );
206198 $ data ['frontend_label ' ] ??= $ data ['default_frontend_label ' ];
207199
208200 return $ this ->dataProcessor ->process ($ this , $ data );
0 commit comments