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

wp-graphql / wp-graphql-woocommerce / 28450327807

30 Jun 2026 02:03PM UTC coverage: 91.801% (+0.001%) from 91.8%
28450327807

push

github

web-flow
fix: resolve product variation type when the node is a base Post model (#1020)

* fix: resolve product variation type when the node is a base Post model

A cart item's variation node can be loaded through the generic post loader (e.g. under Polylang, which doesn't manage the product_variation post-type), arriving as a base \WPGraphQL\Model\Post that has no get_type(). resolve_product_variation_type() then fatals. Fall back to resolving the variation's product type from its ID via wc_get_product() when get_type() isn't callable.

* chore: remove stray graphql_debug() from ProductWithPricing price resolver

* test: disable Customer Note email so order-note tests aren't polluted by the WC email-sent note

* test: cover product-variation type fallback when the node loads as a base Post model

6 of 7 new or added lines in 1 file covered. (85.71%)

18531 of 20186 relevant lines covered (91.8%)

153.15 hits per line

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

78.79
/includes/class-post-types.php
1
<?php
2
/**
3
 * Registers WooCommerce post types and related schema filters.
4
 *
5
 * @package \WPGraphQL\WooCommerce
6
 * @since   0.0.1
7
 */
8

9
namespace WPGraphQL\WooCommerce;
10

11
use GraphQL\Error\UserError;
12
use WPGraphQL\WooCommerce\Data\Factory;
13
use WPGraphQL\WooCommerce\Data\Loader\WC_CPT_Loader;
14
use WPGraphQL\WooCommerce\Data\Loader\WC_Cart_Item_Loader;
15
use WPGraphQL\WooCommerce\Data\Loader\WC_Customer_Loader;
16
use WPGraphQL\WooCommerce\Data\Loader\WC_Downloadable_Item_Loader;
17
use WPGraphQL\WooCommerce\Data\Loader\WC_Order_Item_Loader;
18
use WPGraphQL\WooCommerce\Data\Loader\WC_Shipping_Method_Loader;
19
use WPGraphQL\WooCommerce\Data\Loader\WC_Shipping_Zone_Loader;
20
use WPGraphQL\WooCommerce\Data\Loader\WC_Tax_Class_Loader;
21
use WPGraphQL\WooCommerce\Data\Loader\WC_Tax_Rate_Loader;
22
use WPGraphQL\WooCommerce\WP_GraphQL_WooCommerce as WooGraphQL;
23

24
/**
25
 * Class Post_Types
26
 */
27
class Post_Types {
28
        /**
29
         * Register filters
30
         *
31
         * @return void
32
         */
33
        public static function init() {
34
                // Registers WooCommerce CPTs.
35
                add_filter( 'register_post_type_args', [ self::class, 'register_post_types' ], 10, 2 );
33✔
36
                add_filter( 'graphql_post_entities_allowed_post_types', [ self::class, 'skip_type_registry' ], 10 );
33✔
37

38
                // Add data-loaders to AppContext.
39
                add_filter( 'graphql_data_loader_classes', [ self::class, 'graphql_data_loader_classes' ], 10 );
33✔
40

41
                // Add node resolvers.
42
                add_filter(
33✔
43
                        'graphql_resolve_node',
33✔
44
                        [ '\WPGraphQL\WooCommerce\Data\Factory', 'resolve_node' ],
33✔
45
                        10,
33✔
46
                        4
33✔
47
                );
33✔
48
                add_filter(
33✔
49
                        'graphql_resolve_node_type',
33✔
50
                        [ '\WPGraphQL\WooCommerce\Data\Factory', 'resolve_node_type' ],
33✔
51
                        10,
33✔
52
                        2
33✔
53
                );
33✔
54

55
                // Filter Unions.
56
                add_filter(
33✔
57
                        'graphql_wp_union_type_config',
33✔
58
                        [ self::class, 'inject_union_types' ],
33✔
59
                        10,
33✔
60
                        2
33✔
61
                );
33✔
62

63
                add_filter(
33✔
64
                        'graphql_union_resolve_type',
33✔
65
                        [ self::class, 'inject_type_resolver' ],
33✔
66
                        10,
33✔
67
                        2
33✔
68
                );
33✔
69

70
                add_filter(
33✔
71
                        'graphql_interface_resolve_type',
33✔
72
                        [ self::class, 'inject_type_resolver' ],
33✔
73
                        10,
33✔
74
                        2
33✔
75
                );
33✔
76

77
                add_filter(
33✔
78
                        'graphql_dataloader_pre_get_model',
33✔
79
                        [ '\WPGraphQL\WooCommerce\Data\Loader\WC_CPT_Loader', 'inject_post_loader_models' ],
33✔
80
                        10,
33✔
81
                        3
33✔
82
                );
33✔
83

84
                // Filter to allow order notes to be visible in GraphQL queries.
85
                add_filter(
33✔
86
                        'graphql_data_is_private',
33✔
87
                        [ self::class, 'make_order_notes_visible' ],
33✔
88
                        10,
33✔
89
                        3
33✔
90
                );
33✔
91

92
                // Filter to set order notes visibility to public for authorized users.
93
                add_filter(
33✔
94
                        'graphql_object_visibility',
33✔
95
                        [ self::class, 'set_order_notes_visibility' ],
33✔
96
                        10,
33✔
97
                        5
33✔
98
                );
33✔
99

100
                add_filter(
33✔
101
                        'graphql_dataloader_get_model',
33✔
102
                        [ '\WPGraphQL\WooCommerce\Data\Loader\WC_Customer_Loader', 'inject_user_loader_models' ],
33✔
103
                        10,
33✔
104
                        3
33✔
105
                );
33✔
106

107
                add_filter(
33✔
108
                        'graphql_map_input_fields_to_wp_query',
33✔
109
                        [ '\WPGraphQL\WooCommerce\Connection\Coupons', 'map_input_fields_to_wp_query' ],
33✔
110
                        10,
33✔
111
                        7
33✔
112
                );
33✔
113

114
                add_filter(
33✔
115
                        'graphql_map_input_fields_to_wp_user_query',
33✔
116
                        [ '\WPGraphQL\WooCommerce\Connection\Customers', 'map_input_fields_to_wp_query' ],
33✔
117
                        10,
33✔
118
                        6
33✔
119
                );
33✔
120

121
                add_filter(
33✔
122
                        'graphql_connection',
33✔
123
                        [ '\WPGraphQL\WooCommerce\Connection\Customers', 'upgrade_models' ],
33✔
124
                        10,
33✔
125
                        2
33✔
126
                );
33✔
127

128
                add_filter(
33✔
129
                        'graphql_wp_connection_type_config',
33✔
130
                        [ '\WPGraphQL\WooCommerce\Connection\Products', 'set_connection_config' ]
33✔
131
                );
33✔
132
        }
133

134
        /**
135
         * Registers WooCommerce post-types to be used in GraphQL schema
136
         *
137
         * @param array  $args      - allowed post-types.
138
         * @param string $post_type - name of taxonomy being checked.
139
         *
140
         * @return array
141
         */
142
        public static function register_post_types( $args, $post_type ) {
143
                if ( 'product' === $post_type ) {
33✔
144
                        $args['show_in_graphql']                  = true;
33✔
145
                        $args['model']                            = \WPGraphQL\WooCommerce\Model\Product::class;
33✔
146
                        $args['graphql_single_name']              = 'Product';
33✔
147
                        $args['graphql_plural_name']              = 'Products';
33✔
148
                        $args['graphql_kind']                     = 'interface';
33✔
149
                        $args['graphql_interfaces']               = [ 'ContentNode', 'ProductUnion' ];
33✔
150
                        $args['graphql_register_root_field']      = false;
33✔
151
                        $args['graphql_register_root_connection'] = false;
33✔
152
                        $args['graphql_resolve_type']             = [ self::class, 'resolve_product_type' ];
33✔
153
                        $args['graphql_exclude_mutations']        = [ 'create', 'delete', 'update' ];
33✔
154
                }//end if
155
                if ( 'product_variation' === $post_type ) {
33✔
156
                        $args['show_in_graphql']                  = true;
33✔
157
                        $args['model']                            = \WPGraphQL\WooCommerce\Model\Product_Variation::class;
33✔
158
                        $args['graphql_single_name']              = 'ProductVariation';
33✔
159
                        $args['graphql_plural_name']              = 'ProductVariations';
33✔
160
                        $args['publicly_queryable']               = true;
33✔
161
                        $args['graphql_kind']                     = 'interface';
33✔
162
                        $args['graphql_interfaces']               = [
33✔
163
                                'Node',
33✔
164
                                'NodeWithFeaturedImage',
33✔
165
                                'ContentNode',
33✔
166
                                'ProductUnion',
33✔
167
                                'UniformResourceIdentifiable',
33✔
168
                                'ProductWithPricing',
33✔
169
                                'ProductWithDimensions',
33✔
170
                                'InventoriedProduct',
33✔
171
                                'DownloadableProduct',
33✔
172
                        ];
33✔
173
                        $args['graphql_register_root_field']      = false;
33✔
174
                        $args['graphql_register_root_connection'] = false;
33✔
175
                        $args['graphql_resolve_type']             = [ self::class, 'resolve_product_variation_type' ];
33✔
176
                        $args['graphql_exclude_mutations']        = [ 'create', 'delete', 'update' ];
33✔
177
                }
178
                if ( 'shop_coupon' === $post_type ) {
33✔
179
                        $args['show_in_graphql']            = true;
33✔
180
                        $args['graphql_single_name']        = 'Coupon';
33✔
181
                        $args['graphql_plural_name']        = 'Coupons';
33✔
182
                        $args['publicly_queryable']         = true;
33✔
183
                        $args['skip_graphql_type_registry'] = true;
33✔
184
                }
185
                if ( 'shop_order' === $post_type ) {
33✔
186
                        $args['show_in_graphql']            = true;
33✔
187
                        $args['graphql_single_name']        = 'Order';
33✔
188
                        $args['graphql_plural_name']        = 'Orders';
33✔
189
                        $args['skip_graphql_type_registry'] = true;
33✔
190
                }
191
                if ( 'shop_order_refund' === $post_type ) {
33✔
192
                        $args['show_in_graphql']            = true;
33✔
193
                        $args['graphql_single_name']        = 'Refund';
33✔
194
                        $args['graphql_plural_name']        = 'Refunds';
33✔
195
                        $args['skip_graphql_type_registry'] = true;
33✔
196
                }
197

198
                return $args;
33✔
199
        }
200

201
        /**
202
         * Filters "allowed_post_types" and removed Woocommerce CPTs.
203
         *
204
         * @param array $post_types  Post types registered in GraphQL schema.
205
         *
206
         * @return array
207
         */
208
        public static function skip_type_registry( $post_types ) {
209
                return array_diff(
299✔
210
                        $post_types,
299✔
211
                        get_post_types(
299✔
212
                                [
299✔
213
                                        'show_in_graphql'            => true,
299✔
214
                                        'skip_graphql_type_registry' => true,
299✔
215
                                ]
299✔
216
                        )
299✔
217
                );
299✔
218
        }
219

220
        /**
221
         * Registers data-loaders to be used when resolving WooCommerce-related GraphQL types
222
         *
223
         * @param array $loaders  Assigned loaders.
224
         *
225
         * @return array
226
         */
227
        public static function graphql_data_loader_classes( $loaders ) {
228
                // WooCommerce customer loader.
229
                $loaders['wc_customer'] = WC_Customer_Loader::class;
310✔
230

231
                // WooCommerce CPT loader.
232
                $loaders['wc_post'] = WC_CPT_Loader::class;
310✔
233

234
                // WooCommerce DB loaders.
235
                $loaders['cart_item']         = WC_Cart_Item_Loader::class;
310✔
236
                $loaders['downloadable_item'] = WC_Downloadable_Item_Loader::class;
310✔
237
                $loaders['tax_class']         = WC_Tax_Class_Loader::class;
310✔
238
                $loaders['tax_rate']          = WC_Tax_Rate_Loader::class;
310✔
239
                $loaders['order_item']        = WC_Order_Item_Loader::class;
310✔
240
                $loaders['shipping_method']   = WC_Shipping_Method_Loader::class;
310✔
241
                $loaders['shipping_zone']     = WC_Shipping_Zone_Loader::class;
310✔
242
                return $loaders;
310✔
243
        }
244

245
        /**
246
         * Inject Union types that resolve to Product with Product types
247
         *
248
         * @param array                       $config    WPUnion config.
249
         * @param \WPGraphQL\Type\WPUnionType $wp_union  WPUnion object.
250
         *
251
         * @return array
252
         */
253
        public static function inject_union_types( $config, $wp_union ) {
254
                $refresh_callback = false;
140✔
255
                if ( in_array( 'Product', $config['typeNames'], true ) ) {
140✔
256
                        // Strip 'Product' from config and child product types.
257
                        $config['typeNames'] = array_merge(
×
258
                                array_filter(
×
259
                                        $config['typeNames'],
×
260
                                        static function ( $type ) {
×
261
                                                return 'Product' !== $type;
×
262
                                        }
×
263
                                ),
×
264
                                array_values( WooGraphQL::get_enabled_product_types() ),
×
265
                                [ WooGraphQL::get_supported_product_type() ]
×
266
                        );
×
267
                        $refresh_callback    = true;
×
268
                }
269

270
                // Update 'types' callback.
271
                if ( $refresh_callback ) {
140✔
272
                        $config['types'] = static function () use ( $config, $wp_union ) {
×
273
                                $prepared_types = [];
×
274
                                foreach ( $config['typeNames'] as $type_name ) {
×
275
                                        $prepared_types[] = $wp_union->type_registry->get_type( $type_name );
×
276
                                }
277
                                return $prepared_types;
×
278
                        };
×
279
                }
280

281
                return $config;
140✔
282
        }
283

284
        /**
285
         * Inject Union type resolver that resolve to Product with Product types
286
         *
287
         * @param \WPGraphQL\Type\WPObjectType $type      Type be resolve to.
288
         * @param mixed                        $value     Object for which the type is being resolve config.
289
         * @param \WPGraphQL\Type\WPUnionType  $wp_union  WPUnion object.
290
         *
291
         * @return \WPGraphQL\Type\WPObjectType
292
         */
293
        public static function inject_union_type_resolver( $type, $value, $wp_union ) {
294
                switch ( get_class( $value ) ) {
×
295
                        case 'WPGraphQL\WooCommerce\Model\Product':
×
296
                        case 'WPGraphQL\WooCommerce\Model\Coupon':
×
297
                        case 'WPGraphQL\WooCommerce\Model\Order':
×
298
                                $new_type = Factory::resolve_node_type( $type, $value );
×
299
                                if ( $new_type ) {
×
300
                                        $type = $wp_union->type_registry->get_type( $new_type );
×
301
                                }
302
                                break;
×
303
                }
304

305
                return $type;
×
306
        }
307

308
        /**
309
         * Inject Union type resolver that resolve to Product with Product types
310
         *
311
         * @param \WPGraphQL\Type\WPObjectType|null $type   Type be resolve to.
312
         * @param mixed                             $value  Object for which the type is being resolve config.
313
         *
314
         * @throws \GraphQL\Error\UserError Invalid product type received.
315
         *
316
         * @return \WPGraphQL\Type\WPObjectType|null
317
         */
318
        public static function inject_type_resolver( $type, $value ) {
319

320
                $type_registry = \WPGraphQL::get_type_registry();
160✔
321
                switch ( $type ) {
322
                        case 'Coupon':
160✔
323
                        case 'Order':
160✔
324
                                $new_type = Factory::resolve_node_type( $type, $value );
×
325
                                if ( $new_type ) {
×
326
                                        $type = $type_registry->get_type( $new_type );
×
327
                                }
328
                                break;
×
329
                        case 'ProductVariation':
160✔
330
                                $type = self::resolve_product_variation_type( $value );
×
331
                                break;
×
332
                        case 'Product':
160✔
333
                                $type = self::resolve_product_type( $value );
×
334
                }//end switch
335

336
                return $type;
160✔
337
        }
338

339
        /**
340
         * Resolves GraphQL type for provided product model.
341
         *
342
         * @param \WPGraphQL\WooCommerce\Model\Product|\WPGraphQL\WooCommerce\Model\Product_Variation $value  Product model.
343
         *
344
         * @throws \GraphQL\Error\UserError Invalid product type requested.
345
         *
346
         * @return mixed
347
         */
348
        public static function resolve_product_type( $value ) {
349
                $type_registry  = \WPGraphQL::get_type_registry();
130✔
350
                $possible_types = WooGraphQL::get_enabled_product_types();
130✔
351

352
                if ( $value instanceof \WPGraphQL\Model\Post && ( 'product' === $value->post_type || 'product_variation' === $value->post_type ) ) {
130✔
353
                        $product_model = \WPGraphQL::get_app_context()
130✔
354
                                ->get_loader( 'wc_post' )
130✔
355
                                ->load( $value->ID );
130✔
356
                } elseif ( $value instanceof \WPGraphQL\Model\Post && ( 'product' !== $value->post_type && 'product_variation' !== $value->post_type ) ) {
×
357
                        throw new UserError(
×
358
                                sprintf(
×
359
                                        /* translators: %s: Post type slug */
360
                                        __( 'The "%s" post type is not a valid product type.', 'wp-graphql-woocommerce' ),
×
361
                                        $value->post_type
×
362
                                )
×
363
                        );
×
364
                } else {
365
                        $product_model = $value;
×
366
                }
367

368
                $product_type = $product_model->get_type();
130✔
369
                if ( isset( $possible_types[ $product_type ] ) ) {
130✔
370
                        return $type_registry->get_type( $possible_types[ $product_type ] );
128✔
371
                } elseif ( $product_model instanceof \WPGraphQL\WooCommerce\Model\Product_Variation ) {
3✔
372
                        return self::resolve_product_variation_type( $product_model );
1✔
373
                } elseif ( 'on' === woographql_setting( 'enable_unsupported_product_type', 'off' ) ) {
2✔
374
                        $unsupported_type = WooGraphQL::get_supported_product_type();
1✔
375
                        return $type_registry->get_type( $unsupported_type );
1✔
376
                }
377

378
                throw new UserError(
1✔
379
                        sprintf(
1✔
380
                        /* translators: %s: Product type */
381
                                __( 'The "%s" product type is not supported by the core WPGraphQL for WooCommerce (WooGraphQL) schema.', 'wp-graphql-woocommerce' ),
1✔
382
                                $product_model->type
1✔
383
                        )
1✔
384
                );
1✔
385
        }
386

387
        /**
388
         * Resolves GraphQL type for provided product variation model.
389
         *
390
         * @param \WPGraphQL\WooCommerce\Model\Product_Variation $value  Product model.
391
         *
392
         * @throws \GraphQL\Error\UserError Invalid product type requested.
393
         *
394
         * @return mixed
395
         */
396
        public static function resolve_product_variation_type( $value ) {
397
                $type_registry  = \WPGraphQL::get_type_registry();
30✔
398
                $possible_types = WooGraphQL::get_enabled_product_variation_types();
30✔
399

400
                // Normally $value is a Product_Variation model. In some setups the
401
                // variation node is loaded through the generic post loader (e.g. a cart
402
                // item's variation under Polylang, which doesn't manage the
403
                // product_variation post-type) and arrives as a base
404
                // \WPGraphQL\Model\Post with no get_type(). Fall back to resolving the
405
                // variation's product type from its ID.
406
                if ( is_callable( [ $value, 'get_type' ] ) ) {
30✔
407
                        $product_type = $value->get_type();
29✔
408
                } else {
409
                        $variation_id = $value->databaseId ?? ( $value->ID ?? 0 );
1✔
410
                        $product      = $variation_id ? wc_get_product( $variation_id ) : false;
1✔
411
                        $product_type = $product ? $product->get_type() : null;
1✔
412
                }
413

414
                if ( $product_type && isset( $possible_types[ $product_type ] ) ) {
30✔
415
                        return $type_registry->get_type( $possible_types[ $product_type ] );
30✔
416
                }
417

418
                throw new UserError(
×
419
                        sprintf(
×
420
                        /* translators: %s: Product type */
421
                                __( 'The "%s" product variation type is not supported by the core WPGraphQL for WooCommerce (WooGraphQL) schema.', 'wp-graphql-woocommerce' ),
×
NEW
422
                                $product_type ?? ''
×
423
                        )
×
424
                );
×
425
        }
426

427
        /**
428
         * Filter to make order notes visible in GraphQL queries for authorized users.
429
         *
430
         * @param bool   $is_private Whether the data is private.
431
         * @param string $model_name The name of the model being checked.
432
         * @param mixed  $data       The data being checked.
433
         *
434
         * @return bool
435
         */
436
        public static function make_order_notes_visible( $is_private, $model_name, $data ) {
437
                // Only apply to Comment models.
438
                if ( 'CommentObject' !== $model_name ) {
246✔
439
                        return $is_private;
245✔
440
                }
441

442
                // Check if this is an order note.
443
                if ( $data instanceof \WP_Comment && 'order_note' === $data->comment_type ) {
8✔
444
                        // Get the parent order.
445
                        $order_id = absint( $data->comment_post_ID );
3✔
446
                        $order    = wc_get_order( $order_id );
3✔
447

448
                        if ( ! $order ) {
3✔
449
                                return true; // Keep it private if order not found.
×
450
                        }
451

452
                        // Allow shop managers and admins to see all order notes.
453
                        if ( current_user_can( 'edit_shop_orders' ) ) {
3✔
454
                                return false; // Not private.
3✔
455
                        }
456

457
                        // Allow customers to see customer notes on their own orders.
458
                        $comment_id       = absint( $data->comment_ID );
1✔
459
                        $is_customer_note = get_comment_meta( $comment_id, 'is_customer_note', true );
1✔
460
                        if ( $is_customer_note && ! is_bool( $order ) && is_a( $order, \WC_Order::class ) ) {
1✔
461
                                if ( get_current_user_id() === $order->get_customer_id() ) {
1✔
462
                                        return false; // Not private.
1✔
463
                                }
464
                        } elseif ( $is_customer_note && ! is_bool( $order ) && is_a( $order, \WC_Order_Refund::class ) ) {
×
465
                                /** @var \WC_Order|false $parent */
466
                                $parent = wc_get_order( $order->get_parent_id() );
×
467
                                if ( $parent && get_current_user_id() === $parent->get_customer_id() ) {
×
468
                                        return false; // Not private.
×
469
                                }
470
                        }
471

472
                        // Otherwise keep it private.
473
                        return true;
×
474
                }
475

476
                return $is_private;
5✔
477
        }
478

479
        /**
480
         * Filter to set order notes visibility to public for authorized users.
481
         *
482
         * @param string   $visibility   The visibility of the object.
483
         * @param string   $model_name   The name of the model being checked.
484
         * @param mixed    $data         The data being checked.
485
         * @param int|null $owner        The owner of the object.
486
         * @param \WP_User $current_user The current user.
487
         *
488
         * @return string
489
         */
490
        public static function set_order_notes_visibility( $visibility, $model_name, $data, $owner, $current_user ) {
491
                // Only apply to Comment models.
492
                if ( 'CommentObject' !== $model_name ) {
246✔
493
                        return $visibility;
245✔
494
                }
495

496
                // Check if this is an order note and if user owns the order.
497
                if ( $data instanceof \WP_Comment && 'order_note' === $data->comment_type ) {
8✔
498
                        $order = wc_get_order( $data->comment_post_ID );
3✔
499

500
                        // If user is the order owner, make it public.
501
                        if ( $order && ! is_bool( $order ) && is_a( $order, \WC_Order::class ) ) {
3✔
502
                                return get_current_user_id() === $order->get_customer_id() ? 'public' : $visibility;
3✔
503
                        } elseif ( $order && ! is_bool( $order ) && is_a( $order, \WC_Order_Refund::class ) ) {
×
504
                                /** @var \WC_Order|false $parent */
505
                                $parent = wc_get_order( $order->get_parent_id() );
×
506
                                return $parent && get_current_user_id() === $parent->get_customer_id() ? 'public' : $visibility;
×
507
                        }
508
                }
509

510
                return $visibility;
5✔
511
        }
512
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc