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

wp-graphql / wp-graphql / 15710056976

17 Jun 2025 02:27PM UTC coverage: 84.17% (-0.1%) from 84.287%
15710056976

push

github

actions-user
release: merge develop into master for v2.3.3

15925 of 18920 relevant lines covered (84.17%)

258.66 hits per line

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

95.44
/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\Union\MenuItemObjectUnion;
121
use WPGraphQL\Type\Union\PostObjectUnion;
122
use WPGraphQL\Type\Union\TermObjectUnion;
123
use WPGraphQL\Type\WPConnectionType;
124
use WPGraphQL\Type\WPEnumType;
125
use WPGraphQL\Type\WPInputObjectType;
126
use WPGraphQL\Type\WPInterfaceType;
127
use WPGraphQL\Type\WPMutationType;
128
use WPGraphQL\Type\WPObjectType;
129
use WPGraphQL\Type\WPScalar;
130
use WPGraphQL\Type\WPUnionType;
131
use WPGraphQL\Utils\Utils;
132

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

150
        /**
151
         * The registered Types
152
         *
153
         * @var array<string,?TypeDef>
154
         */
155
        protected $types;
156

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

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

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

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

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

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

210
        /**
211
         * TypeRegistry constructor.
212
         */
213
        public function __construct() {
601✔
214
                $this->types          = [];
601✔
215
                $this->type_loaders   = [];
601✔
216
                $this->eager_type_map = [];
601✔
217
        }
218

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

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

244
                $resolved_types = [];
11✔
245
                foreach ( $this->eager_type_map as $type_name ) {
11✔
246
                        $resolved_types[ $type_name ] = $this->get_type( $type_name );
11✔
247
                }
248

249
                return $resolved_types;
11✔
250
        }
251

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

269
                /**
270
                 * When the Type Registry is initialized execute these files
271
                 */
272
                add_action( 'init_graphql_type_registry', [ $this, 'init_type_registry' ], 5, 1 );
599✔
273

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

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

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

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

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

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

386
                DateInput::register_type();
599✔
387
                DateQueryInput::register_type();
599✔
388
                PostObjectsConnectionOrderbyInput::register_type();
599✔
389
                UsersConnectionOrderbyInput::register_type();
599✔
390

391
                // Deprecated types.
392
                MenuItemObjectUnion::register_type( $this ); /* @phpstan-ignore staticMethod.deprecatedClass */
599✔
393
                PostObjectUnion::register_type( $this ); /* @phpstan-ignore staticMethod.deprecatedClass */
599✔
394
                TermObjectUnion::register_type( $this ); /* @phpstan-ignore staticMethod.deprecatedClass */
599✔
395

396
                /**
397
                 * Register core connections
398
                 */
399
                Comments::register_connections();
599✔
400
                MenuItems::register_connections();
599✔
401
                PostObjects::register_connections();
599✔
402
                Taxonomies::register_connections();
599✔
403
                TermObjects::register_connections();
599✔
404
                Users::register_connections();
599✔
405

406
                /**
407
                 * Register core mutations
408
                 */
409
                CommentCreate::register_mutation();
599✔
410
                CommentDelete::register_mutation();
599✔
411
                CommentRestore::register_mutation();
599✔
412
                CommentUpdate::register_mutation();
599✔
413
                MediaItemCreate::register_mutation();
599✔
414
                MediaItemDelete::register_mutation();
599✔
415
                MediaItemUpdate::register_mutation();
599✔
416
                ResetUserPassword::register_mutation();
599✔
417
                SendPasswordResetEmail::register_mutation();
599✔
418
                UserCreate::register_mutation();
599✔
419
                UserDelete::register_mutation();
599✔
420
                UserUpdate::register_mutation();
599✔
421
                UserRegister::register_mutation();
599✔
422
                UpdateSettings::register_mutation( $this );
599✔
423

424
                /**
425
                 * Register PostObject types based on post_types configured to show_in_graphql.
426
                 */
427
                $allowed_post_types = WPGraphQL::get_allowed_post_types( 'objects' );
599✔
428
                $allowed_taxonomies = WPGraphQL::get_allowed_taxonomies( 'objects' );
599✔
429

430
                foreach ( $allowed_post_types as $post_type_object ) {
599✔
431
                        PostObject::register_types( $post_type_object );
599✔
432

433
                        /**
434
                         * Mutations for attachments are handled differently
435
                         * because they require different inputs
436
                         */
437
                        if ( 'attachment' !== $post_type_object->name ) {
599✔
438

439
                                /**
440
                                 * Revisions are created behind the scenes as a side effect of post updates,
441
                                 * they aren't created manually.
442
                                 */
443
                                if ( 'revision' !== $post_type_object->name ) {
599✔
444
                                        if ( empty( $post_type_object->graphql_exclude_mutations ) || ! in_array( 'create', $post_type_object->graphql_exclude_mutations, true ) ) {
599✔
445
                                                PostObjectCreate::register_mutation( $post_type_object );
599✔
446
                                        }
447

448
                                        if ( empty( $post_type_object->graphql_exclude_mutations ) || ! in_array( 'update', $post_type_object->graphql_exclude_mutations, true ) ) {
599✔
449
                                                PostObjectUpdate::register_mutation( $post_type_object );
599✔
450
                                        }
451
                                }
452

453
                                if ( empty( $post_type_object->graphql_exclude_mutations ) || ! in_array( 'delete', $post_type_object->graphql_exclude_mutations, true ) ) {
599✔
454
                                        PostObjectDelete::register_mutation( $post_type_object );
599✔
455
                                }
456
                        }
457

458
                        foreach ( $allowed_taxonomies as $tax_object ) {
599✔
459
                                // If the taxonomy is in the array of taxonomies registered to the post_type
460
                                if ( in_array( $tax_object->name, get_object_taxonomies( $post_type_object->name ), true ) ) {
599✔
461
                                        register_graphql_input_type(
599✔
462
                                                $post_type_object->graphql_single_name . ucfirst( $tax_object->graphql_plural_name ) . 'NodeInput',
599✔
463
                                                [
599✔
464
                                                        'description' => static function () use ( $tax_object, $post_type_object ) {
599✔
465
                                                                return sprintf(
15✔
466
                                                                                // translators: %1$s is the GraphQL plural name of the taxonomy, %2$s is the GraphQL singular name of the post type.
467
                                                                        __( '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✔
468
                                                                        $tax_object->graphql_plural_name,
15✔
469
                                                                        $post_type_object->graphql_single_name
15✔
470
                                                                );
15✔
471
                                                        },
599✔
472
                                                        'fields'      => [
599✔
473
                                                                'id'          => [
599✔
474
                                                                        'type'        => 'Id',
599✔
475
                                                                        'description' => static function () use ( $tax_object, $post_type_object ) {
599✔
476
                                                                                return sprintf(
15✔
477
                                                                                                // translators: %1$s is the GraphQL name of the taxonomy, %2$s is the GraphQL name of the post type.
478
                                                                                        __( '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✔
479
                                                                                        $tax_object->graphql_single_name,
15✔
480
                                                                                        $post_type_object->graphql_single_name
15✔
481
                                                                                );
15✔
482
                                                                        },
599✔
483
                                                                ],
599✔
484
                                                                'slug'        => [
599✔
485
                                                                        'type'        => 'String',
599✔
486
                                                                        'description' => static function () use ( $tax_object ) {
599✔
487
                                                                                return sprintf(
15✔
488
                                                                                        // translators: %1$s is the GraphQL name of the taxonomy.
489
                                                                                        __( '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✔
490
                                                                                        $tax_object->graphql_single_name
15✔
491
                                                                                );
15✔
492
                                                                        },
599✔
493
                                                                ],
599✔
494
                                                                'description' => [
599✔
495
                                                                        'type'        => 'String',
599✔
496
                                                                        'description' => static function () use ( $tax_object ) {
599✔
497
                                                                                return sprintf(
15✔
498
                                                                                        // translators: %1$s is the GraphQL name of the taxonomy.
499
                                                                                        __( '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✔
500
                                                                                        $tax_object->graphql_single_name
15✔
501
                                                                                );
15✔
502
                                                                        },
599✔
503
                                                                ],
599✔
504
                                                                'name'        => [
599✔
505
                                                                        'type'        => 'String',
599✔
506
                                                                        'description' => static function () use ( $tax_object ) {
599✔
507
                                                                                return sprintf(
15✔
508
                                                                                        // translators: %1$s is the GraphQL name of the taxonomy.
509
                                                                                        __( '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✔
510
                                                                                        $tax_object->graphql_single_name
15✔
511
                                                                                );
15✔
512
                                                                        },
599✔
513
                                                                ],
599✔
514
                                                        ],
599✔
515
                                                ]
599✔
516
                                        );
599✔
517

518
                                        register_graphql_input_type(
599✔
519
                                                ucfirst( $post_type_object->graphql_single_name ) . ucfirst( $tax_object->graphql_plural_name ) . 'Input',
599✔
520
                                                [
599✔
521
                                                        'description' => static function () use ( $tax_object, $post_type_object ) {
599✔
522
                                                                return sprintf(
15✔
523
                                                                        // translators: %1$s is the GraphQL name of the post type, %2$s is the plural GraphQL name of the taxonomy.
524
                                                                        __( 'Set relationships between the %1$s to %2$s', 'wp-graphql' ),
15✔
525
                                                                        $post_type_object->graphql_single_name,
15✔
526
                                                                        $tax_object->graphql_plural_name
15✔
527
                                                                );
15✔
528
                                                        },
599✔
529
                                                        'fields'      => [
599✔
530
                                                                'append' => [
599✔
531
                                                                        'type'        => 'Boolean',
599✔
532
                                                                        'description' => static function () use ( $tax_object ) {
599✔
533
                                                                                return sprintf(
15✔
534
                                                                                        // translators: %1$s is the GraphQL name of the taxonomy, %2$s is the plural GraphQL name of the taxonomy.
535
                                                                                        __( '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✔
536
                                                                                        $tax_object->graphql_single_name,
15✔
537
                                                                                        $tax_object->graphql_plural_name
15✔
538
                                                                                );
15✔
539
                                                                        },
599✔
540
                                                                ],
599✔
541
                                                                'nodes'  => [
599✔
542
                                                                        'type'        => [
599✔
543
                                                                                'list_of' => $post_type_object->graphql_single_name . ucfirst( $tax_object->graphql_plural_name ) . 'NodeInput',
599✔
544
                                                                        ],
599✔
545
                                                                        'description' => static function () {
599✔
546
                                                                                return __( 'The input list of items to set.', 'wp-graphql' );
15✔
547
                                                                        },
599✔
548
                                                                ],
599✔
549
                                                        ],
599✔
550
                                                ]
599✔
551
                                        );
599✔
552
                                }
553
                        }
554
                }
555

556
                /**
557
                 * Register TermObject types based on taxonomies configured to show_in_graphql
558
                 */
559
                foreach ( $allowed_taxonomies as $tax_object ) {
599✔
560
                        TermObject::register_types( $tax_object );
599✔
561

562
                        if ( empty( $tax_object->graphql_exclude_mutations ) || ! in_array( 'create', $tax_object->graphql_exclude_mutations, true ) ) {
599✔
563
                                TermObjectCreate::register_mutation( $tax_object );
599✔
564
                        }
565

566
                        if ( empty( $tax_object->graphql_exclude_mutations ) || ! in_array( 'update', $tax_object->graphql_exclude_mutations, true ) ) {
599✔
567
                                TermObjectUpdate::register_mutation( $tax_object );
599✔
568
                        }
569

570
                        if ( empty( $tax_object->graphql_exclude_mutations ) || ! in_array( 'delete', $tax_object->graphql_exclude_mutations, true ) ) {
599✔
571
                                TermObjectDelete::register_mutation( $tax_object );
599✔
572
                        }
573
                }
574

575
                /**
576
                 * Create the root query fields for any setting type in
577
                 * the $allowed_setting_types array.
578
                 */
579
                $allowed_setting_types = DataSource::get_allowed_settings_by_group( $this );
599✔
580

581
                /**
582
                 * The url is not a registered setting for multisite, so this is a polyfill
583
                 * to expose the URL to the Schema for multisite sites
584
                 */
585
                if ( is_multisite() ) {
599✔
586
                        $this->register_field(
599✔
587
                                'GeneralSettings',
599✔
588
                                'url',
599✔
589
                                [
599✔
590
                                        'type'        => 'String',
599✔
591
                                        'description' => static function () {
599✔
592
                                                return __( 'Site URL.', 'wp-graphql' );
18✔
593
                                        },
599✔
594
                                        'resolve'     => static function () {
599✔
595
                                                return get_site_url();
1✔
596
                                        },
599✔
597
                                ]
599✔
598
                        );
599✔
599
                }
600

601
                if ( ! empty( $allowed_setting_types ) && is_array( $allowed_setting_types ) ) {
599✔
602
                        foreach ( $allowed_setting_types as $group_name => $setting_type ) {
599✔
603
                                $group_name = DataSource::format_group_name( $group_name );
599✔
604
                                $type_name  = SettingGroup::register_settings_group( $group_name, $group_name, $this );
599✔
605

606
                                if ( ! $type_name ) {
599✔
607
                                        continue;
×
608
                                }
609

610
                                register_graphql_field(
599✔
611
                                        'RootQuery',
599✔
612
                                        Utils::format_field_name( $type_name ),
599✔
613
                                        [
599✔
614
                                                'type'        => $type_name,
599✔
615
                                                'description' => static function () use ( $group_name ) {
599✔
616
                                                        return sprintf(
69✔
617
                                                                // translators: %s is the GraphQL name of the settings group.
618
                                                                __( "Fields of the '%s' settings group", 'wp-graphql' ),
69✔
619
                                                                ucfirst( $group_name ) . 'Settings'
69✔
620
                                                        );
69✔
621
                                                },
599✔
622
                                                'resolve'     => static function () use ( $setting_type ) {
599✔
623
                                                        return $setting_type;
8✔
624
                                                },
599✔
625
                                        ]
599✔
626
                                );
599✔
627
                        }
628
                }
629

630
                /**
631
                 * Fire an action as the type registry is initialized. This executes
632
                 * before 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', $type_registry );
599✔
637

638
                /**
639
                 * Fire an action as the type registry is initialized. This executes
640
                 * during the `graphql_register_types` action to allow for earlier hooking
641
                 *
642
                 * @param \WPGraphQL\Registry\TypeRegistry $registry Instance of the TypeRegistry
643
                 */
644
                do_action( 'graphql_register_types_late', $type_registry );
599✔
645
        }
646

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

664
        /**
665
         * Registers connections that were passed through the Type registration config
666
         *
667
         * @param array<string,mixed> $config Type config
668
         *
669
         * @return void
670
         *
671
         * @throws \Exception
672
         */
673
        protected function register_connections_from_config( array $config ) {
599✔
674
                $connections = $config['connections'] ?? null;
599✔
675

676
                if ( ! is_array( $connections ) ) {
599✔
677
                        return;
×
678
                }
679

680
                foreach ( $connections as $field_name => $connection_config ) {
599✔
681
                        if ( ! is_array( $connection_config ) ) {
599✔
682
                                continue;
×
683
                        }
684

685
                        $connection_config['fromType']      = $config['name'];
599✔
686
                        $connection_config['fromFieldName'] = $field_name;
599✔
687
                        register_graphql_connection( $connection_config );
599✔
688
                }
689
        }
690

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

724
                $type_key = $this->format_key( $type_name );
599✔
725

726
                // If the Type Name is already registered, skip it.
727
                if ( isset( $this->types[ $type_key ] ) || isset( $this->type_loaders[ $type_key ] ) ) {
599✔
728
                        graphql_debug(
2✔
729
                                sprintf(
2✔
730
                                        // translators: %s is the name of the type.
731
                                        __( '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✔
732
                                        $type_name
2✔
733
                                ),
2✔
734
                                [
2✔
735
                                        'type'      => 'DUPLICATE_TYPE',
2✔
736
                                        'type_name' => $type_name,
2✔
737
                                ]
2✔
738
                        );
2✔
739
                        return;
2✔
740
                }
741

742
                // Register the type loader.
743
                $this->type_loaders[ $type_key ] = function () use ( $type_name, $config ) {
612✔
744
                        return $this->prepare_type( $type_name, $config );
612✔
745
                };
612✔
746

747
                // If the config isn't an array, there's nothing left to do.
748
                if ( ! is_array( $config ) ) {
599✔
749
                        return;
599✔
750
                }
751

752
                // Register any connections that were passed through the Type config
753
                if ( isset( $config['connections'] ) ) {
599✔
754
                        $config['name'] = ucfirst( $type_name ); // Other types are capitalized in the prepare_type method.
599✔
755
                        $this->register_connections_from_config( $config );
599✔
756
                }
757

758
                // Load eager types if this is an introspection query.
759
                $should_load_eagerly = WPGraphQL::is_introspection_query() && ! empty( $config['eagerlyLoadType'] );
599✔
760
                if ( ! $should_load_eagerly || isset( $this->eager_type_map[ $type_key ] ) ) {
599✔
761
                        return;
599✔
762
                }
763

764
                $this->eager_type_map[ $type_key ] = $type_key;
69✔
765
        }
766

767
        /**
768
         * Add an Object Type to the Registry
769
         *
770
         * @param string              $type_name The name of the type to register
771
         * @param array<string,mixed> $config The configuration of the type
772
         *
773
         * @throws \Exception
774
         */
775
        public function register_object_type( string $type_name, array $config ): void {
599✔
776
                $config['kind'] = 'object';
599✔
777
                $this->register_type( $type_name, $config );
599✔
778
        }
779

780
        /**
781
         * Add an Interface Type to the registry
782
         *
783
         * @param string              $type_name The name of the type to register
784
         * @param array<string,mixed> $config The configuration of the type
785
         *
786
         * @throws \Exception
787
         */
788
        public function register_interface_type( string $type_name, $config ): void {
599✔
789
                $config['kind'] = 'interface';
599✔
790
                $this->register_type( $type_name, $config );
599✔
791
        }
792

793
        /**
794
         * Add an Enum Type to the registry
795
         *
796
         * @param string              $type_name The name of the type to register
797
         * @param array<string,mixed> $config he configuration of the type
798
         *
799
         * @phpstan-param WPEnumTypeConfig $config
800
         *
801
         * @throws \Exception
802
         */
803
        public function register_enum_type( string $type_name, array $config ): void {
×
804
                $config['kind'] = 'enum';
×
805
                $this->register_type( $type_name, $config );
×
806
        }
807

808
        /**
809
         * Add an Input Type to the Registry
810
         *
811
         * @param string              $type_name The name of the type to register
812
         * @param array<string,mixed> $config he configuration of the type
813
         *
814
         * @throws \Exception
815
         */
816
        public function register_input_type( string $type_name, array $config ): void {
599✔
817
                $config['kind'] = 'input';
599✔
818
                $this->register_type( $type_name, $config );
599✔
819
        }
820

821
        /**
822
         * Add a Union Type to the Registry
823
         *
824
         * @param string              $type_name The name of the type to register
825
         * @param array<string,mixed> $config he configuration of the type
826
         *
827
         * @throws \Exception
828
         */
829
        public function register_union_type( string $type_name, array $config ): void {
×
830
                $config['kind'] = 'union';
×
831
                $this->register_type( $type_name, $config );
×
832
        }
833

834
        /**
835
         * Get the keys that are prepared for introspection.
836
         *
837
         * @return array<string>
838
         */
839
        protected static function get_introspection_keys(): array {
613✔
840

841
                if ( null === self::$introspection_keys ) {
613✔
842
                        /**
843
                         * Filter the keys that are prepared for introspection.
844
                         *
845
                         * @param array<string> $introspection_keys The keys to prepare for introspection.
846
                         */
847
                        $introspection_keys       = \apply_filters( 'graphql_introspection_keys', [ 'description', 'deprecationReason' ] );
1✔
848
                        self::$introspection_keys = $introspection_keys;
1✔
849
                }
850

851
                return self::$introspection_keys;
613✔
852
        }
853

854
        /**
855
         * Prepare the config for introspection. This is used to resolve callable values for description and deprecationReason for
856
         * introspection queries.
857
         *
858
         * @template T of array<string,mixed>
859
         * @param T $config The config to prepare.
860
         *
861
         * @return array<string,mixed> The prepared config.
862
         * @phpstan-return T|array{description?: string|null, deprecationReason?: string|null}
863
         *
864
         * @internal
865
         */
866
        public static function prepare_config_for_introspection( array $config ): array {
613✔
867

868
                // Get the keys that are prepared for introspection.
869
                $introspection_keys = self::get_introspection_keys();
613✔
870

871
                foreach ( $introspection_keys as $key ) {
613✔
872
                        if ( ! isset( $config[ $key ] ) || ! is_callable( $config[ $key ] ) ) {
613✔
873
                                continue;
613✔
874
                        }
875

876
                        if ( ! WPGraphQL::is_introspection_query() ) {
611✔
877
                                // If not introspection, set to null.
878
                                $config[ $key ] = null;
544✔
879
                                continue;
544✔
880
                        }
881

882
                        $config[ $key ] = is_callable( $config[ $key ] ) ? $config[ $key ]() : '';
73✔
883
                }
884

885
                return $config;
613✔
886
        }
887

888
        /**
889
         * Prepare the type for registration.
890
         *
891
         * @template T of WPEnumTypeConfig|WPScalarConfig|array<string,mixed>
892
         *
893
         * @param string    $type_name The name of the type to prepare
894
         * @param T|TypeDef $config    The config for the type
895
         *
896
         * @return ?TypeDef The prepared type
897
         */
898
        protected function prepare_type( string $type_name, $config ) {
612✔
899
                if ( ! is_array( $config ) ) {
612✔
900
                        return $config;
600✔
901
                }
902

903
                if ( empty( $config ) ) {
612✔
904
                        return null;
×
905
                }
906

907
                $config         = self::prepare_config_for_introspection( $config );
612✔
908
                $config['name'] = ucfirst( $type_name );
612✔
909

910
                $kind = isset( $config['kind'] ) ? $config['kind'] : null;
612✔
911
                switch ( $kind ) {
912
                        case 'enum':
612✔
913
                                /** @var WPEnumTypeConfig $config */
914
                                $prepared_type = new WPEnumType( $config );
408✔
915
                                break;
408✔
916
                        case 'input':
611✔
917
                                /** @var InputObjectConfig $config */
918
                                $prepared_type = new WPInputObjectType( $config, $this );
425✔
919
                                break;
425✔
920
                        case 'scalar':
610✔
921
                                $prepared_type = new WPScalar( $config, $this );
1✔
922
                                break;
1✔
923
                        case 'union':
610✔
924
                                $prepared_type = new WPUnionType( $config, $this );
127✔
925
                                break;
127✔
926
                        case 'interface':
610✔
927
                                /** @var InterfaceConfig $config */
928
                                $prepared_type = new WPInterfaceType( $config, $this );
542✔
929
                                break;
542✔
930
                        case 'object':
610✔
931
                        default:
932
                                /** @var ObjectConfig $config */
933
                                $prepared_type = new WPObjectType( $config, $this );
610✔
934
                }
935

936
                return $prepared_type;
612✔
937
        }
938

939
        /**
940
         * Given a type name, returns the type or null if not found
941
         *
942
         * @param string $type_name The name of the Type to get from the registry
943
         *
944
         * @return ?TypeDef
945
         */
946
        public function get_type( string $type_name ) {
637✔
947
                $key = $this->format_key( $type_name );
637✔
948

949
                if ( isset( $this->type_loaders[ $key ] ) ) {
637✔
950
                        $type = $this->type_loaders[ $key ]();
612✔
951
                        /**
952
                         * Filter the type before it is loaded into the registry.
953
                         *
954
                         * @param ?TypeDef $type The type to load.
955
                         * @param string   $type_name The name of the type.
956
                         */
957
                        $this->types[ $key ] = apply_filters( 'graphql_get_type', $type, $type_name );
612✔
958
                        unset( $this->type_loaders[ $key ] );
612✔
959
                }
960

961
                return $this->types[ $key ] ?? null;
637✔
962
        }
963

964
        /**
965
         * Given a type name, determines if the type is already present in the Type Loader
966
         *
967
         * @param string $type_name The name of the type to check the registry for
968
         */
969
        public function has_type( string $type_name ): bool {
599✔
970
                return isset( $this->type_loaders[ $this->format_key( $type_name ) ] );
599✔
971
        }
972

973
        /**
974
         * Return the Types in the registry
975
         *
976
         * @return TypeDef[]
977
         */
978
        public function get_types(): array {
125✔
979
                // The full map of types is merged with eager types to support the
980
                // rename_graphql_type API.
981
                //
982
                // All of the types are closures, but eager Types are the full
983
                // Type definitions up front
984
                return array_filter( array_merge( $this->types, $this->get_eager_type_map() ) );
125✔
985
        }
986

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

1007
                return $prepared_fields;
611✔
1008
        }
1009

1010
        /**
1011
         * Prepare the field to be registered on the type
1012
         *
1013
         * @param string              $field_name   Friendly name of the field
1014
         * @param array<string,mixed> $field_config Config data about the field to prepare
1015
         * @param string              $type_name    Name of the type to prepare the field for
1016
         *
1017
         * @return ?array<string,mixed>
1018
         * @throws \Exception
1019
         */
1020
        protected function prepare_field( string $field_name, array $field_config, string $type_name ): ?array {
618✔
1021
                if ( ! isset( $field_config['name'] ) ) {
611✔
1022
                        $field_config['name'] = lcfirst( $field_name );
608✔
1023
                }
1024

1025
                if ( ! isset( $field_config['type'] ) ) {
611✔
1026
                        graphql_debug(
1✔
1027
                                sprintf(
1✔
1028
                                        /* translators: %s is the Field name. */
1029
                                        __( 'The registered field \'%s\' does not have a Type defined. Make sure to define a type for all fields.', 'wp-graphql' ),
1✔
1030
                                        $field_name
1✔
1031
                                ),
1✔
1032
                                [
1✔
1033
                                        'type'       => 'INVALID_FIELD_TYPE',
1✔
1034
                                        'type_name'  => $type_name,
1✔
1035
                                        'field_name' => $field_name,
1✔
1036
                                ]
1✔
1037
                        );
1✔
1038
                        return null;
1✔
1039
                }
1040

1041
                /**
1042
                 * If the type is a string, create a callable wrapper to get the type from
1043
                 * type registry. This preserves lazy-loading and prevents a bug where a type
1044
                 * has the same name as a function in the global scope (e.g., `header()`) and
1045
                 * is called since it passes `is_callable`.
1046
                 */
1047
                if ( is_string( $field_config['type'] ) ) {
611✔
1048
                        // Bail if the type is excluded from the Schema.
1049
                        if ( in_array( strtolower( $field_config['type'] ), $this->get_excluded_types(), true ) ) {
610✔
1050
                                return null;
4✔
1051
                        }
1052

1053
                        $field_config['type'] = function () use ( $field_config, $type_name ) {
618✔
1054
                                $type = $this->get_type( $field_config['type'] );
563✔
1055
                                if ( ! $type ) {
563✔
1056
                                        $message = sprintf(
1✔
1057
                                        /* 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. */
1058
                                                __( '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✔
1059
                                                $field_config['name'],
1✔
1060
                                                $type_name,
1✔
1061
                                                $field_config['type']
1✔
1062
                                        );
1✔
1063
                                        // 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,
1064
                                        // but now it will have a more helpful error message.
1065
                                        throw new Error( esc_html( $message ) );
1✔
1066
                                }
1067
                                return $type;
563✔
1068
                        };
618✔
1069
                }
1070

1071
                /**
1072
                 * If the type is an array, it contains type modifiers (e.g., "non_null").
1073
                 * Create a callable wrapper to preserve lazy-loading.
1074
                 */
1075
                if ( is_array( $field_config['type'] ) ) {
611✔
1076
                        // Bail if the type is excluded from the Schema.
1077
                        $unmodified_type_name = $this->get_unmodified_type_name( $field_config['type'] );
608✔
1078

1079
                        if ( empty( $unmodified_type_name ) || in_array( strtolower( $unmodified_type_name ), $this->get_excluded_types(), true ) ) {
608✔
1080
                                return null;
1✔
1081
                        }
1082

1083
                        $field_config['type'] = function () use ( $field_config ) {
613✔
1084
                                return $this->setup_type_modifiers( $field_config['type'] );
526✔
1085
                        };
613✔
1086
                }
1087

1088
                /**
1089
                 * If the field has arguments, each one must be prepared.
1090
                 */
1091
                if ( isset( $field_config['args'] ) && is_array( $field_config['args'] ) ) {
611✔
1092
                        foreach ( $field_config['args'] as $arg_name => $arg_config ) {
603✔
1093
                                $arg = $this->prepare_field( $arg_name, $arg_config, $type_name );
603✔
1094

1095
                                // Remove the arg if the field could not be prepared.
1096
                                if ( empty( $arg ) ) {
603✔
1097
                                        unset( $field_config['args'][ $arg_name ] );
1✔
1098
                                        continue;
1✔
1099
                                }
1100

1101
                                $field_config['args'][ $arg_name ] = $arg;
603✔
1102
                        }
1103
                }
1104

1105
                /**
1106
                 * If the field has no (remaining) valid arguments, unset the key.
1107
                 */
1108
                if ( empty( $field_config['args'] ) ) {
611✔
1109
                        unset( $field_config['args'] );
611✔
1110
                }
1111

1112
                $field_config = self::prepare_config_for_introspection( $field_config );
611✔
1113

1114
                return $field_config;
611✔
1115
        }
1116

1117
        /**
1118
         * Processes type modifiers (e.g., "non-null"). Loads types immediately, so do
1119
         * not call before types are ready to be loaded.
1120
         *
1121
         * @template WrappedType of array{non_null:mixed}|array{list_of:mixed}
1122
         * @param WrappedType|array<string,mixed>|string|\GraphQL\Type\Definition\Type $type The type to process.
1123
         *
1124
         * @return ($type is WrappedType ? \GraphQL\Type\Definition\Type : (array<string,mixed>|string|\GraphQL\Type\Definition\Type))
1125
         * @throws \Exception
1126
         */
1127
        public function setup_type_modifiers( $type ) {
526✔
1128
                if ( ! is_array( $type ) ) {
526✔
1129
                        return $type;
526✔
1130
                }
1131

1132
                if ( isset( $type['non_null'] ) ) {
526✔
1133
                        /** @var TypeDef inner_type */
1134
                        $inner_type = $this->setup_type_modifiers( $type['non_null'] );
524✔
1135
                        return $this->non_null( $inner_type );
524✔
1136
                }
1137

1138
                if ( isset( $type['list_of'] ) ) {
397✔
1139
                        /** @var TypeDef $inner_type */
1140
                        $inner_type = $this->setup_type_modifiers( $type['list_of'] );
397✔
1141
                        return $this->list_of( $inner_type );
397✔
1142
                }
1143

1144
                return $type;
×
1145
        }
1146

1147
        /**
1148
         * Wrapper for the register_field method to register multiple fields at once
1149
         *
1150
         * @param string                            $type_name Name of the type in the Type Registry to add the fields to
1151
         * @param array<string,array<string,mixed>> $fields    Fields to register
1152
         *
1153
         * @throws \Exception
1154
         */
1155
        public function register_fields( string $type_name, array $fields = [] ): void {
598✔
1156
                if ( ! empty( $fields ) ) {
598✔
1157
                        foreach ( $fields as $field_name => $config ) {
598✔
1158
                                if ( is_string( $field_name ) && ! empty( $config ) && is_array( $config ) ) {
598✔
1159
                                        $this->register_field( $type_name, $field_name, $config );
598✔
1160
                                }
1161
                        }
1162
                }
1163
        }
1164

1165
        /**
1166
         * Add a field to a Type in the Type Registry
1167
         *
1168
         * @param string              $type_name  Name of the type in the Type Registry to add the fields to
1169
         * @param string              $field_name Name of the field to add to the type
1170
         * @param array<string,mixed> $config     Info about the field to register to the type
1171
         *
1172
         * @throws \Exception
1173
         */
1174
        public function register_field( string $type_name, string $field_name, array $config ): void {
599✔
1175
                add_filter(
599✔
1176
                        'graphql_' . $type_name . '_fields',
599✔
1177
                        function ( $fields ) use ( $type_name, $field_name, $config ) {
599✔
1178

1179
                                // Whether the field should be allowed to have underscores in the field name
1180
                                $allow_field_underscores = isset( $config['allowFieldUnderscores'] ) && true === $config['allowFieldUnderscores'];
598✔
1181

1182
                                $field_name = Utils::format_field_name( $field_name, $allow_field_underscores );
598✔
1183

1184
                                if ( preg_match( '/^\d/', $field_name ) ) {
598✔
1185
                                        graphql_debug(
1✔
1186
                                                sprintf(
1✔
1187
                                                        // translators: %1$s is the field name, %2$s is the type name.
1188
                                                        __( 'The field \'%1$s\' on Type \'%2$s\' is invalid. Field names cannot start with a number.', 'wp-graphql' ),
1✔
1189
                                                        $field_name,
1✔
1190
                                                        $type_name
1✔
1191
                                                ),
1✔
1192
                                                [
1✔
1193
                                                        'type'       => 'INVALID_FIELD_NAME',
1✔
1194
                                                        'field_name' => $field_name,
1✔
1195
                                                        'type_name'  => $type_name,
1✔
1196
                                                ]
1✔
1197
                                        );
1✔
1198
                                        return $fields;
1✔
1199
                                }
1200

1201
                                // if a field has already been registered with the same name output a debug message
1202
                                if ( isset( $fields[ $field_name ] ) ) {
598✔
1203

1204
                                        // if the existing field is a connection type
1205
                                        // and the new field is also a connection type
1206
                                        // and the toType is the same for both
1207
                                        // then we can allow the duplicate field
1208
                                        if (
1209
                                                isset(
1210
                                                        $fields[ $field_name ]['isConnectionField'],
1✔
1211
                                                        $config['isConnectionField'],
1✔
1212
                                                        $fields[ $field_name ]['toType'],
1✔
1213
                                                        $config['toType'],
1✔
1214
                                                        $fields[ $field_name ]['connectionTypeName'],
1✔
1215
                                                        $config['connectionTypeName']
1✔
1216
                                                ) &&
1217
                                                $fields[ $field_name ]['toType'] === $config['toType'] &&
1✔
1218
                                                $fields[ $field_name ]['connectionTypeName'] === $config['connectionTypeName']
1✔
1219
                                        ) {
1220
                                                return $fields;
×
1221
                                        }
1222

1223
                                        graphql_debug(
1✔
1224
                                                sprintf(
1✔
1225
                                                        // translators: %1$s is the field name, %2$s is the type name.
1226
                                                        __( '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✔
1227
                                                        $field_name,
1✔
1228
                                                        $type_name
1✔
1229
                                                ),
1✔
1230
                                                [
1✔
1231
                                                        'type'            => 'DUPLICATE_FIELD',
1✔
1232
                                                        'field_name'      => $field_name,
1✔
1233
                                                        'type_name'       => $type_name,
1✔
1234
                                                        'existing_field'  => $fields[ $field_name ],
1✔
1235
                                                        'duplicate_field' => $config,
1✔
1236
                                                ]
1✔
1237
                                        );
1✔
1238
                                        return $fields;
1✔
1239
                                }
1240

1241
                                /**
1242
                                 * If the field returns a properly prepared field, add it the the field registry
1243
                                 */
1244
                                $field = $this->prepare_field( $field_name, $config, $type_name );
598✔
1245

1246
                                if ( ! empty( $field ) ) {
598✔
1247
                                        $fields[ $field_name ] = $field;
598✔
1248
                                }
1249

1250
                                return $fields;
598✔
1251
                        },
599✔
1252
                        10,
599✔
1253
                        1
599✔
1254
                );
599✔
1255
        }
1256

1257
        /**
1258
         * Remove a field from a type
1259
         *
1260
         * @param string $type_name  Name of the Type the field is registered to
1261
         * @param string $field_name Name of the field you would like to remove from the type
1262
         *
1263
         * @return void
1264
         */
1265
        public function deregister_field( string $type_name, string $field_name ) {
1✔
1266
                add_filter(
1✔
1267
                        'graphql_' . $type_name . '_fields',
1✔
1268
                        static function ( $fields ) use ( $field_name ) {
1✔
1269
                                if ( isset( $fields[ $field_name ] ) ) {
1✔
1270
                                        unset( $fields[ $field_name ] );
1✔
1271
                                }
1272

1273
                                return $fields;
1✔
1274
                        }
1✔
1275
                );
1✔
1276
        }
1277

1278
        /**
1279
         * Method to register a new connection in the Type registry
1280
         *
1281
         * @param array<string,mixed> $config The info about the connection being registered
1282
         *
1283
         * @throws \InvalidArgumentException
1284
         * @throws \Exception
1285
         */
1286
        public function register_connection( array $config ): void {
599✔
1287
                new WPConnectionType( $config, $this );
599✔
1288
        }
1289

1290
        /**
1291
         * Handles registration of a mutation to the Type registry
1292
         *
1293
         * @param string              $mutation_name Name of the mutation being registered
1294
         * @param array<string,mixed> $config        Info about the mutation being registered
1295
         *
1296
         * @throws \Exception
1297
         */
1298
        public function register_mutation( string $mutation_name, array $config ): void {
599✔
1299
                // Bail if the mutation has been excluded from the schema.
1300
                if ( in_array( strtolower( $mutation_name ), $this->get_excluded_mutations(), true ) ) {
599✔
1301
                        return;
2✔
1302
                }
1303

1304
                $config['name'] = $mutation_name;
599✔
1305
                new WPMutationType( $config, $this );
599✔
1306
        }
1307

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

1331
                                return $excluded_mutations;
2✔
1332
                        },
2✔
1333
                        10
2✔
1334
                );
2✔
1335
        }
1336

1337
        /**
1338
         * Removes a GraphQL connection from the schema.
1339
         *
1340
         * This works by preventing the connection from being registered in the first place.
1341
         *
1342
         * @uses 'graphql_excluded_connections' filter.
1343
         *
1344
         * @param string $connection_name The GraphQL connection name.
1345
         */
1346
        public function deregister_connection( string $connection_name ): void {
1✔
1347
                add_filter(
1✔
1348
                        'graphql_excluded_connections',
1✔
1349
                        static function ( $excluded_connections ) use ( $connection_name ) {
1✔
1350
                                $connection_name = strtolower( $connection_name );
1✔
1351

1352
                                if ( ! in_array( $connection_name, $excluded_connections, true ) ) {
1✔
1353
                                        $excluded_connections[] = $connection_name;
1✔
1354
                                }
1355

1356
                                return $excluded_connections;
1✔
1357
                        }
1✔
1358
                );
1✔
1359
        }
1360

1361
        /**
1362
         * Given a Type, this returns an instance of a NonNull of that type.
1363
         *
1364
         * @template T of \GraphQL\Type\Definition\NullableType&\GraphQL\Type\Definition\Type
1365
         * @param T|string $type The Type being wrapped.
1366
         */
1367
        public function non_null( $type ): \GraphQL\Type\Definition\NonNull {
524✔
1368
                if ( is_string( $type ) ) {
524✔
1369
                        $type_def = $this->get_type( $type );
524✔
1370

1371
                        /** @phpstan-var T&TypeDef $type_def */
1372
                        return Type::nonNull( $type_def );
524✔
1373
                }
1374

1375
                return Type::nonNull( $type );
373✔
1376
        }
1377

1378
        /**
1379
         * Given a Type, this returns an instance of a listOf of that type.
1380
         *
1381
         * @template T of \GraphQL\Type\Definition\Type
1382
         * @param T|string $type The Type being wrapped.
1383
         *
1384
         * @return \GraphQL\Type\Definition\ListOfType<\GraphQL\Type\Definition\Type>
1385
         */
1386
        public function list_of( $type ): \GraphQL\Type\Definition\ListOfType {
398✔
1387
                if ( is_string( $type ) ) {
398✔
1388
                        $resolved_type = $this->get_type( $type );
215✔
1389

1390
                        if ( is_null( $resolved_type ) ) {
215✔
1391
                                $resolved_type = Type::string();
×
1392
                        }
1393

1394
                        $type = $resolved_type;
215✔
1395
                }
1396

1397
                return Type::listOf( $type );
398✔
1398
        }
1399

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

1423
                        // Normalize the types to be lowercase, to avoid case-sensitivity issue when comparing.
1424
                        $this->excluded_types = ! empty( $excluded_types ) ? array_map( 'strtolower', $excluded_types ) : [];
599✔
1425
                }
1426

1427
                return $this->excluded_types;
611✔
1428
        }
1429

1430
        /**
1431
         * Get the list of GraphQL connections to exclude from the schema.
1432
         *
1433
         * Type names are normalized using `strtolower()`, to avoid case sensitivity issues.
1434
         *
1435
         * @return string[]
1436
         *
1437
         * @since 1.14.0
1438
         */
1439
        public function get_excluded_connections(): array {
599✔
1440
                if ( null === $this->excluded_connections ) {
599✔
1441
                        /**
1442
                         * Filter the list of GraphQL connections to excluded from the registry.
1443
                         *
1444
                         * @param string[] $excluded_connections The names of the GraphQL connections to exclude.
1445
                         *
1446
                         * @since 1.14.0
1447
                         */
1448
                        $excluded_connections = apply_filters( 'graphql_excluded_connections', [] );
599✔
1449

1450
                        // Normalize the types to be lowercase, to avoid case-sensitivity issue when comparing.
1451
                        $this->excluded_connections = ! empty( $excluded_connections ) ? array_map( 'strtolower', $excluded_connections ) : [];
599✔
1452
                }
1453

1454
                return $this->excluded_connections;
599✔
1455
        }
1456

1457
        /**
1458
         * Get the list of GraphQL mutation names to exclude from the schema.
1459
         *
1460
         * Mutation names are normalized using `strtolower()`, to avoid case sensitivity issues.
1461
         *
1462
         * @return string[]
1463
         * @since 1.14.0
1464
         */
1465
        public function get_excluded_mutations(): array {
599✔
1466
                if ( null === $this->excluded_mutations ) {
599✔
1467
                        /**
1468
                         * Filter the list of GraphQL mutations to excluded from the registry.
1469
                         *
1470
                         * @param string[] $excluded_mutations The names of the GraphQL mutations to exclude.
1471
                         *
1472
                         * @since 1.14.0
1473
                         */
1474
                        $excluded_mutations = apply_filters( 'graphql_excluded_mutations', [] );
599✔
1475

1476
                        // Normalize the types to be lowercase, to avoid case-sensitivity issue when comparing.
1477
                        $this->excluded_mutations = ! empty( $excluded_mutations ) ? array_map( 'strtolower', $excluded_mutations ) : [];
599✔
1478
                }
1479

1480
                return $this->excluded_mutations;
599✔
1481
        }
1482

1483
        /**
1484
         * Gets the actual type name, stripped of possible NonNull and ListOf wrappers.
1485
         *
1486
         * Returns an empty string if the type modifiers are malformed.
1487
         *
1488
         * @param string|array<string|int,mixed> $type The (possibly-wrapped) type name.
1489
         */
1490
        protected function get_unmodified_type_name( $type ): string {
608✔
1491
                if ( ! is_array( $type ) ) {
608✔
1492
                        return $type;
608✔
1493
                }
1494

1495
                $type = array_values( $type )[0] ?? '';
608✔
1496

1497
                return $this->get_unmodified_type_name( $type );
608✔
1498
        }
1499
}
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

© 2025 Coveralls, Inc