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

wp-graphql / wp-graphql-woocommerce / 27452430870

13 Jun 2026 01:26AM UTC coverage: 91.8%. Remained the same
27452430870

Pull #1019

github

web-flow
Merge f03617ca3 into 2ce9424e1
Pull Request #1019: fix: address WordPress.org plugin review (rename + prefixing + headers)

1330 of 1587 new or added lines in 201 files covered. (83.81%)

1 existing line in 1 file now uncovered.

18528 of 20183 relevant lines covered (91.8%)

152.68 hits per line

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

78.46
/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(
298✔
210
                        $post_types,
298✔
211
                        get_post_types(
298✔
212
                                [
298✔
213
                                        'show_in_graphql'            => true,
298✔
214
                                        'skip_graphql_type_registry' => true,
298✔
215
                                ]
298✔
216
                        )
298✔
217
                );
298✔
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;
309✔
230

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

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

336
                return $type;
159✔
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 */
NEW
360
                                        __( 'The "%s" post type is not a valid product type.', 'graphql-for-ecommerce' ),
×
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 GraphQL for eCommerce schema.', 'graphql-for-ecommerce' ),
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();
29✔
398
                $possible_types = WooGraphQL::get_enabled_product_variation_types();
29✔
399
                $product_type   = $value->get_type();
29✔
400

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

405
                throw new UserError(
×
406
                        sprintf(
×
407
                        /* translators: %s: Product type */
NEW
408
                                __( 'The "%s" product variation type is not supported by the core GraphQL for eCommerce schema.', 'graphql-for-ecommerce' ),
×
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 ) {
245✔
426
                        return $is_private;
244✔
427
                }
428

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

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

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

444
                        // Allow customers to see customer notes on their own orders.
445
                        $comment_id       = absint( $data->comment_ID );
1✔
446
                        $is_customer_note = get_comment_meta( $comment_id, 'is_customer_note', true );
1✔
447
                        if ( $is_customer_note && ! is_bool( $order ) && is_a( $order, \WC_Order::class ) ) {
1✔
448
                                if ( get_current_user_id() === $order->get_customer_id() ) {
1✔
449
                                        return false; // Not private.
1✔
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

463
                return $is_private;
5✔
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 ) {
245✔
480
                        return $visibility;
244✔
481
                }
482

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

487
                        // If user is the order owner, make it public.
488
                        if ( $order && ! is_bool( $order ) && is_a( $order, \WC_Order::class ) ) {
3✔
489
                                return get_current_user_id() === $order->get_customer_id() ? 'public' : $visibility;
3✔
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

497
                return $visibility;
5✔
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