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

wp-graphql / wp-graphql / 14841806285

05 May 2025 04:57PM UTC coverage: 84.234% (-0.05%) from 84.287%
14841806285

push

github

actions-user
chore: update changeset for PR #3374

15900 of 18876 relevant lines covered (84.23%)

257.2 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\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
 * @package WPGraphQL\Registry
145
 */
146
class TypeRegistry {
147

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

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

162
        /**
163
         * The loaders needed to register types
164
         *
165
         * @var array<string,callable():(mixed|array<string,mixed>|\GraphQL\Type\Definition\Type|null)>
166
         */
167
        protected $type_loaders;
168

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

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

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

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

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

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

228
        /**
229
         * Returns the eager type map, an array of Type definitions for Types that
230
         * are not directly referenced in the schema.
231
         *
232
         * Types can add "eagerlyLoadType => true" when being registered to be included
233
         * in the eager_type_map.
234
         *
235
         * @return array<string,mixed>
236
         */
237
        protected function get_eager_type_map() {
593✔
238
                if ( ! empty( $this->eager_type_map ) ) {
593✔
239
                        return array_map(
69✔
240
                                function ( $type_name ) {
69✔
241
                                        return $this->get_type( $type_name );
69✔
242
                                },
69✔
243
                                $this->eager_type_map
69✔
244
                        );
69✔
245
                }
246

247
                return [];
524✔
248
        }
249

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

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

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

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

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

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

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

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

384
                DateInput::register_type();
593✔
385
                DateQueryInput::register_type();
593✔
386
                PostObjectsConnectionOrderbyInput::register_type();
593✔
387
                UsersConnectionOrderbyInput::register_type();
593✔
388

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

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

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

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

428
                foreach ( $allowed_post_types as $post_type_object ) {
593✔
429
                        PostObject::register_types( $post_type_object );
593✔
430

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

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

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

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

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

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

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

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

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

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

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

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

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

604
                                if ( ! $type_name ) {
593✔
605
                                        continue;
×
606
                                }
607

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

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

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

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

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

674
                if ( ! is_array( $connections ) ) {
593✔
675
                        return;
×
676
                }
677

678
                foreach ( $connections as $field_name => $connection_config ) {
593✔
679
                        if ( ! is_array( $connection_config ) ) {
593✔
680
                                continue;
×
681
                        }
682

683
                        $connection_config['fromType']      = $config['name'];
593✔
684
                        $connection_config['fromFieldName'] = $field_name;
593✔
685
                        register_graphql_connection( $connection_config );
593✔
686
                }
687
        }
688

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

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

740
                /**
741
                 * Register any connections that were passed through the Type config
742
                 */
743
                if ( is_array( $config ) && isset( $config['connections'] ) ) {
593✔
744
                        $config['name'] = ucfirst( $type_name );
593✔
745
                        $this->register_connections_from_config( $config );
593✔
746
                }
747

748
                $this->type_loaders[ $this->format_key( $type_name ) ] = function () use ( $type_name, $config ) {
606✔
749
                        return $this->prepare_type( $type_name, $config );
606✔
750
                };
606✔
751

752
                if ( WPGraphQL::is_introspection_query() && is_array( $config ) && isset( $config['eagerlyLoadType'] ) && true === $config['eagerlyLoadType'] && ! isset( $this->eager_type_map[ $this->format_key( $type_name ) ] ) ) {
593✔
753
                        $this->eager_type_map[ $this->format_key( $type_name ) ] = $this->format_key( $type_name );
69✔
754
                }
755
        }
756

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

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

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

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

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

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

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

841
                return self::$introspection_keys;
607✔
842
        }
843

844
        /**
845
         * Prepare the config for introspection. This is used to resolve callable values for description and deprecationReason for
846
         * introspection queries.
847
         *
848
         * @template T of array<string,mixed>
849
         *
850
         * @param array<string,mixed> $config The config to prepare.
851
         * @phpstan-param T $config
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 {
607✔
859

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

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

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

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

877
                return $config;
607✔
878
        }
879

880
        /**
881
         * Prepare the type for registration.
882
         *
883
         * @param string                                            $type_name The name of the type to prepare
884
         * @param array<string,mixed>|\GraphQL\Type\Definition\Type $config    The config for the type
885
         *
886
         * @phpstan-param WPEnumTypeConfig|WPScalarConfig|\GraphQL\Type\Definition\Type|array<string,mixed> $config
887
         *
888
         * @return \GraphQL\Type\Definition\Type|null The prepared type
889
         */
890
        protected function prepare_type( string $type_name, $config ) {
606✔
891
                if ( ! is_array( $config ) ) {
606✔
892
                        return $config;
594✔
893
                }
894

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

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

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

928
                return $prepared_type;
606✔
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 mixed|array<string,mixed>|\GraphQL\Type\Definition\Type|null
937
         */
938
        public function get_type( string $type_name ) {
631✔
939
                $key = $this->format_key( $type_name );
631✔
940

941
                if ( isset( $this->type_loaders[ $key ] ) ) {
631✔
942
                        $type                = $this->type_loaders[ $key ]();
606✔
943
                        $this->types[ $key ] = apply_filters( 'graphql_get_type', $type, $type_name );
606✔
944
                        unset( $this->type_loaders[ $key ] );
606✔
945
                }
946

947
                return $this->types[ $key ] ?? null;
631✔
948
        }
949

950
        /**
951
         * Given a type name, determines if the type is already present in the Type Loader
952
         *
953
         * @param string $type_name The name of the type to check the registry for
954
         */
955
        public function has_type( string $type_name ): bool {
593✔
956
                return isset( $this->type_loaders[ $this->format_key( $type_name ) ] );
593✔
957
        }
958

959
        /**
960
         * Return the Types in the registry
961
         *
962
         * @return array<string,mixed>
963
         */
964
        public function get_types(): array {
593✔
965

966
                // The full map of types is merged with eager types to support the
967
                // rename_graphql_type API.
968
                //
969
                // All of the types are closures, but eager Types are the full
970
                // Type definitions up front
971
                return array_merge( $this->types, $this->get_eager_type_map() );
593✔
972
        }
973

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

994
                return $prepared_fields;
605✔
995
        }
996

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

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

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

1040
                        $field_config['type'] = function () use ( $field_config, $type_name ) {
612✔
1041
                                $type = $this->get_type( $field_config['type'] );
557✔
1042
                                if ( ! $type ) {
557✔
1043
                                        $message = sprintf(
1✔
1044
                                        /* 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. */
1045
                                                __( '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✔
1046
                                                $field_config['name'],
1✔
1047
                                                $type_name,
1✔
1048
                                                $field_config['type']
1✔
1049
                                        );
1✔
1050
                                        // 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,
1051
                                        // but now it will have a more helpful error message.
1052
                                        throw new Error( esc_html( $message ) );
1✔
1053
                                }
1054
                                return $type;
557✔
1055
                        };
612✔
1056
                }
1057

1058
                /**
1059
                 * If the type is an array, it contains type modifiers (e.g., "non_null").
1060
                 * Create a callable wrapper to preserve lazy-loading.
1061
                 */
1062
                if ( is_array( $field_config['type'] ) ) {
605✔
1063
                        // Bail if the type is excluded from the Schema.
1064
                        $unmodified_type_name = $this->get_unmodified_type_name( $field_config['type'] );
603✔
1065

1066
                        if ( empty( $unmodified_type_name ) || in_array( strtolower( $unmodified_type_name ), $this->get_excluded_types(), true ) ) {
603✔
1067
                                return null;
1✔
1068
                        }
1069

1070
                        $field_config['type'] = function () use ( $field_config ) {
608✔
1071
                                return $this->setup_type_modifiers( $field_config['type'] );
522✔
1072
                        };
608✔
1073
                }
1074

1075
                /**
1076
                 * If the field has arguments, each one must be prepared.
1077
                 */
1078
                if ( isset( $field_config['args'] ) && is_array( $field_config['args'] ) ) {
605✔
1079
                        foreach ( $field_config['args'] as $arg_name => $arg_config ) {
598✔
1080
                                $arg = $this->prepare_field( $arg_name, $arg_config, $type_name );
598✔
1081

1082
                                // Remove the arg if the field could not be prepared.
1083
                                if ( empty( $arg ) ) {
598✔
1084
                                        unset( $field_config['args'][ $arg_name ] );
1✔
1085
                                        continue;
1✔
1086
                                }
1087

1088
                                $field_config['args'][ $arg_name ] = $arg;
598✔
1089
                        }
1090
                }
1091

1092
                /**
1093
                 * If the field has no (remaining) valid arguments, unset the key.
1094
                 */
1095
                if ( empty( $field_config['args'] ) ) {
605✔
1096
                        unset( $field_config['args'] );
605✔
1097
                }
1098

1099
                $field_config = self::prepare_config_for_introspection( $field_config );
605✔
1100

1101
                return $field_config;
605✔
1102
        }
1103

1104
        /**
1105
         * Processes type modifiers (e.g., "non-null"). Loads types immediately, so do
1106
         * not call before types are ready to be loaded.
1107
         *
1108
         * @param \GraphQL\Type\Definition\Type|string|array<string,mixed> $type The type definition to process.
1109
         *
1110
         * @return \GraphQL\Type\Definition\Type|string|array<string,mixed>
1111
         * @throws \Exception
1112
         */
1113
        public function setup_type_modifiers( $type ) {
522✔
1114
                if ( ! is_array( $type ) ) {
522✔
1115
                        return $type;
522✔
1116
                }
1117

1118
                if ( isset( $type['non_null'] ) ) {
522✔
1119
                        /** @var \GraphQL\Type\Definition\Type $inner_type */
1120
                        $inner_type = $this->setup_type_modifiers( $type['non_null'] );
520✔
1121

1122
                        return $this->non_null( $inner_type );
520✔
1123
                }
1124

1125
                if ( isset( $type['list_of'] ) ) {
395✔
1126
                        /** @var \GraphQL\Type\Definition\Type $inner_type */
1127
                        $inner_type = $this->setup_type_modifiers( $type['list_of'] );
395✔
1128
                        return $this->list_of( $inner_type );
395✔
1129
                }
1130

1131
                return $type;
×
1132
        }
1133

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

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

1166
                                // Whether the field should be allowed to have underscores in the field name
1167
                                $allow_field_underscores = isset( $config['allowFieldUnderscores'] ) && true === $config['allowFieldUnderscores'];
593✔
1168

1169
                                $field_name = Utils::format_field_name( $field_name, $allow_field_underscores );
593✔
1170

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

1188
                                // if a field has already been registered with the same name output a debug message
1189
                                if ( isset( $fields[ $field_name ] ) ) {
593✔
1190

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

1210
                                        graphql_debug(
1✔
1211
                                                sprintf(
1✔
1212
                                                        // translators: %1$s is the field name, %2$s is the type name.
1213
                                                        __( '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✔
1214
                                                        $field_name,
1✔
1215
                                                        $type_name
1✔
1216
                                                ),
1✔
1217
                                                [
1✔
1218
                                                        'type'            => 'DUPLICATE_FIELD',
1✔
1219
                                                        'field_name'      => $field_name,
1✔
1220
                                                        'type_name'       => $type_name,
1✔
1221
                                                        'existing_field'  => $fields[ $field_name ],
1✔
1222
                                                        'duplicate_field' => $config,
1✔
1223
                                                ]
1✔
1224
                                        );
1✔
1225
                                        return $fields;
1✔
1226
                                }
1227

1228
                                /**
1229
                                 * If the field returns a properly prepared field, add it the the field registry
1230
                                 */
1231
                                $field = $this->prepare_field( $field_name, $config, $type_name );
593✔
1232

1233
                                if ( ! empty( $field ) ) {
593✔
1234
                                        $fields[ $field_name ] = $field;
593✔
1235
                                }
1236

1237
                                return $fields;
593✔
1238
                        },
593✔
1239
                        10,
593✔
1240
                        1
593✔
1241
                );
593✔
1242
        }
1243

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

1260
                                return $fields;
1✔
1261
                        }
1✔
1262
                );
1✔
1263
        }
1264

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

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

1291
                $config['name'] = $mutation_name;
593✔
1292
                new WPMutationType( $config, $this );
593✔
1293
        }
1294

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

1318
                                return $excluded_mutations;
2✔
1319
                        },
2✔
1320
                        10
2✔
1321
                );
2✔
1322
        }
1323

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

1339
                                if ( ! in_array( $connection_name, $excluded_connections, true ) ) {
1✔
1340
                                        $excluded_connections[] = $connection_name;
1✔
1341
                                }
1342

1343
                                return $excluded_connections;
1✔
1344
                        }
1✔
1345
                );
1✔
1346
        }
1347

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

1362
                return Type::nonNull( $type );
372✔
1363
        }
1364

1365
        /**
1366
         * Given a Type, this returns an instance of a listOf of that type.
1367
         *
1368
         * @param \GraphQL\Type\Definition\Type|string $type The Type being wrapped.
1369
         *
1370
         * @phpstan-template T of \GraphQL\Type\Definition\Type
1371
         * @phpstan-param T|string $type The Type being wrapped.
1372
         * @phpstan-return \GraphQL\Type\Definition\ListOfType<T>
1373
         */
1374
        public function list_of( $type ): \GraphQL\Type\Definition\ListOfType {
396✔
1375
                if ( is_string( $type ) ) {
396✔
1376
                        $resolved_type = $this->get_type( $type );
214✔
1377

1378
                        if ( is_null( $resolved_type ) ) {
214✔
1379
                                $resolved_type = Type::string();
×
1380
                        }
1381

1382
                        /** @phpstan-var T $resolved_type */
1383
                        $type = $resolved_type;
214✔
1384
                }
1385

1386
                return Type::listOf( $type );
396✔
1387
        }
1388

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

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

1416
                return $this->excluded_types;
605✔
1417
        }
1418

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

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

1443
                return $this->excluded_connections;
593✔
1444
        }
1445

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

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

1469
                return $this->excluded_mutations;
593✔
1470
        }
1471

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

1484
                $type = array_values( $type )[0] ?? '';
603✔
1485

1486
                return $this->get_unmodified_type_name( $type );
603✔
1487
        }
1488
}
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