• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

wp-graphql / wp-graphql-woocommerce / 23354837106

20 Mar 2026 05:31PM UTC coverage: 83.637% (+0.4%) from 83.244%
23354837106

push

github

web-flow
feat: Product and product attribute mutations (#851)

* devops: Product and product attribute mutation tests generated

* feat: Product Mutations implemented and tested

* chore: Linter and PHPStan compliance met

* fix: resolve test failures in product and variation mutation tests

- Removed duplicate requires for payment-method classes (rebase artifact)
- Wrapped bare `attributes` queries in `... on SimpleProduct` inline
  fragments (attributes field is on ProductWithAttributes interface,
  not the base Product type)
- Fixed attribute label expectations to match factory output (lowercase)
- Fixed relay ID prefix from 'product'/'product_variation' to 'post'
  to match WPGraphQL's actual encoding
- Cache WP_Post before force-deletion and re-cache after so the Product
  model's type resolver can still determine the GraphQL type in the
  response
- Added wp_cache_flush() before asserting product deletion to avoid
  false positives from the re-cached post

* chore: fix PHPCS lint errors in mutation files

* fix: resolve CI test failures for attribute and variation mutations

- Fixed ProductVariation type registration in schema filters so the
  variation mutation output field resolves correctly
- Renamed attribute slug from 'pattern' to 'fabric' in
  ProductAttributeMutationsTest to avoid collision with the pattern
  attribute created by ProductAttributeQueriesTest
- Fixed PHPStan errors for redundant is_wp_error checks
- Updated IntrospectionQueryTest

1251 of 1437 new or added lines in 23 files covered. (87.06%)

13995 of 16733 relevant lines covered (83.64%)

91.46 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

66.45
/includes/data/mutation/class-product-mutation.php
1
<?php
2
/**
3
 * Defines helper functions for executing mutations related to products.
4
 *
5
 * @package WPGraphQL\WooCommerce\Data\Mutation
6
 * @since TBD
7
 */
8

9
namespace WPGraphQL\WooCommerce\Data\Mutation;
10

11
use GraphQL\Error\UserError;
12

13
/**
14
 * Class - Product_Mutation
15
 */
16
class Product_Mutation {
17
        /**
18
         * Save product shipping data
19
         *
20
         * @param \WC_Product $product  Product object.
21
         * @param array       $input    Mutation input.
22
         *
23
         * @return \WC_Product
24
         */
25
        public static function save_product_shipping_data( $product, $input ) {
26
                // Virtual.
27
                if ( isset( $input['virtual'] ) && true === $input['virtual'] ) {
12✔
NEW
28
                        $product->set_weight( '' );
×
NEW
29
                        $product->set_height( '' );
×
NEW
30
                        $product->set_length( '' );
×
NEW
31
                        $product->set_width( '' );
×
32
                } else {
33
                        if ( isset( $input['weight'] ) ) {
12✔
NEW
34
                                $product->set_weight( $input['weight'] );
×
35
                        }
36

37
                        if ( isset( $input['dimensions']['height'] ) ) {
12✔
38
                                $product->set_height( $input['dimensions']['height'] );
1✔
39
                        }
40

41
                        if ( isset( $input['dimensions']['width'] ) ) {
12✔
42
                                $product->set_width( $input['dimensions']['width'] );
1✔
43
                        }
44

45
                        if ( isset( $input['dimensions']['length'] ) ) {
12✔
46
                                $product->set_length( $input['dimensions']['length'] );
1✔
47
                        }
48
                }
49

50
                if ( isset( $input['shippingClass'] ) ) {
12✔
51
                        /**
52
                         * Shipping class.
53
                         *
54
                         * @var string $shippingClass
55
                         */
NEW
56
                        $shippingClass = wc_clean( $input['shippingClass'] );
×
57
                        /**
58
                         * WC product data store instance.
59
                         *
60
                         * @var \WC_Product_Data_Store_Interface $data_store
61
                         */
NEW
62
                        $data_store        = $product->get_data_store();
×
NEW
63
                        $shipping_class_id = $data_store->get_shipping_class_id_by_slug( $shippingClass );
×
NEW
64
                        if ( $shipping_class_id ) {
×
NEW
65
                                $product->set_shipping_class_id( $shipping_class_id );
×
66
                        } else {
NEW
67
                                graphql_debug(
×
68
                                        /* translators: %s: Shipping class */
NEW
69
                                        sprintf( __( 'Invalid shipping class: %s', 'wp-graphql-woocommerce' ), $shippingClass )
×
NEW
70
                                );
×
71
                        }
72
                }
73

74
                return $product;
12✔
75
        }
76

77
        /**
78
         * Prepare product attribute
79
         *
80
         * @param array $attribute  Product attribute data.
81
         *
82
         * @return \WC_Product_Attribute|null
83
         */
84
        public static function prepare_attribute( $attribute ) {
85
                /**
86
                 * Attribute ID.
87
                 *
88
                 * @var int $attribute_id
89
                 */
90
                $attribute_id = 0;
2✔
91
                /**
92
                 * Attribute name.
93
                 *
94
                 * @var string $attribute_name
95
                 */
96
                $attribute_name = '';
2✔
97

98
                if ( ! empty( $attribute['id'] ) ) {
2✔
99
                        $attribute_id   = absint( $attribute['id'] );
2✔
100
                        $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id );
2✔
101
                } elseif ( ! empty( $attribute['name'] ) ) {
1✔
102
                        /**
103
                         * @var string $attribute_name
104
                         */
105
                        $attribute_name = wc_clean( $attribute['name'] );
1✔
106
                }
107

108
                if ( ! $attribute_id && ! $attribute_name ) {
2✔
NEW
109
                        return null;
×
110
                }
111

112
                if ( $attribute_id ) {
2✔
113
                        if ( isset( $attribute['options'] ) ) {
2✔
114
                                $options = $attribute['options'];
2✔
115

116
                                if ( ! is_array( $options ) ) {
2✔
NEW
117
                                        $options = explode( WC_DELIMITER, $options );
×
118
                                }
119

120
                                $values = array_map( 'wc_sanitize_term_text_based', $options );
2✔
121
                                /** @var callable(string): bool */
122
                                $callback = 'strlen';
2✔
123
                                $values   = array_filter( $values, $callback );
2✔
124
                        } else {
NEW
125
                                $values = [];
×
126
                        }
127

128
                        if ( ! empty( $values ) ) {
2✔
129
                                $attribute_object = new \WC_Product_Attribute();
2✔
130
                                $attribute_object->set_id( $attribute_id );
2✔
131
                                $attribute_object->set_name( $attribute_name );
2✔
132
                                $attribute_object->set_options( $values );
2✔
133
                                $attribute_object->set_position( isset( $attribute['position'] ) ? absint( $attribute['position'] ) : 0 );
2✔
134
                                $attribute_object->set_visible( isset( $attribute['visible'] ) ? $attribute['visible'] : false );
2✔
135
                                $attribute_object->set_variation( isset( $attribute['variation'] ) ? $attribute['variation'] : false );
2✔
136

137
                                return $attribute_object;
2✔
138
                        }
139
                } elseif ( isset( $attribute['options'] ) ) {
1✔
140
                        if ( is_array( $attribute['options'] ) ) {
1✔
141
                                $values = $attribute['options'];
1✔
142
                        } else {
NEW
143
                                $values = explode( WC_DELIMITER, $attribute['options'] );
×
144
                        }
145

146
                        $attribute_object = new \WC_Product_Attribute();
1✔
147
                        $attribute_object->set_name( $attribute_name );
1✔
148
                        $attribute_object->set_options( $values );
1✔
149
                        $attribute_object->set_position( isset( $attribute['position'] ) ? absint( $attribute['position'] ) : 0 );
1✔
150
                        $attribute_object->set_visible( isset( $attribute['visible'] ) ? $attribute['visible'] : false );
1✔
151
                        $attribute_object->set_variation( isset( $attribute['variation'] ) ? $attribute['variation'] : false );
1✔
152

153
                        return $attribute_object;
1✔
154
                }
155

NEW
156
                return null;
×
157
        }
158

159
        /**
160
         * Save product attributes
161
         *
162
         * @param \WC_Product $product   Product instance.
163
         * @param array       $term_ids  Terms IDs.
164
         * @param string      $taxonomy  Taxonomy name.
165
         *
166
         * @return \WC_Product
167
         */
168
        public static function save_taxonomy_terms( $product, $term_ids, $taxonomy = 'cat' ) {
169
                if ( 'cat' === $taxonomy ) {
1✔
170
                        $product->set_category_ids( $term_ids );
1✔
171
                } elseif ( 'tag' === $taxonomy ) {
1✔
172
                        $product->set_tag_ids( $term_ids );
1✔
173
                }
174

175
                return $product;
1✔
176
        }
177

178
        /**
179
         * Save product downloadable files
180
         *
181
         * @param \WC_Product|\WC_Product_Variation $product    Product instance.
182
         * @param array                             $downloads  Downloads data.
183
         *
184
         * @return \WC_Product|\WC_Product_Variation
185
         */
186
        public static function save_downloadable_files( $product, $downloads ) {
NEW
187
                $files = [];
×
NEW
188
                foreach ( $downloads as $key => $file ) {
×
NEW
189
                        if ( empty( $file['file'] ) ) {
×
NEW
190
                                continue;
×
191
                        }
192

NEW
193
                        $download = new \WC_Product_Download();
×
NEW
194
                        $download->set_id( ! empty( $file['id'] ) ? $file['id'] : wp_generate_uuid4() );
×
NEW
195
                        $download->set_name( $file['name'] ? $file['name'] : wc_get_filename_from_url( $file['file'] ) );
×
NEW
196
                        $download->set_file( apply_filters( 'woocommerce_file_download_path', $file['file'], $product, $key ) ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
×
NEW
197
                        $files[] = $download;
×
198
                }
199

NEW
200
                $product->set_downloads( $files );
×
201

NEW
202
                return $product;
×
203
        }
204

205
        /**
206
         * Save variable product default attributes
207
         *
208
         * @param \WC_Product $product  Product object.
209
         * @param array       $input    Mutation input.
210
         *
211
         * @return \WC_Product
212
         */
213
        public static function save_default_attributes( $product, $input ) {
214
                if ( ! empty( $input['defaultAttributes'] ) ) {
1✔
215
                        $attributes         = $product->get_attributes();
1✔
216
                        $default_attributes = [];
1✔
217

218
                        foreach ( $input['defaultAttributes'] as $attribute ) {
1✔
219
                                /**
220
                                 * Attribute ID.
221
                                 *
222
                                 * @var int $attribute_id
223
                                 */
224
                                $attribute_id = 0;
1✔
225
                                /**
226
                                 * Attribute name.
227
                                 *
228
                                 * @var string $attribute_name
229
                                 */
230
                                $attribute_name = '';
1✔
231

232
                                if ( ! empty( $attribute['id'] ) ) {
1✔
233
                                        $attribute_id   = absint( $attribute['id'] );
1✔
234
                                        $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id );
1✔
235
                                } elseif ( ! empty( $attribute['attributeName'] ) ) {
1✔
236
                                        $attribute_name = sanitize_title( $attribute['attributeName'] );
1✔
237
                                }
238

239
                                if ( ! $attribute_id && ! $attribute_name ) {
1✔
NEW
240
                                        continue;
×
241
                                }
242

243
                                if ( isset( $attributes[ $attribute_name ] ) ) {
1✔
244
                                        $_attribute = $attributes[ $attribute_name ];
1✔
245

246
                                        if ( $_attribute['is_variation'] ) {
1✔
247
                                                /**
248
                                                 * Attribute value.
249
                                                 *
250
                                                 * @var string $value
251
                                                 */
252
                                                $value = isset( $attribute['attributeValue'] ) ? wc_clean( stripslashes( $attribute['attributeValue'] ) ) : '';
1✔
253

254
                                                if ( ! empty( $_attribute['is_taxonomy'] ) ) {
1✔
255
                                                        $term = get_term_by( 'name', $value, $attribute_name );
1✔
256

257
                                                        if ( ! empty( $term ) ) {
1✔
258
                                                                $value = $term->slug;
1✔
259
                                                        } else {
NEW
260
                                                                $value = sanitize_title( $value );
×
261
                                                        }
262
                                                }
263

264
                                                if ( $value ) {
1✔
265
                                                        $default_attributes[ $attribute_name ] = $value;
1✔
266
                                                }
267
                                        }
268
                                }
269
                        }
270

271
                        $product->set_default_attributes( $default_attributes );
1✔
272
                }
273

274
                return $product;
1✔
275
        }
276

277
        /**
278
         * Set product images
279
         *
280
         * @param \WC_Product $product Product instance.
281
         * @param array       $images  Images data.
282
         *
283
         * @throws \GraphQL\Error\UserError If image upload fails | Invalid image ID.
284
         *
285
         * @return \WC_Product
286
         */
287
        public static function set_product_images( $product, $images ) {
288
                $images = is_array( $images ) ? array_filter( $images ) : [];
1✔
289

290
                if ( ! empty( $images ) ) {
1✔
291
                        $gallery_positions = [];
1✔
292

293
                        foreach ( $images as $index => $image ) {
1✔
294
                                $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0;
1✔
295

296
                                if ( 0 === $attachment_id && isset( $image['src'] ) ) {
1✔
NEW
297
                                        $upload = \wc_rest_upload_image_from_url( $image['src'] );
×
298

NEW
299
                                        if ( is_wp_error( $upload ) ) {
×
NEW
300
                                                if ( ! apply_filters( 'woocommerce_rest_suppress_image_upload_error', false, $upload, $product->get_id(), $images ) ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
×
NEW
301
                                                        throw new UserError( $upload->get_error_message() );
×
302
                                                } else {
NEW
303
                                                        continue;
×
304
                                                }
305
                                        }
306

NEW
307
                                        $attachment_id = \wc_rest_set_uploaded_image_as_attachment( $upload, $product->get_id() );
×
308
                                }
309

310
                                if ( ! wp_attachment_is_image( $attachment_id ) ) {
1✔
NEW
311
                                        throw new UserError(
×
312
                                                /* translators: %s: Attachment ID */
NEW
313
                                                sprintf( __( '#%s is an invalid image ID.', 'wp-graphql-woocommerce' ), $attachment_id )
×
NEW
314
                                        );
×
315
                                }
316

317
                                $gallery_positions[ $attachment_id ] = absint( isset( $image['position'] ) ? $image['position'] : $index );
1✔
318

319
                                // Set the image alt if present.
320
                                if ( ! empty( $image['altText'] ) ) {
1✔
NEW
321
                                        update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) );
×
322
                                }
323

324
                                // Set the image name if present.
325
                                if ( ! empty( $image['name'] ) ) {
1✔
NEW
326
                                        wp_update_post(
×
NEW
327
                                                [
×
NEW
328
                                                        'ID'         => $attachment_id,
×
NEW
329
                                                        'post_title' => $image['name'],
×
NEW
330
                                                ]
×
NEW
331
                                        );
×
332
                                }
333

334
                                // Set the image source if present, for future reference.
335
                                if ( ! empty( $image['src'] ) ) {
1✔
NEW
336
                                        update_post_meta( $attachment_id, '_wc_attachment_source', esc_url_raw( $image['src'] ) );
×
337
                                }
338
                        }
339

340
                        // Sort images and get IDs in correct order.
341
                        asort( $gallery_positions );
1✔
342

343
                        // Get gallery in correct order.
344
                        $gallery = array_keys( $gallery_positions );
1✔
345

346
                        /**
347
                         * Featured image ID in position 0.
348
                         *
349
                         * @var int $image_id
350
                         */
351
                        $image_id = array_shift( $gallery );
1✔
352

353
                        // Set images.
354
                        $product->set_image_id( $image_id );
1✔
355
                        $product->set_gallery_image_ids( $gallery );
1✔
356
                } else {
NEW
357
                        $product->set_image_id( '' );
×
NEW
358
                        $product->set_gallery_image_ids( [] );
×
359
                }
360

361
                return $product;
1✔
362
        }
363

364
        /**
365
         * Retrieve product attribute
366
         *
367
         * @param int $id  Attribute ID.
368
         *
369
         * @throws \GraphQL\Error\UserError If attribute ID is invalid.
370
         *
371
         * @return object{'attribute_id': int}
372
         */
373
        public static function get_attribute( $id ) {
374
                global $wpdb;
3✔
375

376
                $attribute = $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
3✔
377
                        $wpdb->prepare(
3✔
378
                                "
3✔
379
                        SELECT *
380
                        FROM {$wpdb->prefix}woocommerce_attribute_taxonomies
3✔
381
                        WHERE attribute_id = %d
382
                 ",
3✔
383
                                $id
3✔
384
                        )
3✔
385
                );
3✔
386

387
                if ( is_wp_error( $attribute ) || is_null( $attribute ) ) {
3✔
NEW
388
                        throw new UserError( __( 'Invalid attribute ID.', 'wp-graphql-woocommerce' ) );
×
389
                }
390

391
                return $attribute;
3✔
392
        }
393
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc