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

wp-graphql / wp-graphql / 17334288077

29 Aug 2025 09:07PM UTC coverage: 84.593% (+0.4%) from 84.169%
17334288077

push

github

actions-user
chore: update changeset for PR #3410

15884 of 18777 relevant lines covered (84.59%)

260.51 hits per line

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

95.42
/src/Registry/TypeRegistry.php
1
<?php
2

3
namespace WPGraphQL\Registry;
4

5
use GraphQL\Error\Error;
6
use GraphQL\Type\Definition\Type;
7
use WPGraphQL;
8
use WPGraphQL\Data\DataSource;
9
use WPGraphQL\Mutation\CommentCreate;
10
use WPGraphQL\Mutation\CommentDelete;
11
use WPGraphQL\Mutation\CommentRestore;
12
use WPGraphQL\Mutation\CommentUpdate;
13
use WPGraphQL\Mutation\MediaItemCreate;
14
use WPGraphQL\Mutation\MediaItemDelete;
15
use WPGraphQL\Mutation\MediaItemUpdate;
16
use WPGraphQL\Mutation\PostObjectCreate;
17
use WPGraphQL\Mutation\PostObjectDelete;
18
use WPGraphQL\Mutation\PostObjectUpdate;
19
use WPGraphQL\Mutation\ResetUserPassword;
20
use WPGraphQL\Mutation\SendPasswordResetEmail;
21
use WPGraphQL\Mutation\TermObjectCreate;
22
use WPGraphQL\Mutation\TermObjectDelete;
23
use WPGraphQL\Mutation\TermObjectUpdate;
24
use WPGraphQL\Mutation\UpdateSettings;
25
use WPGraphQL\Mutation\UserCreate;
26
use WPGraphQL\Mutation\UserDelete;
27
use WPGraphQL\Mutation\UserRegister;
28
use WPGraphQL\Mutation\UserUpdate;
29
use WPGraphQL\Registry\Utils\PostObject;
30
use WPGraphQL\Registry\Utils\TermObject;
31
use WPGraphQL\Type\Connection\Comments;
32
use WPGraphQL\Type\Connection\MenuItems;
33
use WPGraphQL\Type\Connection\PostObjects;
34
use WPGraphQL\Type\Connection\Taxonomies;
35
use WPGraphQL\Type\Connection\TermObjects;
36
use WPGraphQL\Type\Connection\Users;
37
use WPGraphQL\Type\Enum\AvatarRatingEnum;
38
use WPGraphQL\Type\Enum\CommentNodeIdTypeEnum;
39
use WPGraphQL\Type\Enum\CommentStatusEnum;
40
use WPGraphQL\Type\Enum\CommentsConnectionOrderbyEnum;
41
use WPGraphQL\Type\Enum\ContentNodeIdTypeEnum;
42
use WPGraphQL\Type\Enum\ContentTypeEnum;
43
use WPGraphQL\Type\Enum\ContentTypeIdTypeEnum;
44
use WPGraphQL\Type\Enum\MediaItemSizeEnum;
45
use WPGraphQL\Type\Enum\MediaItemStatusEnum;
46
use WPGraphQL\Type\Enum\MenuItemNodeIdTypeEnum;
47
use WPGraphQL\Type\Enum\MenuLocationEnum;
48
use WPGraphQL\Type\Enum\MenuNodeIdTypeEnum;
49
use WPGraphQL\Type\Enum\MimeTypeEnum;
50
use WPGraphQL\Type\Enum\OrderEnum;
51
use WPGraphQL\Type\Enum\PluginStatusEnum;
52
use WPGraphQL\Type\Enum\PostObjectFieldFormatEnum;
53
use WPGraphQL\Type\Enum\PostObjectsConnectionDateColumnEnum;
54
use WPGraphQL\Type\Enum\PostObjectsConnectionOrderbyEnum;
55
use WPGraphQL\Type\Enum\PostStatusEnum;
56
use WPGraphQL\Type\Enum\RelationEnum;
57
use WPGraphQL\Type\Enum\ScriptLoadingGroupLocationEnum;
58
use WPGraphQL\Type\Enum\ScriptLoadingStrategyEnum;
59
use WPGraphQL\Type\Enum\TaxonomyEnum;
60
use WPGraphQL\Type\Enum\TaxonomyIdTypeEnum;
61
use WPGraphQL\Type\Enum\TermNodeIdTypeEnum;
62
use WPGraphQL\Type\Enum\TermObjectsConnectionOrderbyEnum;
63
use WPGraphQL\Type\Enum\TimezoneEnum;
64
use WPGraphQL\Type\Enum\UserNodeIdTypeEnum;
65
use WPGraphQL\Type\Enum\UserRoleEnum;
66
use WPGraphQL\Type\Enum\UsersConnectionOrderbyEnum;
67
use WPGraphQL\Type\Enum\UsersConnectionSearchColumnEnum;
68
use WPGraphQL\Type\Input\DateInput;
69
use WPGraphQL\Type\Input\DateQueryInput;
70
use WPGraphQL\Type\Input\PostObjectsConnectionOrderbyInput;
71
use WPGraphQL\Type\Input\UsersConnectionOrderbyInput;
72
use WPGraphQL\Type\InterfaceType\Commenter;
73
use WPGraphQL\Type\InterfaceType\Connection;
74
use WPGraphQL\Type\InterfaceType\ContentNode;
75
use WPGraphQL\Type\InterfaceType\ContentTemplate;
76
use WPGraphQL\Type\InterfaceType\DatabaseIdentifier;
77
use WPGraphQL\Type\InterfaceType\Edge;
78
use WPGraphQL\Type\InterfaceType\EnqueuedAsset;
79
use WPGraphQL\Type\InterfaceType\HierarchicalContentNode;
80
use WPGraphQL\Type\InterfaceType\HierarchicalNode;
81
use WPGraphQL\Type\InterfaceType\HierarchicalTermNode;
82
use WPGraphQL\Type\InterfaceType\MenuItemLinkable;
83
use WPGraphQL\Type\InterfaceType\Node;
84
use WPGraphQL\Type\InterfaceType\NodeWithAuthor;
85
use WPGraphQL\Type\InterfaceType\NodeWithComments;
86
use WPGraphQL\Type\InterfaceType\NodeWithContentEditor;
87
use WPGraphQL\Type\InterfaceType\NodeWithExcerpt;
88
use WPGraphQL\Type\InterfaceType\NodeWithFeaturedImage;
89
use WPGraphQL\Type\InterfaceType\NodeWithPageAttributes;
90
use WPGraphQL\Type\InterfaceType\NodeWithRevisions;
91
use WPGraphQL\Type\InterfaceType\NodeWithTemplate;
92
use WPGraphQL\Type\InterfaceType\NodeWithTitle;
93
use WPGraphQL\Type\InterfaceType\NodeWithTrackbacks;
94
use WPGraphQL\Type\InterfaceType\OneToOneConnection;
95
use WPGraphQL\Type\InterfaceType\PageInfo;
96
use WPGraphQL\Type\InterfaceType\Previewable;
97
use WPGraphQL\Type\InterfaceType\TermNode;
98
use WPGraphQL\Type\InterfaceType\UniformResourceIdentifiable;
99
use WPGraphQL\Type\ObjectType\Avatar;
100
use WPGraphQL\Type\ObjectType\Comment;
101
use WPGraphQL\Type\ObjectType\CommentAuthor;
102
use WPGraphQL\Type\ObjectType\ContentType;
103
use WPGraphQL\Type\ObjectType\EnqueuedScript;
104
use WPGraphQL\Type\ObjectType\EnqueuedStylesheet;
105
use WPGraphQL\Type\ObjectType\MediaDetails;
106
use WPGraphQL\Type\ObjectType\MediaItemMeta;
107
use WPGraphQL\Type\ObjectType\MediaSize;
108
use WPGraphQL\Type\ObjectType\Menu;
109
use WPGraphQL\Type\ObjectType\MenuItem;
110
use WPGraphQL\Type\ObjectType\Plugin;
111
use WPGraphQL\Type\ObjectType\PostTypeLabelDetails;
112
use WPGraphQL\Type\ObjectType\RootMutation;
113
use WPGraphQL\Type\ObjectType\RootQuery;
114
use WPGraphQL\Type\ObjectType\SettingGroup;
115
use WPGraphQL\Type\ObjectType\Settings;
116
use WPGraphQL\Type\ObjectType\Taxonomy;
117
use WPGraphQL\Type\ObjectType\Theme;
118
use WPGraphQL\Type\ObjectType\User;
119
use WPGraphQL\Type\ObjectType\UserRole;
120
use WPGraphQL\Type\WPConnectionType;
121
use WPGraphQL\Type\WPEnumType;
122
use WPGraphQL\Type\WPInputObjectType;
123
use WPGraphQL\Type\WPInterfaceType;
124
use WPGraphQL\Type\WPMutationType;
125
use WPGraphQL\Type\WPObjectType;
126
use WPGraphQL\Type\WPScalar;
127
use WPGraphQL\Type\WPUnionType;
128
use WPGraphQL\Utils\Utils;
129

130
/**
131
 * Class TypeRegistry
132
 *
133
 * This class maintains the registry of Types used in the GraphQL Schema
134
 *
135
 * @phpstan-import-type InputObjectConfig from \GraphQL\Type\Definition\InputObjectType
136
 * @phpstan-import-type InterfaceConfig from \GraphQL\Type\Definition\InterfaceType
137
 * @phpstan-import-type ObjectConfig from \GraphQL\Type\Definition\ObjectType
138
 * @phpstan-import-type WPEnumTypeConfig from \WPGraphQL\Type\WPEnumType
139
 * @phpstan-import-type WPScalarConfig from \WPGraphQL\Type\WPScalar
140
 *
141
 * @phpstan-type TypeDef \GraphQL\Type\Definition\Type&\GraphQL\Type\Definition\NamedType
142
 *
143
 * @package WPGraphQL\Registry
144
 */
145
class TypeRegistry {
146

147
        /**
148
         * The registered Types
149
         *
150
         * @var array<string,?TypeDef>
151
         */
152
        protected $types;
153

154
        /**
155
         * The keys that are prepared for introspection.
156
         *
157
         * @var array<string>|null
158
         */
159
        protected static ?array $introspection_keys = null;
160

161
        /**
162
         * The loaders needed to register types
163
         *
164
         * @var array<string,callable(): ?TypeDef>
165
         */
166
        protected $type_loaders;
167

168
        /**
169
         * Stores a list of Types that need to be eagerly loaded instead of lazy loaded.
170
         *
171
         * Types that exist in the Schema but are only part of a Union/Interface ResolveType but not
172
         * referenced directly need to be eagerly loaded.
173
         *
174
         * @var array<string,string>
175
         */
176
        protected $eager_type_map;
177

178
        /**
179
         * Stores a list of Types that should be excluded from the schema.
180
         *
181
         * Type names are filtered by `graphql_excluded_types` and normalized using strtolower(), to avoid case sensitivity issues.
182
         *
183
         * @var string[]
184
         */
185
        protected $excluded_types = null;
186

187
        /**
188
         * Stores a list of mutation names that should be excluded from the schema, along with their generated input and payload types.
189
         *
190
         * Type names are filtered by `graphql_excluded_mutations` and normalized using strtolower(), to avoid case sensitivity issues.
191
         *
192
         * @var string[]
193
         */
194
        protected $excluded_mutations = null;
195

196
        /**
197
         * Stores a list of connection Type names that should be excluded from the schema, along with their generated types.
198
         *
199
         * Type names are filtered by `graphql_excluded_connections` and normalized using strtolower(), to avoid case sensitivity issues.
200
         *
201
         * Type name
202
         *
203
         * @var string[]
204
         */
205
        protected $excluded_connections = null;
206

207
        /**
208
         * TypeRegistry constructor.
209
         */
210
        public function __construct() {
603✔
211
                $this->types          = [];
603✔
212
                $this->type_loaders   = [];
603✔
213
                $this->eager_type_map = [];
603✔
214
        }
215

216
        /**
217
         * Formats the array key to a more friendly format
218
         *
219
         * @param string $key Name of the array key to format
220
         *
221
         * @return string
222
         */
223
        protected function format_key( string $key ) {
639✔
224
                return strtolower( $key );
639✔
225
        }
226

227
        /**
228
         * Returns the eager type map, an array of Type definitions for Types that
229
         * are not directly referenced in the schema.
230
         *
231
         * Types can add "eagerlyLoadType => true" when being registered to be included
232
         * in the eager_type_map.
233
         *
234
         * @return array<string,?TypeDef>
235
         */
236
        protected function get_eager_type_map() {
125✔
237
                if ( empty( $this->eager_type_map ) ) {
125✔
238
                        return [];
114✔
239
                }
240

241
                $resolved_types = [];
11✔
242
                foreach ( $this->eager_type_map as $type_name ) {
11✔
243
                        $resolved_types[ $type_name ] = $this->get_type( $type_name );
11✔
244
                }
245

246
                return $resolved_types;
11✔
247
        }
248

249
        /**
250
         * Initialize the TypeRegistry
251
         *
252
         * @throws \Exception
253
         *
254
         * @return void
255
         */
256
        public function init() {
601✔
257
                $this->register_type( 'Bool', Type::boolean() );
601✔
258
                $this->register_type( 'Boolean', Type::boolean() );
601✔
259
                $this->register_type( 'Float', Type::float() );
601✔
260
                $this->register_type( 'Number', Type::float() );
601✔
261
                $this->register_type( 'Id', Type::id() );
601✔
262
                $this->register_type( 'Int', Type::int() );
601✔
263
                $this->register_type( 'Integer', Type::int() );
601✔
264
                $this->register_type( 'String', Type::string() );
601✔
265

266
                /**
267
                 * When the Type Registry is initialized execute these files
268
                 */
269
                add_action( 'init_graphql_type_registry', [ $this, 'init_type_registry' ], 5, 1 );
601✔
270

271
                /**
272
                 * Fire an action as the Type registry is being initiated
273
                 *
274
                 * @param \WPGraphQL\Registry\TypeRegistry $registry Instance of the TypeRegistry
275
                 */
276
                do_action( 'init_graphql_type_registry', $this );
601✔
277
        }
278

279
        /**
280
         * Initialize the Type Registry
281
         *
282
         * @param \WPGraphQL\Registry\TypeRegistry $type_registry
283
         *
284
         * @return void
285
         * @throws \Exception
286
         */
287
        public function init_type_registry( self $type_registry ) {
601✔
288

289
                /**
290
                 * Fire an action as the type registry is initialized. This executes
291
                 * before the `graphql_register_types` action to allow for earlier hooking
292
                 *
293
                 * @param \WPGraphQL\Registry\TypeRegistry $registry Instance of the TypeRegistry
294
                 */
295
                do_action( 'graphql_register_initial_types', $type_registry );
601✔
296

297
                // Register Interfaces.
298
                Node::register_type();
601✔
299
                Commenter::register_type( $type_registry );
601✔
300
                Connection::register_type( $type_registry );
601✔
301
                ContentNode::register_type( $type_registry );
601✔
302
                ContentTemplate::register_type();
601✔
303
                DatabaseIdentifier::register_type();
601✔
304
                Edge::register_type( $type_registry );
601✔
305
                EnqueuedAsset::register_type( $type_registry );
601✔
306
                HierarchicalContentNode::register_type( $type_registry );
601✔
307
                HierarchicalNode::register_type( $type_registry );
601✔
308
                HierarchicalTermNode::register_type( $type_registry );
601✔
309
                MenuItemLinkable::register_type( $type_registry );
601✔
310
                NodeWithAuthor::register_type( $type_registry );
601✔
311
                NodeWithComments::register_type( $type_registry );
601✔
312
                NodeWithContentEditor::register_type( $type_registry );
601✔
313
                NodeWithExcerpt::register_type( $type_registry );
601✔
314
                NodeWithFeaturedImage::register_type( $type_registry );
601✔
315
                NodeWithRevisions::register_type( $type_registry );
601✔
316
                NodeWithTitle::register_type( $type_registry );
601✔
317
                NodeWithTemplate::register_type( $type_registry );
601✔
318
                NodeWithTrackbacks::register_type( $type_registry );
601✔
319
                NodeWithPageAttributes::register_type( $type_registry );
601✔
320
                PageInfo::register_type( $type_registry );
601✔
321
                Previewable::register_type( $type_registry );
601✔
322
                OneToOneConnection::register_type( $type_registry );
601✔
323
                TermNode::register_type( $type_registry );
601✔
324
                UniformResourceIdentifiable::register_type( $type_registry );
601✔
325

326
                // register types
327
                RootQuery::register_type();
601✔
328
                RootQuery::register_post_object_fields();
601✔
329
                RootQuery::register_term_object_fields();
601✔
330
                RootMutation::register_type();
601✔
331
                Avatar::register_type();
601✔
332
                Comment::register_type();
601✔
333
                CommentAuthor::register_type();
601✔
334
                ContentTemplate::register_content_template_types();
601✔
335
                EnqueuedStylesheet::register_type();
601✔
336
                EnqueuedScript::register_type();
601✔
337
                MediaDetails::register_type();
601✔
338
                MediaItemMeta::register_type();
601✔
339
                MediaSize::register_type();
601✔
340
                Menu::register_type();
601✔
341
                MenuItem::register_type();
601✔
342
                Plugin::register_type();
601✔
343
                ContentType::register_type();
601✔
344
                PostTypeLabelDetails::register_type();
601✔
345
                Settings::register_type( $this );
601✔
346
                Taxonomy::register_type();
601✔
347
                Theme::register_type();
601✔
348
                User::register_type();
601✔
349
                UserRole::register_type();
601✔
350

351
                AvatarRatingEnum::register_type();
601✔
352
                CommentNodeIdTypeEnum::register_type();
601✔
353
                CommentsConnectionOrderbyEnum::register_type();
601✔
354
                CommentStatusEnum::register_type();
601✔
355
                ContentNodeIdTypeEnum::register_type();
601✔
356
                ContentTypeEnum::register_type();
601✔
357
                ContentTypeIdTypeEnum::register_type();
601✔
358
                MediaItemSizeEnum::register_type();
601✔
359
                MediaItemStatusEnum::register_type();
601✔
360
                MenuLocationEnum::register_type();
601✔
361
                MenuItemNodeIdTypeEnum::register_type();
601✔
362
                MenuNodeIdTypeEnum::register_type();
601✔
363
                MimeTypeEnum::register_type();
601✔
364
                OrderEnum::register_type();
601✔
365
                PluginStatusEnum::register_type();
601✔
366
                PostObjectFieldFormatEnum::register_type();
601✔
367
                PostObjectsConnectionDateColumnEnum::register_type();
601✔
368
                PostObjectsConnectionOrderbyEnum::register_type();
601✔
369
                PostStatusEnum::register_type();
601✔
370
                RelationEnum::register_type();
601✔
371
                ScriptLoadingStrategyEnum::register_type();
601✔
372
                ScriptLoadingGroupLocationEnum::register_type();
601✔
373
                TaxonomyEnum::register_type();
601✔
374
                TaxonomyIdTypeEnum::register_type();
601✔
375
                TermNodeIdTypeEnum::register_type();
601✔
376
                TermObjectsConnectionOrderbyEnum::register_type();
601✔
377
                TimezoneEnum::register_type();
601✔
378
                UserNodeIdTypeEnum::register_type();
601✔
379
                UserRoleEnum::register_type();
601✔
380
                UsersConnectionOrderbyEnum::register_type();
601✔
381
                UsersConnectionSearchColumnEnum::register_type();
601✔
382

383
                DateInput::register_type();
601✔
384
                DateQueryInput::register_type();
601✔
385
                PostObjectsConnectionOrderbyInput::register_type();
601✔
386
                UsersConnectionOrderbyInput::register_type();
601✔
387

388
                /**
389
                 * Register core connections
390
                 */
391
                Comments::register_connections();
601✔
392
                MenuItems::register_connections();
601✔
393
                PostObjects::register_connections();
601✔
394
                Taxonomies::register_connections();
601✔
395
                TermObjects::register_connections();
601✔
396
                Users::register_connections();
601✔
397

398
                /**
399
                 * Register core mutations
400
                 */
401
                CommentCreate::register_mutation();
601✔
402
                CommentDelete::register_mutation();
601✔
403
                CommentRestore::register_mutation();
601✔
404
                CommentUpdate::register_mutation();
601✔
405
                MediaItemCreate::register_mutation();
601✔
406
                MediaItemDelete::register_mutation();
601✔
407
                MediaItemUpdate::register_mutation();
601✔
408
                ResetUserPassword::register_mutation();
601✔
409
                SendPasswordResetEmail::register_mutation();
601✔
410
                UserCreate::register_mutation();
601✔
411
                UserDelete::register_mutation();
601✔
412
                UserUpdate::register_mutation();
601✔
413
                UserRegister::register_mutation();
601✔
414
                UpdateSettings::register_mutation( $this );
601✔
415

416
                /**
417
                 * Register PostObject types based on post_types configured to show_in_graphql.
418
                 */
419
                $allowed_post_types = WPGraphQL::get_allowed_post_types( 'objects' );
601✔
420
                $allowed_taxonomies = WPGraphQL::get_allowed_taxonomies( 'objects' );
601✔
421

422
                foreach ( $allowed_post_types as $post_type_object ) {
601✔
423
                        PostObject::register_types( $post_type_object );
601✔
424

425
                        /**
426
                         * Mutations for attachments are handled differently
427
                         * because they require different inputs
428
                         */
429
                        if ( 'attachment' !== $post_type_object->name ) {
601✔
430

431
                                /**
432
                                 * Revisions are created behind the scenes as a side effect of post updates,
433
                                 * they aren't created manually.
434
                                 */
435
                                if ( 'revision' !== $post_type_object->name ) {
601✔
436
                                        if ( empty( $post_type_object->graphql_exclude_mutations ) || ! in_array( 'create', $post_type_object->graphql_exclude_mutations, true ) ) {
601✔
437
                                                PostObjectCreate::register_mutation( $post_type_object );
601✔
438
                                        }
439

440
                                        if ( empty( $post_type_object->graphql_exclude_mutations ) || ! in_array( 'update', $post_type_object->graphql_exclude_mutations, true ) ) {
601✔
441
                                                PostObjectUpdate::register_mutation( $post_type_object );
601✔
442
                                        }
443
                                }
444

445
                                if ( empty( $post_type_object->graphql_exclude_mutations ) || ! in_array( 'delete', $post_type_object->graphql_exclude_mutations, true ) ) {
601✔
446
                                        PostObjectDelete::register_mutation( $post_type_object );
601✔
447
                                }
448
                        }
449

450
                        foreach ( $allowed_taxonomies as $tax_object ) {
601✔
451
                                // If the taxonomy is in the array of taxonomies registered to the post_type
452
                                if ( in_array( $tax_object->name, get_object_taxonomies( $post_type_object->name ), true ) ) {
601✔
453
                                        register_graphql_input_type(
601✔
454
                                                $post_type_object->graphql_single_name . ucfirst( $tax_object->graphql_plural_name ) . 'NodeInput',
601✔
455
                                                [
601✔
456
                                                        'description' => static function () use ( $tax_object, $post_type_object ) {
601✔
457
                                                                return sprintf(
15✔
458
                                                                                // translators: %1$s is the GraphQL plural name of the taxonomy, %2$s is the GraphQL singular name of the post type.
459
                                                                        __( 'List of %1$s to connect the %2$s to. If an ID is set, it will be used to create the connection. If not, it will look for a slug. If neither are valid existing terms, and the site is configured to allow terms to be created during post mutations, a term will be created using the Name if it exists in the input, then fallback to the slug if it exists.', 'wp-graphql' ),
15✔
460
                                                                        $tax_object->graphql_plural_name,
15✔
461
                                                                        $post_type_object->graphql_single_name
15✔
462
                                                                );
15✔
463
                                                        },
601✔
464
                                                        'fields'      => [
601✔
465
                                                                'id'          => [
601✔
466
                                                                        'type'        => 'Id',
601✔
467
                                                                        'description' => static function () use ( $tax_object, $post_type_object ) {
601✔
468
                                                                                return sprintf(
15✔
469
                                                                                                // translators: %1$s is the GraphQL name of the taxonomy, %2$s is the GraphQL name of the post type.
470
                                                                                        __( 'The ID of the %1$s. If present, this will be used to connect to the %2$s. If no existing %1$s exists with this ID, no connection will be made.', 'wp-graphql' ),
15✔
471
                                                                                        $tax_object->graphql_single_name,
15✔
472
                                                                                        $post_type_object->graphql_single_name
15✔
473
                                                                                );
15✔
474
                                                                        },
601✔
475
                                                                ],
601✔
476
                                                                'slug'        => [
601✔
477
                                                                        'type'        => 'String',
601✔
478
                                                                        'description' => static function () use ( $tax_object ) {
601✔
479
                                                                                return sprintf(
15✔
480
                                                                                        // translators: %1$s is the GraphQL name of the taxonomy.
481
                                                                                        __( 'The slug of the %1$s. If no ID is present, this field will be used to make a connection. If no existing term exists with this slug, this field will be used as a fallback to the Name field when creating a new term to connect to, if term creation is enabled as a nested mutation.', 'wp-graphql' ),
15✔
482
                                                                                        $tax_object->graphql_single_name
15✔
483
                                                                                );
15✔
484
                                                                        },
601✔
485
                                                                ],
601✔
486
                                                                'description' => [
601✔
487
                                                                        'type'        => 'String',
601✔
488
                                                                        'description' => static function () use ( $tax_object ) {
601✔
489
                                                                                return sprintf(
15✔
490
                                                                                        // translators: %1$s is the GraphQL name of the taxonomy.
491
                                                                                        __( 'The description of the %1$s. This field is used to set a description of the %1$s if a new one is created during the mutation.', 'wp-graphql' ),
15✔
492
                                                                                        $tax_object->graphql_single_name
15✔
493
                                                                                );
15✔
494
                                                                        },
601✔
495
                                                                ],
601✔
496
                                                                'name'        => [
601✔
497
                                                                        'type'        => 'String',
601✔
498
                                                                        'description' => static function () use ( $tax_object ) {
601✔
499
                                                                                return sprintf(
15✔
500
                                                                                        // translators: %1$s is the GraphQL name of the taxonomy.
501
                                                                                        __( 'The name of the %1$s. This field is used to create a new term, if term creation is enabled in nested mutations, and if one does not already exist with the provided slug or ID or if a slug or ID is not provided. If no name is included and a term is created, the creation will fallback to the slug field.', 'wp-graphql' ),
15✔
502
                                                                                        $tax_object->graphql_single_name
15✔
503
                                                                                );
15✔
504
                                                                        },
601✔
505
                                                                ],
601✔
506
                                                        ],
601✔
507
                                                ]
601✔
508
                                        );
601✔
509

510
                                        register_graphql_input_type(
601✔
511
                                                ucfirst( $post_type_object->graphql_single_name ) . ucfirst( $tax_object->graphql_plural_name ) . 'Input',
601✔
512
                                                [
601✔
513
                                                        'description' => static function () use ( $tax_object, $post_type_object ) {
601✔
514
                                                                return sprintf(
15✔
515
                                                                        // translators: %1$s is the GraphQL name of the post type, %2$s is the plural GraphQL name of the taxonomy.
516
                                                                        __( 'Set relationships between the %1$s to %2$s', 'wp-graphql' ),
15✔
517
                                                                        $post_type_object->graphql_single_name,
15✔
518
                                                                        $tax_object->graphql_plural_name
15✔
519
                                                                );
15✔
520
                                                        },
601✔
521
                                                        'fields'      => [
601✔
522
                                                                'append' => [
601✔
523
                                                                        'type'        => 'Boolean',
601✔
524
                                                                        'description' => static function () use ( $tax_object ) {
601✔
525
                                                                                return sprintf(
15✔
526
                                                                                        // translators: %1$s is the GraphQL name of the taxonomy, %2$s is the plural GraphQL name of the taxonomy.
527
                                                                                        __( 'If true, this will append the %1$s to existing related %2$s. If false, this will replace existing relationships. Default true.', 'wp-graphql' ),
15✔
528
                                                                                        $tax_object->graphql_single_name,
15✔
529
                                                                                        $tax_object->graphql_plural_name
15✔
530
                                                                                );
15✔
531
                                                                        },
601✔
532
                                                                ],
601✔
533
                                                                'nodes'  => [
601✔
534
                                                                        'type'        => [
601✔
535
                                                                                'list_of' => $post_type_object->graphql_single_name . ucfirst( $tax_object->graphql_plural_name ) . 'NodeInput',
601✔
536
                                                                        ],
601✔
537
                                                                        'description' => static function () {
601✔
538
                                                                                return __( 'The input list of items to set.', 'wp-graphql' );
15✔
539
                                                                        },
601✔
540
                                                                ],
601✔
541
                                                        ],
601✔
542
                                                ]
601✔
543
                                        );
601✔
544
                                }
545
                        }
546
                }
547

548
                /**
549
                 * Register TermObject types based on taxonomies configured to show_in_graphql
550
                 */
551
                foreach ( $allowed_taxonomies as $tax_object ) {
601✔
552
                        TermObject::register_types( $tax_object );
601✔
553

554
                        if ( empty( $tax_object->graphql_exclude_mutations ) || ! in_array( 'create', $tax_object->graphql_exclude_mutations, true ) ) {
601✔
555
                                TermObjectCreate::register_mutation( $tax_object );
601✔
556
                        }
557

558
                        if ( empty( $tax_object->graphql_exclude_mutations ) || ! in_array( 'update', $tax_object->graphql_exclude_mutations, true ) ) {
601✔
559
                                TermObjectUpdate::register_mutation( $tax_object );
601✔
560
                        }
561

562
                        if ( empty( $tax_object->graphql_exclude_mutations ) || ! in_array( 'delete', $tax_object->graphql_exclude_mutations, true ) ) {
601✔
563
                                TermObjectDelete::register_mutation( $tax_object );
601✔
564
                        }
565
                }
566

567
                /**
568
                 * Create the root query fields for any setting type in
569
                 * the $allowed_setting_types array.
570
                 */
571
                $allowed_setting_types = DataSource::get_allowed_settings_by_group( $this );
601✔
572

573
                /**
574
                 * The url is not a registered setting for multisite, so this is a polyfill
575
                 * to expose the URL to the Schema for multisite sites
576
                 */
577
                if ( is_multisite() ) {
601✔
578
                        $this->register_field(
601✔
579
                                'GeneralSettings',
601✔
580
                                'url',
601✔
581
                                [
601✔
582
                                        'type'        => 'String',
601✔
583
                                        'description' => static function () {
601✔
584
                                                return __( 'Site URL.', 'wp-graphql' );
18✔
585
                                        },
601✔
586
                                        'resolve'     => static function () {
601✔
587
                                                return get_site_url();
1✔
588
                                        },
601✔
589
                                ]
601✔
590
                        );
601✔
591
                }
592

593
                if ( ! empty( $allowed_setting_types ) && is_array( $allowed_setting_types ) ) {
601✔
594
                        foreach ( $allowed_setting_types as $group_name => $setting_type ) {
601✔
595
                                $group_name = DataSource::format_group_name( $group_name );
601✔
596
                                $type_name  = SettingGroup::register_settings_group( $group_name, $group_name, $this );
601✔
597

598
                                if ( ! $type_name ) {
601✔
599
                                        continue;
×
600
                                }
601

602
                                register_graphql_field(
601✔
603
                                        'RootQuery',
601✔
604
                                        Utils::format_field_name( $type_name ),
601✔
605
                                        [
601✔
606
                                                'type'        => $type_name,
601✔
607
                                                'description' => static function () use ( $group_name ) {
601✔
608
                                                        return sprintf(
69✔
609
                                                                // translators: %s is the GraphQL name of the settings group.
610
                                                                __( "Fields of the '%s' settings group", 'wp-graphql' ),
69✔
611
                                                                ucfirst( $group_name ) . 'Settings'
69✔
612
                                                        );
69✔
613
                                                },
601✔
614
                                                'resolve'     => static function () use ( $setting_type ) {
601✔
615
                                                        return $setting_type;
8✔
616
                                                },
601✔
617
                                        ]
601✔
618
                                );
601✔
619
                        }
620
                }
621

622
                /**
623
                 * Fire an action as the type registry is initialized. This executes
624
                 * before the `graphql_register_types` action to allow for earlier hooking
625
                 *
626
                 * @param \WPGraphQL\Registry\TypeRegistry $registry Instance of the TypeRegistry
627
                 */
628
                do_action( 'graphql_register_types', $type_registry );
601✔
629

630
                /**
631
                 * Fire an action as the type registry is initialized. This executes
632
                 * during the `graphql_register_types` action to allow for earlier hooking
633
                 *
634
                 * @param \WPGraphQL\Registry\TypeRegistry $registry Instance of the TypeRegistry
635
                 */
636
                do_action( 'graphql_register_types_late', $type_registry );
601✔
637
        }
638

639
        /**
640
         * Given a config for a custom Scalar, this adds the Scalar for use in the Schema.
641
         *
642
         * @param string              $type_name The name of the Type to register
643
         * @param array<string,mixed> $config    The config for the scalar type to register
644
         *
645
         * @phpstan-param WPScalarConfig $config
646
         *
647
         * @throws \Exception
648
         *
649
         * @return void
650
         */
651
        public function register_scalar( string $type_name, array $config ) {
×
652
                $config['kind'] = 'scalar';
×
653
                $this->register_type( $type_name, $config );
×
654
        }
655

656
        /**
657
         * Registers connections that were passed through the Type registration config
658
         *
659
         * @param array<string,mixed> $config Type config
660
         *
661
         * @return void
662
         *
663
         * @throws \Exception
664
         */
665
        protected function register_connections_from_config( array $config ) {
601✔
666
                $connections = $config['connections'] ?? null;
601✔
667

668
                if ( ! is_array( $connections ) ) {
601✔
669
                        return;
×
670
                }
671

672
                foreach ( $connections as $field_name => $connection_config ) {
601✔
673
                        if ( ! is_array( $connection_config ) ) {
601✔
674
                                continue;
×
675
                        }
676

677
                        $connection_config['fromType']      = $config['name'];
601✔
678
                        $connection_config['fromFieldName'] = $field_name;
601✔
679
                        register_graphql_connection( $connection_config );
601✔
680
                }
681
        }
682

683
        /**
684
         * Add a Type to the Registry
685
         *
686
         * @param string                      $type_name The name of the type to register
687
         * @param array<string,mixed>|TypeDef $config The config for the type
688
         *
689
         * @throws \Exception
690
         */
691
        public function register_type( string $type_name, $config ): void {
614✔
692
                /**
693
                 * If the type should be excluded from the schema, skip it.
694
                 */
695
                if ( in_array( strtolower( $type_name ), $this->get_excluded_types(), true ) ) {
601✔
696
                        return;
5✔
697
                }
698
                /**
699
                 * If the Type Name starts with a number, skip it.
700
                 */
701
                if ( ! is_valid_graphql_name( $type_name ) ) {
601✔
702
                        graphql_debug(
×
703
                                sprintf(
×
704
                                        // translators: %s is the name of the type.
705
                                        __( 'The Type name \'%1$s\' is invalid and has not been added to the GraphQL Schema.', 'wp-graphql' ),
×
706
                                        $type_name
×
707
                                ),
×
708
                                [
×
709
                                        'type'      => 'INVALID_TYPE_NAME',
×
710
                                        'type_name' => $type_name,
×
711
                                ]
×
712
                        );
×
713
                        return;
×
714
                }
715

716
                $type_key = $this->format_key( $type_name );
601✔
717

718
                // If the Type Name is already registered, skip it.
719
                if ( isset( $this->types[ $type_key ] ) || isset( $this->type_loaders[ $type_key ] ) ) {
601✔
720
                        graphql_debug(
2✔
721
                                sprintf(
2✔
722
                                        // translators: %s is the name of the type.
723
                                        __( 'You cannot register duplicate Types to the Schema. The Type \'%1$s\' already exists in the Schema. Make sure to give new Types a unique name.', 'wp-graphql' ),
2✔
724
                                        $type_name
2✔
725
                                ),
2✔
726
                                [
2✔
727
                                        'type'      => 'DUPLICATE_TYPE',
2✔
728
                                        'type_name' => $type_name,
2✔
729
                                ]
2✔
730
                        );
2✔
731
                        return;
2✔
732
                }
733

734
                // Register the type loader.
735
                $this->type_loaders[ $type_key ] = function () use ( $type_name, $config ) {
614✔
736
                        return $this->prepare_type( $type_name, $config );
614✔
737
                };
614✔
738

739
                // If the config isn't an array, there's nothing left to do.
740
                if ( ! is_array( $config ) ) {
601✔
741
                        return;
601✔
742
                }
743

744
                // Register any connections that were passed through the Type config
745
                if ( isset( $config['connections'] ) ) {
601✔
746
                        $config['name'] = ucfirst( $type_name ); // Other types are capitalized in the prepare_type method.
601✔
747
                        $this->register_connections_from_config( $config );
601✔
748
                }
749

750
                // Load eager types if this is an introspection query.
751
                $should_load_eagerly = WPGraphQL::is_introspection_query() && ! empty( $config['eagerlyLoadType'] );
601✔
752
                if ( ! $should_load_eagerly || isset( $this->eager_type_map[ $type_key ] ) ) {
601✔
753
                        return;
601✔
754
                }
755

756
                $this->eager_type_map[ $type_key ] = $type_key;
69✔
757
        }
758

759
        /**
760
         * Add an Object Type to the Registry
761
         *
762
         * @param string              $type_name The name of the type to register
763
         * @param array<string,mixed> $config The configuration of the type
764
         *
765
         * @throws \Exception
766
         */
767
        public function register_object_type( string $type_name, array $config ): void {
601✔
768
                $config['kind'] = 'object';
601✔
769
                $this->register_type( $type_name, $config );
601✔
770
        }
771

772
        /**
773
         * Add an Interface Type to the registry
774
         *
775
         * @param string              $type_name The name of the type to register
776
         * @param array<string,mixed> $config The configuration of the type
777
         *
778
         * @throws \Exception
779
         */
780
        public function register_interface_type( string $type_name, $config ): void {
601✔
781
                $config['kind'] = 'interface';
601✔
782
                $this->register_type( $type_name, $config );
601✔
783
        }
784

785
        /**
786
         * Add an Enum Type to the registry
787
         *
788
         * @param string              $type_name The name of the type to register
789
         * @param array<string,mixed> $config he configuration of the type
790
         *
791
         * @phpstan-param WPEnumTypeConfig $config
792
         *
793
         * @throws \Exception
794
         */
795
        public function register_enum_type( string $type_name, array $config ): void {
×
796
                $config['kind'] = 'enum';
×
797
                $this->register_type( $type_name, $config );
×
798
        }
799

800
        /**
801
         * Add an Input Type to the Registry
802
         *
803
         * @param string              $type_name The name of the type to register
804
         * @param array<string,mixed> $config he configuration of the type
805
         *
806
         * @throws \Exception
807
         */
808
        public function register_input_type( string $type_name, array $config ): void {
601✔
809
                $config['kind'] = 'input';
601✔
810
                $this->register_type( $type_name, $config );
601✔
811
        }
812

813
        /**
814
         * Add a Union Type to the Registry
815
         *
816
         * @param string              $type_name The name of the type to register
817
         * @param array<string,mixed> $config he configuration of the type
818
         *
819
         * @throws \Exception
820
         */
821
        public function register_union_type( string $type_name, array $config ): void {
×
822
                $config['kind'] = 'union';
×
823
                $this->register_type( $type_name, $config );
×
824
        }
825

826
        /**
827
         * Get the keys that are prepared for introspection.
828
         *
829
         * @return array<string>
830
         */
831
        protected static function get_introspection_keys(): array {
615✔
832

833
                if ( null === self::$introspection_keys ) {
615✔
834
                        /**
835
                         * Filter the keys that are prepared for introspection.
836
                         *
837
                         * @param array<string> $introspection_keys The keys to prepare for introspection.
838
                         */
839
                        $introspection_keys       = \apply_filters( 'graphql_introspection_keys', [ 'description', 'deprecationReason' ] );
1✔
840
                        self::$introspection_keys = $introspection_keys;
1✔
841
                }
842

843
                return self::$introspection_keys;
615✔
844
        }
845

846
        /**
847
         * Prepare the config for introspection. This is used to resolve callable values for description and deprecationReason for
848
         * introspection queries.
849
         *
850
         * @template T of array<string,mixed>
851
         * @param T $config The config to prepare.
852
         *
853
         * @return array<string,mixed> The prepared config.
854
         * @phpstan-return T|array{description?: string|null, deprecationReason?: string|null}
855
         *
856
         * @internal
857
         */
858
        public static function prepare_config_for_introspection( array $config ): array {
615✔
859

860
                // Get the keys that are prepared for introspection.
861
                $introspection_keys = self::get_introspection_keys();
615✔
862

863
                foreach ( $introspection_keys as $key ) {
615✔
864
                        if ( ! isset( $config[ $key ] ) || ! is_callable( $config[ $key ] ) ) {
615✔
865
                                continue;
615✔
866
                        }
867

868
                        if ( ! WPGraphQL::is_introspection_query() ) {
613✔
869
                                // If not introspection, set to null.
870
                                $config[ $key ] = null;
546✔
871
                                continue;
546✔
872
                        }
873

874
                        $config[ $key ] = is_callable( $config[ $key ] ) ? $config[ $key ]() : '';
73✔
875
                }
876

877
                return $config;
615✔
878
        }
879

880
        /**
881
         * Prepare the type for registration.
882
         *
883
         * @template T of WPEnumTypeConfig|WPScalarConfig|array<string,mixed>
884
         *
885
         * @param string    $type_name The name of the type to prepare
886
         * @param T|TypeDef $config    The config for the type
887
         *
888
         * @return ?TypeDef The prepared type
889
         */
890
        protected function prepare_type( string $type_name, $config ) {
614✔
891
                if ( ! is_array( $config ) ) {
614✔
892
                        return $config;
602✔
893
                }
894

895
                if ( empty( $config ) ) {
614✔
896
                        return null;
×
897
                }
898

899
                $config         = self::prepare_config_for_introspection( $config );
614✔
900
                $config['name'] = ucfirst( $type_name );
614✔
901

902
                $kind = isset( $config['kind'] ) ? $config['kind'] : null;
614✔
903
                switch ( $kind ) {
904
                        case 'enum':
614✔
905
                                /** @var WPEnumTypeConfig $config */
906
                                $prepared_type = new WPEnumType( $config );
410✔
907
                                break;
410✔
908
                        case 'input':
613✔
909
                                /** @var InputObjectConfig $config */
910
                                $prepared_type = new WPInputObjectType( $config, $this );
427✔
911
                                break;
427✔
912
                        case 'scalar':
612✔
913
                                $prepared_type = new WPScalar( $config, $this );
1✔
914
                                break;
1✔
915
                        case 'union':
612✔
916
                                $prepared_type = new WPUnionType( $config, $this );
126✔
917
                                break;
126✔
918
                        case 'interface':
612✔
919
                                /** @var InterfaceConfig $config */
920
                                $prepared_type = new WPInterfaceType( $config, $this );
544✔
921
                                break;
544✔
922
                        case 'object':
612✔
923
                        default:
924
                                /** @var ObjectConfig $config */
925
                                $prepared_type = new WPObjectType( $config, $this );
612✔
926
                }
927

928
                return $prepared_type;
614✔
929
        }
930

931
        /**
932
         * Given a type name, returns the type or null if not found
933
         *
934
         * @param string $type_name The name of the Type to get from the registry
935
         *
936
         * @return ?TypeDef
937
         */
938
        public function get_type( string $type_name ) {
639✔
939
                $key = $this->format_key( $type_name );
639✔
940

941
                if ( isset( $this->type_loaders[ $key ] ) ) {
639✔
942
                        $type = $this->type_loaders[ $key ]();
614✔
943
                        /**
944
                         * Filter the type before it is loaded into the registry.
945
                         *
946
                         * @param ?TypeDef $type The type to load.
947
                         * @param string   $type_name The name of the type.
948
                         */
949
                        $this->types[ $key ] = apply_filters( 'graphql_get_type', $type, $type_name );
614✔
950
                        unset( $this->type_loaders[ $key ] );
614✔
951
                }
952

953
                return $this->types[ $key ] ?? null;
639✔
954
        }
955

956
        /**
957
         * Given a type name, determines if the type is already present in the Type Loader
958
         *
959
         * @param string $type_name The name of the type to check the registry for
960
         */
961
        public function has_type( string $type_name ): bool {
601✔
962
                return isset( $this->type_loaders[ $this->format_key( $type_name ) ] );
601✔
963
        }
964

965
        /**
966
         * Return the Types in the registry
967
         *
968
         * @return TypeDef[]
969
         */
970
        public function get_types(): array {
125✔
971
                // The full map of types is merged with eager types to support the
972
                // rename_graphql_type API.
973
                //
974
                // All of the types are closures, but eager Types are the full
975
                // Type definitions up front
976
                return array_filter( array_merge( $this->types, $this->get_eager_type_map() ) );
125✔
977
        }
978

979
        /**
980
         * Wrapper for prepare_field to prepare multiple fields for registration at once
981
         *
982
         * @param array<string,mixed> $fields    Array of fields and their settings to register on a Type
983
         * @param string              $type_name Name of the Type to register the fields to
984
         *
985
         * @return array<string,mixed>
986
         * @throws \Exception
987
         */
988
        public function prepare_fields( array $fields, string $type_name ): array {
613✔
989
                $prepared_fields = [];
613✔
990
                foreach ( $fields as $field_name => $field_config ) {
613✔
991
                        if ( is_array( $field_config ) && isset( $field_config['type'] ) ) {
613✔
992
                                $prepared_field = $this->prepare_field( $field_name, $field_config, $type_name );
613✔
993
                                if ( ! empty( $prepared_field ) ) {
613✔
994
                                        $prepared_fields[ $this->format_key( $field_name ) ] = $prepared_field;
613✔
995
                                }
996
                        }
997
                }
998

999
                return $prepared_fields;
613✔
1000
        }
1001

1002
        /**
1003
         * Prepare the field to be registered on the type
1004
         *
1005
         * @param string              $field_name   Friendly name of the field
1006
         * @param array<string,mixed> $field_config Config data about the field to prepare
1007
         * @param string              $type_name    Name of the type to prepare the field for
1008
         *
1009
         * @return ?array<string,mixed>
1010
         * @throws \Exception
1011
         */
1012
        protected function prepare_field( string $field_name, array $field_config, string $type_name ): ?array {
620✔
1013
                if ( ! isset( $field_config['name'] ) ) {
613✔
1014
                        $field_config['name'] = lcfirst( $field_name );
610✔
1015
                }
1016

1017
                if ( ! isset( $field_config['type'] ) ) {
613✔
1018
                        graphql_debug(
1✔
1019
                                sprintf(
1✔
1020
                                        /* translators: %s is the Field name. */
1021
                                        __( 'The registered field \'%s\' does not have a Type defined. Make sure to define a type for all fields.', 'wp-graphql' ),
1✔
1022
                                        $field_name
1✔
1023
                                ),
1✔
1024
                                [
1✔
1025
                                        'type'       => 'INVALID_FIELD_TYPE',
1✔
1026
                                        'type_name'  => $type_name,
1✔
1027
                                        'field_name' => $field_name,
1✔
1028
                                ]
1✔
1029
                        );
1✔
1030
                        return null;
1✔
1031
                }
1032

1033
                /**
1034
                 * If the type is a string, create a callable wrapper to get the type from
1035
                 * type registry. This preserves lazy-loading and prevents a bug where a type
1036
                 * has the same name as a function in the global scope (e.g., `header()`) and
1037
                 * is called since it passes `is_callable`.
1038
                 */
1039
                if ( is_string( $field_config['type'] ) ) {
613✔
1040
                        // Bail if the type is excluded from the Schema.
1041
                        if ( in_array( strtolower( $field_config['type'] ), $this->get_excluded_types(), true ) ) {
612✔
1042
                                return null;
4✔
1043
                        }
1044

1045
                        $field_config['type'] = function () use ( $field_config, $type_name ) {
620✔
1046
                                $type = $this->get_type( $field_config['type'] );
565✔
1047
                                if ( ! $type ) {
565✔
1048
                                        $message = sprintf(
1✔
1049
                                        /* translators: %1$s is the Field name, %2$s is the type name the field belongs to. %3$s is the non-existent type name being referenced. */
1050
                                                __( 'The field \'%1$s\' on Type \'%2$s\' is configured to return \'%3$s\' which is a non-existent Type in the Schema. Make sure to define a valid type for all fields. This might occur if there was a typo with \'%3$s\', or it needs to be registered to the Schema.', 'wp-graphql' ),
1✔
1051
                                                $field_config['name'],
1✔
1052
                                                $type_name,
1✔
1053
                                                $field_config['type']
1✔
1054
                                        );
1✔
1055
                                        // We throw an error here instead of graphql_debug message, as an error would already be thrown if a type didn't exist at this point,
1056
                                        // but now it will have a more helpful error message.
1057
                                        throw new Error( esc_html( $message ) );
1✔
1058
                                }
1059
                                return $type;
565✔
1060
                        };
620✔
1061
                }
1062

1063
                /**
1064
                 * If the type is an array, it contains type modifiers (e.g., "non_null").
1065
                 * Create a callable wrapper to preserve lazy-loading.
1066
                 */
1067
                if ( is_array( $field_config['type'] ) ) {
613✔
1068
                        // Bail if the type is excluded from the Schema.
1069
                        $unmodified_type_name = $this->get_unmodified_type_name( $field_config['type'] );
610✔
1070

1071
                        if ( empty( $unmodified_type_name ) || in_array( strtolower( $unmodified_type_name ), $this->get_excluded_types(), true ) ) {
610✔
1072
                                return null;
1✔
1073
                        }
1074

1075
                        $field_config['type'] = function () use ( $field_config ) {
615✔
1076
                                return $this->setup_type_modifiers( $field_config['type'] );
528✔
1077
                        };
615✔
1078
                }
1079

1080
                /**
1081
                 * If the field has arguments, each one must be prepared.
1082
                 */
1083
                if ( isset( $field_config['args'] ) && is_array( $field_config['args'] ) ) {
613✔
1084
                        foreach ( $field_config['args'] as $arg_name => $arg_config ) {
605✔
1085
                                $arg = $this->prepare_field( $arg_name, $arg_config, $type_name );
605✔
1086

1087
                                // Remove the arg if the field could not be prepared.
1088
                                if ( empty( $arg ) ) {
605✔
1089
                                        unset( $field_config['args'][ $arg_name ] );
1✔
1090
                                        continue;
1✔
1091
                                }
1092

1093
                                $field_config['args'][ $arg_name ] = $arg;
605✔
1094
                        }
1095
                }
1096

1097
                /**
1098
                 * If the field has no (remaining) valid arguments, unset the key.
1099
                 */
1100
                if ( empty( $field_config['args'] ) ) {
613✔
1101
                        unset( $field_config['args'] );
613✔
1102
                }
1103

1104
                $field_config = self::prepare_config_for_introspection( $field_config );
613✔
1105

1106
                return $field_config;
613✔
1107
        }
1108

1109
        /**
1110
         * Processes type modifiers (e.g., "non-null"). Loads types immediately, so do
1111
         * not call before types are ready to be loaded.
1112
         *
1113
         * @template WrappedType of array{non_null:mixed}|array{list_of:mixed}
1114
         * @param WrappedType|array<string,mixed>|string|\GraphQL\Type\Definition\Type $type The type to process.
1115
         *
1116
         * @return ($type is WrappedType ? \GraphQL\Type\Definition\Type : (array<string,mixed>|string|\GraphQL\Type\Definition\Type))
1117
         * @throws \Exception
1118
         */
1119
        public function setup_type_modifiers( $type ) {
528✔
1120
                if ( ! is_array( $type ) ) {
528✔
1121
                        return $type;
528✔
1122
                }
1123

1124
                if ( isset( $type['non_null'] ) ) {
528✔
1125
                        /** @var TypeDef inner_type */
1126
                        $inner_type = $this->setup_type_modifiers( $type['non_null'] );
526✔
1127
                        return $this->non_null( $inner_type );
526✔
1128
                }
1129

1130
                if ( isset( $type['list_of'] ) ) {
399✔
1131
                        /** @var TypeDef $inner_type */
1132
                        $inner_type = $this->setup_type_modifiers( $type['list_of'] );
399✔
1133
                        return $this->list_of( $inner_type );
399✔
1134
                }
1135

1136
                return $type;
×
1137
        }
1138

1139
        /**
1140
         * Wrapper for the register_field method to register multiple fields at once
1141
         *
1142
         * @param string                            $type_name Name of the type in the Type Registry to add the fields to
1143
         * @param array<string,array<string,mixed>> $fields    Fields to register
1144
         *
1145
         * @throws \Exception
1146
         */
1147
        public function register_fields( string $type_name, array $fields = [] ): void {
600✔
1148
                if ( ! empty( $fields ) ) {
600✔
1149
                        foreach ( $fields as $field_name => $config ) {
600✔
1150
                                if ( is_string( $field_name ) && ! empty( $config ) && is_array( $config ) ) {
600✔
1151
                                        $this->register_field( $type_name, $field_name, $config );
600✔
1152
                                }
1153
                        }
1154
                }
1155
        }
1156

1157
        /**
1158
         * Add a field to a Type in the Type Registry
1159
         *
1160
         * @param string              $type_name  Name of the type in the Type Registry to add the fields to
1161
         * @param string              $field_name Name of the field to add to the type
1162
         * @param array<string,mixed> $config     Info about the field to register to the type
1163
         *
1164
         * @throws \Exception
1165
         */
1166
        public function register_field( string $type_name, string $field_name, array $config ): void {
601✔
1167
                add_filter(
601✔
1168
                        'graphql_' . $type_name . '_fields',
601✔
1169
                        function ( $fields ) use ( $type_name, $field_name, $config ) {
601✔
1170

1171
                                // Whether the field should be allowed to have underscores in the field name
1172
                                $allow_field_underscores = isset( $config['allowFieldUnderscores'] ) && true === $config['allowFieldUnderscores'];
600✔
1173

1174
                                $field_name = Utils::format_field_name( $field_name, $allow_field_underscores );
600✔
1175

1176
                                if ( preg_match( '/^\d/', $field_name ) ) {
600✔
1177
                                        graphql_debug(
1✔
1178
                                                sprintf(
1✔
1179
                                                        // translators: %1$s is the field name, %2$s is the type name.
1180
                                                        __( 'The field \'%1$s\' on Type \'%2$s\' is invalid. Field names cannot start with a number.', 'wp-graphql' ),
1✔
1181
                                                        $field_name,
1✔
1182
                                                        $type_name
1✔
1183
                                                ),
1✔
1184
                                                [
1✔
1185
                                                        'type'       => 'INVALID_FIELD_NAME',
1✔
1186
                                                        'field_name' => $field_name,
1✔
1187
                                                        'type_name'  => $type_name,
1✔
1188
                                                ]
1✔
1189
                                        );
1✔
1190
                                        return $fields;
1✔
1191
                                }
1192

1193
                                // if a field has already been registered with the same name output a debug message
1194
                                if ( isset( $fields[ $field_name ] ) ) {
600✔
1195

1196
                                        // if the existing field is a connection type
1197
                                        // and the new field is also a connection type
1198
                                        // and the toType is the same for both
1199
                                        // then we can allow the duplicate field
1200
                                        if (
1201
                                                isset(
1202
                                                        $fields[ $field_name ]['isConnectionField'],
1✔
1203
                                                        $config['isConnectionField'],
1✔
1204
                                                        $fields[ $field_name ]['toType'],
1✔
1205
                                                        $config['toType'],
1✔
1206
                                                        $fields[ $field_name ]['connectionTypeName'],
1✔
1207
                                                        $config['connectionTypeName']
1✔
1208
                                                ) &&
1209
                                                $fields[ $field_name ]['toType'] === $config['toType'] &&
1✔
1210
                                                $fields[ $field_name ]['connectionTypeName'] === $config['connectionTypeName']
1✔
1211
                                        ) {
1212
                                                return $fields;
×
1213
                                        }
1214

1215
                                        graphql_debug(
1✔
1216
                                                sprintf(
1✔
1217
                                                        // translators: %1$s is the field name, %2$s is the type name.
1218
                                                        __( 'You cannot register duplicate fields on the same Type. The field \'%1$s\' already exists on the type \'%2$s\'. Make sure to give the field a unique name.', 'wp-graphql' ),
1✔
1219
                                                        $field_name,
1✔
1220
                                                        $type_name
1✔
1221
                                                ),
1✔
1222
                                                [
1✔
1223
                                                        'type'            => 'DUPLICATE_FIELD',
1✔
1224
                                                        'field_name'      => $field_name,
1✔
1225
                                                        'type_name'       => $type_name,
1✔
1226
                                                        'existing_field'  => $fields[ $field_name ],
1✔
1227
                                                        'duplicate_field' => $config,
1✔
1228
                                                ]
1✔
1229
                                        );
1✔
1230
                                        return $fields;
1✔
1231
                                }
1232

1233
                                /**
1234
                                 * If the field returns a properly prepared field, add it the the field registry
1235
                                 */
1236
                                $field = $this->prepare_field( $field_name, $config, $type_name );
600✔
1237

1238
                                if ( ! empty( $field ) ) {
600✔
1239
                                        $fields[ $field_name ] = $field;
600✔
1240
                                }
1241

1242
                                return $fields;
600✔
1243
                        },
601✔
1244
                        10,
601✔
1245
                        1
601✔
1246
                );
601✔
1247
        }
1248

1249
        /**
1250
         * Remove a field from a type
1251
         *
1252
         * @param string $type_name  Name of the Type the field is registered to
1253
         * @param string $field_name Name of the field you would like to remove from the type
1254
         *
1255
         * @return void
1256
         */
1257
        public function deregister_field( string $type_name, string $field_name ) {
1✔
1258
                add_filter(
1✔
1259
                        'graphql_' . $type_name . '_fields',
1✔
1260
                        static function ( $fields ) use ( $field_name ) {
1✔
1261
                                if ( isset( $fields[ $field_name ] ) ) {
1✔
1262
                                        unset( $fields[ $field_name ] );
1✔
1263
                                }
1264

1265
                                return $fields;
1✔
1266
                        }
1✔
1267
                );
1✔
1268
        }
1269

1270
        /**
1271
         * Method to register a new connection in the Type registry
1272
         *
1273
         * @param array<string,mixed> $config The info about the connection being registered
1274
         *
1275
         * @throws \InvalidArgumentException
1276
         * @throws \Exception
1277
         */
1278
        public function register_connection( array $config ): void {
601✔
1279
                new WPConnectionType( $config, $this );
601✔
1280
        }
1281

1282
        /**
1283
         * Handles registration of a mutation to the Type registry
1284
         *
1285
         * @param string              $mutation_name Name of the mutation being registered
1286
         * @param array<string,mixed> $config        Info about the mutation being registered
1287
         *
1288
         * @throws \Exception
1289
         */
1290
        public function register_mutation( string $mutation_name, array $config ): void {
601✔
1291
                // Bail if the mutation has been excluded from the schema.
1292
                if ( in_array( strtolower( $mutation_name ), $this->get_excluded_mutations(), true ) ) {
601✔
1293
                        return;
2✔
1294
                }
1295

1296
                $config['name'] = $mutation_name;
601✔
1297
                new WPMutationType( $config, $this );
601✔
1298
        }
1299

1300
        /**
1301
         * Removes a GraphQL mutation from the schema.
1302
         *
1303
         * This works by preventing the mutation from being registered in the first place.
1304
         *
1305
         * @uses 'graphql_excluded_mutations' filter.
1306
         *
1307
         * @param string $mutation_name Name of the mutation to remove from the schema.
1308
         *
1309
         * @since 1.14.0
1310
         */
1311
        public function deregister_mutation( string $mutation_name ): void {
2✔
1312
                // Prevent the mutation from being registered to the scheme directly.
1313
                add_filter(
2✔
1314
                        'graphql_excluded_mutations',
2✔
1315
                        static function ( $excluded_mutations ) use ( $mutation_name ): array {
2✔
1316
                                // Normalize the types to prevent case sensitivity issues.
1317
                                $mutation_name = strtolower( $mutation_name );
2✔
1318
                                // If the type isn't already excluded, add it to the array.
1319
                                if ( ! in_array( $mutation_name, $excluded_mutations, true ) ) {
2✔
1320
                                        $excluded_mutations[] = $mutation_name;
2✔
1321
                                }
1322

1323
                                return $excluded_mutations;
2✔
1324
                        },
2✔
1325
                        10
2✔
1326
                );
2✔
1327
        }
1328

1329
        /**
1330
         * Removes a GraphQL connection from the schema.
1331
         *
1332
         * This works by preventing the connection from being registered in the first place.
1333
         *
1334
         * @uses 'graphql_excluded_connections' filter.
1335
         *
1336
         * @param string $connection_name The GraphQL connection name.
1337
         */
1338
        public function deregister_connection( string $connection_name ): void {
1✔
1339
                add_filter(
1✔
1340
                        'graphql_excluded_connections',
1✔
1341
                        static function ( $excluded_connections ) use ( $connection_name ) {
1✔
1342
                                $connection_name = strtolower( $connection_name );
1✔
1343

1344
                                if ( ! in_array( $connection_name, $excluded_connections, true ) ) {
1✔
1345
                                        $excluded_connections[] = $connection_name;
1✔
1346
                                }
1347

1348
                                return $excluded_connections;
1✔
1349
                        }
1✔
1350
                );
1✔
1351
        }
1352

1353
        /**
1354
         * Given a Type, this returns an instance of a NonNull of that type.
1355
         *
1356
         * @template T of \GraphQL\Type\Definition\NullableType&\GraphQL\Type\Definition\Type
1357
         * @param T|string $type The Type being wrapped.
1358
         */
1359
        public function non_null( $type ): \GraphQL\Type\Definition\NonNull {
526✔
1360
                if ( is_string( $type ) ) {
526✔
1361
                        $type_def = $this->get_type( $type );
526✔
1362

1363
                        /** @phpstan-var T&TypeDef $type_def */
1364
                        return Type::nonNull( $type_def );
526✔
1365
                }
1366

1367
                return Type::nonNull( $type );
375✔
1368
        }
1369

1370
        /**
1371
         * Given a Type, this returns an instance of a listOf of that type.
1372
         *
1373
         * @template T of \GraphQL\Type\Definition\Type
1374
         * @param T|string $type The Type being wrapped.
1375
         *
1376
         * @return \GraphQL\Type\Definition\ListOfType<\GraphQL\Type\Definition\Type>
1377
         */
1378
        public function list_of( $type ): \GraphQL\Type\Definition\ListOfType {
400✔
1379
                if ( is_string( $type ) ) {
400✔
1380
                        $resolved_type = $this->get_type( $type );
215✔
1381

1382
                        if ( is_null( $resolved_type ) ) {
215✔
1383
                                $resolved_type = Type::string();
×
1384
                        }
1385

1386
                        $type = $resolved_type;
215✔
1387
                }
1388

1389
                return Type::listOf( $type );
400✔
1390
        }
1391

1392
        /**
1393
         * Get the list of GraphQL type names to exclude from the schema.
1394
         *
1395
         * Type names are normalized using `strtolower()`, to avoid case sensitivity issues.
1396
         *
1397
         * @since 1.13.0
1398
         *
1399
         * @return string[]
1400
         */
1401
        public function get_excluded_types(): array {
613✔
1402
                if ( null === $this->excluded_types ) {
613✔
1403
                        /**
1404
                         * Filter the list of GraphQL types to exclude from the schema.
1405
                         *
1406
                         * Note: using this filter directly will NOT remove the type from being referenced as a possible interface or a union type.
1407
                         * To remove a GraphQL from the schema **entirely**, please use deregister_graphql_type();
1408
                         *
1409
                         * @param string[] $excluded_types The names of the GraphQL Types to exclude.
1410
                         *
1411
                         * @since 1.13.0
1412
                         */
1413
                        $excluded_types = apply_filters( 'graphql_excluded_types', [] );
601✔
1414

1415
                        // Normalize the types to be lowercase, to avoid case-sensitivity issue when comparing.
1416
                        $this->excluded_types = ! empty( $excluded_types ) ? array_map( 'strtolower', $excluded_types ) : [];
601✔
1417
                }
1418

1419
                return $this->excluded_types;
613✔
1420
        }
1421

1422
        /**
1423
         * Get the list of GraphQL connections to exclude from the schema.
1424
         *
1425
         * Type names are normalized using `strtolower()`, to avoid case sensitivity issues.
1426
         *
1427
         * @return string[]
1428
         *
1429
         * @since 1.14.0
1430
         */
1431
        public function get_excluded_connections(): array {
601✔
1432
                if ( null === $this->excluded_connections ) {
601✔
1433
                        /**
1434
                         * Filter the list of GraphQL connections to excluded from the registry.
1435
                         *
1436
                         * @param string[] $excluded_connections The names of the GraphQL connections to exclude.
1437
                         *
1438
                         * @since 1.14.0
1439
                         */
1440
                        $excluded_connections = apply_filters( 'graphql_excluded_connections', [] );
601✔
1441

1442
                        // Normalize the types to be lowercase, to avoid case-sensitivity issue when comparing.
1443
                        $this->excluded_connections = ! empty( $excluded_connections ) ? array_map( 'strtolower', $excluded_connections ) : [];
601✔
1444
                }
1445

1446
                return $this->excluded_connections;
601✔
1447
        }
1448

1449
        /**
1450
         * Get the list of GraphQL mutation names to exclude from the schema.
1451
         *
1452
         * Mutation names are normalized using `strtolower()`, to avoid case sensitivity issues.
1453
         *
1454
         * @return string[]
1455
         * @since 1.14.0
1456
         */
1457
        public function get_excluded_mutations(): array {
601✔
1458
                if ( null === $this->excluded_mutations ) {
601✔
1459
                        /**
1460
                         * Filter the list of GraphQL mutations to excluded from the registry.
1461
                         *
1462
                         * @param string[] $excluded_mutations The names of the GraphQL mutations to exclude.
1463
                         *
1464
                         * @since 1.14.0
1465
                         */
1466
                        $excluded_mutations = apply_filters( 'graphql_excluded_mutations', [] );
601✔
1467

1468
                        // Normalize the types to be lowercase, to avoid case-sensitivity issue when comparing.
1469
                        $this->excluded_mutations = ! empty( $excluded_mutations ) ? array_map( 'strtolower', $excluded_mutations ) : [];
601✔
1470
                }
1471

1472
                return $this->excluded_mutations;
601✔
1473
        }
1474

1475
        /**
1476
         * Gets the actual type name, stripped of possible NonNull and ListOf wrappers.
1477
         *
1478
         * Returns an empty string if the type modifiers are malformed.
1479
         *
1480
         * @param string|array<string|int,mixed> $type The (possibly-wrapped) type name.
1481
         */
1482
        protected function get_unmodified_type_name( $type ): string {
610✔
1483
                if ( ! is_array( $type ) ) {
610✔
1484
                        return $type;
610✔
1485
                }
1486

1487
                $type = array_values( $type )[0] ?? '';
610✔
1488

1489
                return $this->get_unmodified_type_name( $type );
610✔
1490
        }
1491
}
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