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

wp-graphql / wp-graphql-woocommerce / 23675172456

28 Mar 2026 02:10AM UTC coverage: 70.983% (-18.4%) from 89.424%
23675172456

Pull #1003

github

web-flow
Merge 05339093d into 6fb7b226f
Pull Request #1003: devops: WC email template tests, COT cursor HPOS fix, checkout account auth

71 of 81 new or added lines in 5 files covered. (87.65%)

3346 existing lines in 124 files now uncovered.

12576 of 17717 relevant lines covered (70.98%)

55.38 hits per line

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

18.22
/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.
UNCOV
35
                add_filter( 'register_post_type_args', [ self::class, 'register_post_types' ], 10, 2 );
×
UNCOV
36
                add_filter( 'graphql_post_entities_allowed_post_types', [ self::class, 'skip_type_registry' ], 10 );
×
37

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

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

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

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

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

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

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

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

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

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

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

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

UNCOV
128
                add_filter(
×
UNCOV
129
                        'graphql_wp_connection_type_config',
×
UNCOV
130
                        [ '\WPGraphQL\WooCommerce\Connection\Products', 'set_connection_config' ]
×
UNCOV
131
                );
×
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 ) {
UNCOV
143
                if ( 'product' === $post_type ) {
×
UNCOV
144
                        $args['show_in_graphql']                  = true;
×
UNCOV
145
                        $args['model']                            = \WPGraphQL\WooCommerce\Model\Product::class;
×
UNCOV
146
                        $args['graphql_single_name']              = 'Product';
×
UNCOV
147
                        $args['graphql_plural_name']              = 'Products';
×
UNCOV
148
                        $args['graphql_kind']                     = 'interface';
×
UNCOV
149
                        $args['graphql_interfaces']               = [ 'ContentNode', 'ProductUnion' ];
×
UNCOV
150
                        $args['graphql_register_root_field']      = false;
×
UNCOV
151
                        $args['graphql_register_root_connection'] = false;
×
UNCOV
152
                        $args['graphql_resolve_type']             = [ self::class, 'resolve_product_type' ];
×
UNCOV
153
                        $args['graphql_exclude_mutations']        = [ 'create', 'delete', 'update' ];
×
154
                }//end if
UNCOV
155
                if ( 'product_variation' === $post_type ) {
×
UNCOV
156
                        $args['show_in_graphql']                  = true;
×
UNCOV
157
                        $args['model']                            = \WPGraphQL\WooCommerce\Model\Product_Variation::class;
×
UNCOV
158
                        $args['graphql_single_name']              = 'ProductVariation';
×
UNCOV
159
                        $args['graphql_plural_name']              = 'ProductVariations';
×
UNCOV
160
                        $args['publicly_queryable']               = true;
×
UNCOV
161
                        $args['graphql_kind']                     = 'interface';
×
UNCOV
162
                        $args['graphql_interfaces']               = [
×
UNCOV
163
                                'Node',
×
UNCOV
164
                                'NodeWithFeaturedImage',
×
UNCOV
165
                                'ContentNode',
×
UNCOV
166
                                'ProductUnion',
×
UNCOV
167
                                'UniformResourceIdentifiable',
×
UNCOV
168
                                'ProductWithPricing',
×
UNCOV
169
                                'ProductWithDimensions',
×
UNCOV
170
                                'InventoriedProduct',
×
UNCOV
171
                                'DownloadableProduct',
×
UNCOV
172
                        ];
×
UNCOV
173
                        $args['graphql_register_root_field']      = false;
×
UNCOV
174
                        $args['graphql_register_root_connection'] = false;
×
UNCOV
175
                        $args['graphql_resolve_type']             = [ self::class, 'resolve_product_variation_type' ];
×
UNCOV
176
                        $args['graphql_exclude_mutations']        = [ 'create', 'delete', 'update' ];
×
177
                }
UNCOV
178
                if ( 'shop_coupon' === $post_type ) {
×
UNCOV
179
                        $args['show_in_graphql']            = true;
×
UNCOV
180
                        $args['graphql_single_name']        = 'Coupon';
×
UNCOV
181
                        $args['graphql_plural_name']        = 'Coupons';
×
UNCOV
182
                        $args['publicly_queryable']         = true;
×
UNCOV
183
                        $args['skip_graphql_type_registry'] = true;
×
184
                }
UNCOV
185
                if ( 'shop_order' === $post_type ) {
×
UNCOV
186
                        $args['show_in_graphql']            = true;
×
UNCOV
187
                        $args['graphql_single_name']        = 'Order';
×
UNCOV
188
                        $args['graphql_plural_name']        = 'Orders';
×
UNCOV
189
                        $args['skip_graphql_type_registry'] = true;
×
190
                }
UNCOV
191
                if ( 'shop_order_refund' === $post_type ) {
×
UNCOV
192
                        $args['show_in_graphql']            = true;
×
UNCOV
193
                        $args['graphql_single_name']        = 'Refund';
×
UNCOV
194
                        $args['graphql_plural_name']        = 'Refunds';
×
UNCOV
195
                        $args['skip_graphql_type_registry'] = true;
×
196
                }
197

UNCOV
198
                return $args;
×
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(
110✔
210
                        $post_types,
110✔
211
                        get_post_types(
110✔
212
                                [
110✔
213
                                        'show_in_graphql'            => true,
110✔
214
                                        'skip_graphql_type_registry' => true,
110✔
215
                                ]
110✔
216
                        )
110✔
217
                );
110✔
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;
117✔
230

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

234
                // WooCommerce DB loaders.
235
                $loaders['cart_item']         = WC_Cart_Item_Loader::class;
117✔
236
                $loaders['downloadable_item'] = WC_Downloadable_Item_Loader::class;
117✔
237
                $loaders['tax_class']         = WC_Tax_Class_Loader::class;
117✔
238
                $loaders['tax_rate']          = WC_Tax_Rate_Loader::class;
117✔
239
                $loaders['order_item']        = WC_Order_Item_Loader::class;
117✔
240
                $loaders['shipping_method']   = WC_Shipping_Method_Loader::class;
117✔
241
                $loaders['shipping_zone']     = WC_Shipping_Zone_Loader::class;
117✔
242
                return $loaders;
117✔
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;
46✔
255
                if ( in_array( 'Product', $config['typeNames'], true ) ) {
46✔
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 ) {
46✔
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;
46✔
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();
47✔
321
                switch ( $type ) {
322
                        case 'Coupon':
47✔
323
                        case 'Order':
47✔
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':
47✔
330
                                $type = self::resolve_product_variation_type( $value );
×
331
                                break;
×
332
                        case 'Product':
47✔
333
                                $type = self::resolve_product_type( $value );
×
334
                }//end switch
335

336
                return $type;
47✔
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();
33✔
350
                $possible_types = WooGraphQL::get_enabled_product_types();
33✔
351

352
                if ( $value instanceof \WPGraphQL\Model\Post && ( 'product' === $value->post_type || 'product_variation' === $value->post_type ) ) {
33✔
353
                        $product_model = \WPGraphQL::get_app_context()
33✔
354
                                ->get_loader( 'wc_post' )
33✔
355
                                ->load( $value->ID );
33✔
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();
33✔
369
                if ( isset( $possible_types[ $product_type ] ) ) {
33✔
370
                        return $type_registry->get_type( $possible_types[ $product_type ] );
33✔
UNCOV
371
                } elseif ( $product_model instanceof \WPGraphQL\WooCommerce\Model\Product_Variation ) {
×
UNCOV
372
                        return self::resolve_product_variation_type( $product_model );
×
UNCOV
373
                } elseif ( 'on' === woographql_setting( 'enable_unsupported_product_type', 'off' ) ) {
×
UNCOV
374
                        $unsupported_type = WooGraphQL::get_supported_product_type();
×
UNCOV
375
                        return $type_registry->get_type( $unsupported_type );
×
376
                }
377

UNCOV
378
                throw new UserError(
×
UNCOV
379
                        sprintf(
×
380
                        /* translators: %s: Product type */
UNCOV
381
                                __( 'The "%s" product type is not supported by the core WPGraphQL for WooCommerce (WooGraphQL) schema.', 'wp-graphql-woocommerce' ),
×
UNCOV
382
                                $product_model->type
×
UNCOV
383
                        )
×
UNCOV
384
                );
×
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();
10✔
398
                $possible_types = WooGraphQL::get_enabled_product_variation_types();
10✔
399
                $product_type   = $value->get_type();
10✔
400

401
                if ( isset( $possible_types[ $product_type ] ) ) {
10✔
402
                        return $type_registry->get_type( $possible_types[ $product_type ] );
10✔
403
                }
404

405
                throw new UserError(
×
406
                        sprintf(
×
407
                        /* translators: %s: Product type */
408
                                __( 'The "%s" product variation type is not supported by the core WPGraphQL for WooCommerce (WooGraphQL) schema.', 'wp-graphql-woocommerce' ),
×
409
                                $value->type
×
410
                        )
×
411
                );
×
412
        }
413

414
        /**
415
         * Filter to make order notes visible in GraphQL queries for authorized users.
416
         *
417
         * @param bool   $is_private Whether the data is private.
418
         * @param string $model_name The name of the model being checked.
419
         * @param mixed  $data       The data being checked.
420
         *
421
         * @return bool
422
         */
423
        public static function make_order_notes_visible( $is_private, $model_name, $data ) {
424
                // Only apply to Comment models.
425
                if ( 'CommentObject' !== $model_name ) {
88✔
426
                        return $is_private;
88✔
427
                }
428

429
                // Check if this is an order note.
UNCOV
430
                if ( $data instanceof \WP_Comment && 'order_note' === $data->comment_type ) {
×
431
                        // Get the parent order.
UNCOV
432
                        $order_id = absint( $data->comment_post_ID );
×
UNCOV
433
                        $order    = wc_get_order( $order_id );
×
434

UNCOV
435
                        if ( ! $order ) {
×
436
                                return true; // Keep it private if order not found.
×
437
                        }
438

439
                        // Allow shop managers and admins to see all order notes.
UNCOV
440
                        if ( current_user_can( 'edit_shop_orders' ) ) {
×
UNCOV
441
                                return false; // Not private.
×
442
                        }
443

444
                        // Allow customers to see customer notes on their own orders.
UNCOV
445
                        $comment_id       = absint( $data->comment_ID );
×
UNCOV
446
                        $is_customer_note = get_comment_meta( $comment_id, 'is_customer_note', true );
×
UNCOV
447
                        if ( $is_customer_note && ! is_bool( $order ) && is_a( $order, \WC_Order::class ) ) {
×
UNCOV
448
                                if ( get_current_user_id() === $order->get_customer_id() ) {
×
UNCOV
449
                                        return false; // Not private.
×
450
                                }
451
                        } elseif ( $is_customer_note && ! is_bool( $order ) && is_a( $order, \WC_Order_Refund::class ) ) {
×
452
                                /** @var \WC_Order|false $parent */
453
                                $parent = wc_get_order( $order->get_parent_id() );
×
454
                                if ( $parent && get_current_user_id() === $parent->get_customer_id() ) {
×
455
                                        return false; // Not private.
×
456
                                }
457
                        }
458

459
                        // Otherwise keep it private.
460
                        return true;
×
461
                }
462

UNCOV
463
                return $is_private;
×
464
        }
465

466
        /**
467
         * Filter to set order notes visibility to public for authorized users.
468
         *
469
         * @param string   $visibility   The visibility of the object.
470
         * @param string   $model_name   The name of the model being checked.
471
         * @param mixed    $data         The data being checked.
472
         * @param int|null $owner        The owner of the object.
473
         * @param \WP_User $current_user The current user.
474
         *
475
         * @return string
476
         */
477
        public static function set_order_notes_visibility( $visibility, $model_name, $data, $owner, $current_user ) {
478
                // Only apply to Comment models.
479
                if ( 'CommentObject' !== $model_name ) {
88✔
480
                        return $visibility;
88✔
481
                }
482

483
                // Check if this is an order note and if user owns the order.
UNCOV
484
                if ( $data instanceof \WP_Comment && 'order_note' === $data->comment_type ) {
×
UNCOV
485
                        $order = wc_get_order( $data->comment_post_ID );
×
486

487
                        // If user is the order owner, make it public.
UNCOV
488
                        if ( $order && ! is_bool( $order ) && is_a( $order, \WC_Order::class ) ) {
×
UNCOV
489
                                return get_current_user_id() === $order->get_customer_id() ? 'public' : $visibility;
×
490
                        } elseif ( $order && ! is_bool( $order ) && is_a( $order, \WC_Order_Refund::class ) ) {
×
491
                                /** @var \WC_Order|false $parent */
492
                                $parent = wc_get_order( $order->get_parent_id() );
×
493
                                return $parent && get_current_user_id() === $parent->get_customer_id() ? 'public' : $visibility;
×
494
                        }
495
                }
496

UNCOV
497
                return $visibility;
×
498
        }
499
}
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