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

wp-graphql / wp-graphql / 13316763745

13 Feb 2025 08:45PM UTC coverage: 82.712% (-0.3%) from 83.023%
13316763745

push

github

web-flow
Merge pull request #3307 from wp-graphql/release/v2.0.0

release: v2.0.0

195 of 270 new or added lines in 20 files covered. (72.22%)

180 existing lines in 42 files now uncovered.

13836 of 16728 relevant lines covered (82.71%)

299.8 hits per line

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

88.89
/src/Utils/QueryAnalyzer.php
1
<?php
2

3
namespace WPGraphQL\Utils;
4

5
use GraphQL\Error\SyntaxError;
6
use GraphQL\Language\Parser;
7
use GraphQL\Language\Visitor;
8
use GraphQL\Server\OperationParams;
9
use GraphQL\Type\Definition\FieldDefinition;
10
use GraphQL\Type\Definition\InterfaceType;
11
use GraphQL\Type\Definition\ListOfType;
12
use GraphQL\Type\Definition\NonNull;
13
use GraphQL\Type\Definition\ObjectType;
14
use GraphQL\Type\Definition\Type;
15
use GraphQL\Type\Schema;
16
use GraphQL\Utils\TypeInfo;
17
use WPGraphQL;
18
use WPGraphQL\Request;
19
use WPGraphQL\WPSchema;
20

21
/**
22
 * This class is used to identify "keys" relevant to the GraphQL Request.
23
 *
24
 * These keys can be used to identify common patterns across documents.
25
 *
26
 * A common use case would be for caching a GraphQL request and tagging the cached
27
 * object with these keys, then later using these keys to evict the cached
28
 * document.
29
 *
30
 * These keys can also be used by loggers to identify patterns, etc.
31
 */
32
class QueryAnalyzer {
33

34
        /**
35
         * @var \WPGraphQL\WPSchema|null
36
         */
37
        protected $schema;
38

39
        /**
40
         * Types that are referenced in the query
41
         *
42
         * @var string[]
43
         */
44
        protected $queried_types = [];
45

46
        /**
47
         * @var string
48
         */
49
        protected $root_operation = '';
50

51
        /**
52
         * Models that are referenced in the query
53
         *
54
         * @var string[]
55
         */
56
        protected $models = [];
57

58
        /**
59
         * Types in the query that are lists
60
         *
61
         * @var string[]
62
         */
63
        protected $list_types = [];
64

65
        /**
66
         * @var string[]|int[]
67
         */
68
        protected $runtime_nodes = [];
69

70
        /**
71
         * @var array<string,int[]|string[]>
72
         */
73
        protected $runtime_nodes_by_type = [];
74

75
        /**
76
         * @var string
77
         */
78
        protected $query_id;
79

80
        /**
81
         * @var \WPGraphQL\Request
82
         */
83
        protected $request;
84

85
        /**
86
         * @var Int The character length limit for headers
87
         */
88
        protected $header_length_limit;
89

90
        /**
91
         * @var string The keys that were skipped from being returned in the X-GraphQL-Keys header.
92
         */
93
        protected $skipped_keys = '';
94

95
        /**
96
         * @var string[] The GraphQL keys to return in the X-GraphQL-Keys header.
97
         */
98
        protected $graphql_keys = [];
99

100
        /**
101
         * @var mixed[] Track all Types that were queried as a list
102
         */
103
        protected $queried_list_types = [];
104

105
        /**
106
         * @var ?bool Whether the Query Analyzer is enabled for the specific or not.
107
         */
108
        protected $is_enabled_for_query;
109

110
        /**
111
         * @param \WPGraphQL\Request $request The GraphQL request being executed
112
         */
113
        public function __construct( Request $request ) {
754✔
114
                $this->request       = $request;
754✔
115
                $this->runtime_nodes = [];
754✔
116
                $this->models        = [];
754✔
117
                $this->list_types    = [];
754✔
118
                $this->queried_types = [];
754✔
119
        }
120

121
        /**
122
         * Checks whether the Query Analyzer is enabled on the site.
123
         *
124
         * @uses `graphql_query_analyzer_enabled` filter.
125
         */
126
        public static function is_enabled(): bool {
754✔
127
                $is_debug_enabled = WPGraphQL::debug();
754✔
128

129
                // The query analyzer is enabled if WPGraphQL Debugging is enabled
130
                $query_analyzer_enabled = $is_debug_enabled;
754✔
131

132
                // If WPGraphQL Debugging is not enabled, check the setting
133
                if ( ! $is_debug_enabled ) {
754✔
134
                        $query_analyzer_enabled = get_graphql_setting( 'query_analyzer_enabled', 'off' );
2✔
135
                        $query_analyzer_enabled = 'on' === $query_analyzer_enabled;
2✔
136
                }
137

138
                /**
139
                 * Filters whether to analyze queries for all GraphQL requests.
140
                 *
141
                 * @param bool $should_track_types Whether to analyze queries or not. Defaults to `true` if GraphQL Debugging is enabled, otherwise `false`.
142
                 */
143
                return apply_filters( 'graphql_should_analyze_queries', $query_analyzer_enabled );
754✔
144
        }
145

146
        /**
147
         * Get the GraphQL Schema.
148
         * If the schema is not set, it will be set.
149
         *
150
         * @throws \Exception
151
         */
152
        public function get_schema(): ?WPSchema {
745✔
153
                if ( ! $this->schema ) {
745✔
154
                        $this->schema = WPGraphQL::get_schema();
745✔
155
                }
156

157
                return $this->schema;
745✔
158
        }
159

160
        /**
161
         * Gets the request object.
162
         */
163
        public function get_request(): Request {
754✔
164
                return $this->request;
754✔
165
        }
166

167
        /**
168
         * Checks if the Query Analyzer is enabled.
169
         *
170
         * @uses `graphql_should_analyze_queries` filter.
171
         */
172
        public function is_enabled_for_query(): bool {
754✔
173
                if ( ! isset( $this->is_enabled_for_query ) ) {
754✔
174
                        $is_enabled = self::is_enabled();
754✔
175

176
                        /**
177
                         * Filters whether to analyze queries or for a specific GraphQL request.
178
                         *
179
                         * @param bool          $should_analyze_queries Whether to analyze queries for the current request. Defaults to the value of `graphql_query_analyzer_enabled` filter.
180
                         * @param \WPGraphQL\Request $request               The GraphQL request being executed
181
                         */
182
                        $should_analyze_queries = apply_filters( 'graphql_should_analyze_query', $is_enabled, $this->get_request() );
754✔
183

184
                        $this->is_enabled_for_query = true === $should_analyze_queries;
754✔
185
                }
186

187
                return $this->is_enabled_for_query;
754✔
188
        }
189

190
        /**
191
         * Initialize the QueryAnalyzer.
192
         */
193
        public function init(): void {
754✔
194
                $should_analyze_queries = $this->is_enabled_for_query();
754✔
195

196
                // If query analyzer is disabled, bail
197
                if ( true !== $should_analyze_queries ) {
754✔
198
                        return;
2✔
199
                }
200

201
                $this->graphql_keys = [];
753✔
202
                $this->skipped_keys = '';
753✔
203

204
                /**
205
                 * Many clients have an 8k (8192 characters) header length cap.
206
                 *
207
                 * This is the total for ALL headers, not just individual headers.
208
                 *
209
                 * SEE: https://nodejs.org/en/blog/vulnerability/november-2018-security-releases/#denial-of-service-with-large-http-headers-cve-2018-12121
210
                 *
211
                 * In order to respect this, we have a default limit of 4000 characters for the X-GraphQL-Keys header
212
                 * to allow for other headers to total up to 8k.
213
                 *
214
                 * This value can be filtered to be increased or decreased.
215
                 *
216
                 * If you see "Parse Error: Header overflow" errors in your client, you might want to decrease this value.
217
                 *
218
                 * On the other hand, if you've increased your allowed header length in your client
219
                 * (i.e. https://github.com/wp-graphql/wp-graphql/issues/2535#issuecomment-1262499064) then you might want to increase this so that keys are not truncated.
220
                 *
221
                 * @param int $header_length_limit The max limit in (binary) bytes headers should be. Anything longer will be truncated.
222
                 */
223
                $this->header_length_limit = apply_filters( 'graphql_query_analyzer_header_length_limit', 4000 );
753✔
224

225
                // track keys related to the query
226
                add_action( 'do_graphql_request', [ $this, 'determine_graphql_keys' ], 10, 4 );
753✔
227

228
                // Track models loaded during execution
229
                add_filter( 'graphql_dataloader_get_model', [ $this, 'track_nodes' ], 10, 1 );
753✔
230

231
                // Expose query analyzer data in extensions
232
                add_filter(
753✔
233
                        'graphql_request_results',
753✔
234
                        [
753✔
235
                                $this,
753✔
236
                                'show_query_analyzer_in_extensions',
753✔
237
                        ],
753✔
238
                        10,
753✔
239
                        5
753✔
240
                );
753✔
241
        }
242

243
        /**
244
         * Determine the keys associated with the GraphQL document being executed
245
         *
246
         * @param ?string                         $query     The GraphQL query
247
         * @param ?string                         $operation The name of the operation
248
         * @param ?array<string,mixed>            $variables Variables to be passed to your GraphQL request
249
         * @param \GraphQL\Server\OperationParams $params The Operation Params. This includes any extra params,
250
         * such as extensions or any other modifications to the request body
251
         *
252
         * @throws \Exception
253
         */
254
        public function determine_graphql_keys( ?string $query, ?string $operation, ?array $variables, OperationParams $params ): void {
746✔
255

256
                // @todo: support for QueryID?
257

258
                // if the query is empty, get it from the request params
259
                if ( empty( $query ) ) {
746✔
260
                        $query = $this->request->params->query ?: null;
1✔
261
                }
262

263
                if ( empty( $query ) ) {
746✔
264
                        return;
1✔
265
                }
266

267
                $query_id       = Utils::get_query_id( $query );
745✔
268
                $this->query_id = $query_id ?: uniqid( 'gql:', true );
745✔
269

270
                // if there's a query (either saved or part of the request params)
271
                // get the GraphQL Types being asked for by the query
272
                $this->list_types    = $this->set_list_types( $this->get_schema(), $query );
745✔
273
                $this->queried_types = $this->set_query_types( $this->get_schema(), $query );
745✔
274
                $this->models        = $this->set_query_models( $this->get_schema(), $query );
745✔
275

276
                /**
277
                 * @param \WPGraphQL\Utils\QueryAnalyzer $query_analyzer The instance of the query analyzer
278
                 * @param string        $query          The query string being executed
279
                 */
280
                do_action( 'graphql_determine_graphql_keys', $this, $query );
745✔
281
        }
282

283
        /**
284
         * @return string[]
285
         */
286
        public function get_list_types(): array {
746✔
287
                return array_unique( $this->list_types );
746✔
288
        }
289

290
        /**
291
         * @return string[]
292
         */
293
        public function get_query_types(): array {
1✔
294
                return array_unique( $this->queried_types );
1✔
295
        }
296

297
        /**
298
         * @return string[]
299
         */
300
        public function get_query_models(): array {
498✔
301
                return array_unique( $this->models );
498✔
302
        }
303

304
        /**
305
         * @return string[]|int[]
306
         */
307
        public function get_runtime_nodes(): array {
746✔
308
                /**
309
                 * @param string[]|int[] $runtime_nodes Nodes that were resolved during execution
310
                 */
311
                $runtime_nodes = apply_filters( 'graphql_query_analyzer_get_runtime_nodes', $this->runtime_nodes );
746✔
312

313
                return array_unique( $runtime_nodes );
746✔
314
        }
315

316
        /**
317
         * Get the root operation of the query.
318
         */
319
        public function get_root_operation(): string {
746✔
320
                return $this->root_operation;
746✔
321
        }
322

323
        /**
324
         * Returns the operation name of the query, if there is one
325
         */
326
        public function get_operation_name(): ?string {
746✔
327
                $operation_name = ! empty( $this->request->params->operation ) ? $this->request->params->operation : null;
746✔
328

329
                if ( empty( $operation_name ) ) {
746✔
330

331
                        // If the query is not set on the params, return null
332
                        if ( ! isset( $this->request->params->query ) ) {
746✔
333
                                return null;
2✔
334
                        }
335

336
                        try {
337
                                $ast            = Parser::parse( $this->request->params->query );
745✔
338
                                $operation_name = ! empty( $ast->definitions[0]->name->value ) ? $ast->definitions[0]->name->value : null;
745✔
339
                        } catch ( SyntaxError $error ) {
×
340
                                return null;
×
341
                        }
342
                }
343

344
                return ! empty( $operation_name ) ? 'operation:' . $operation_name : null;
745✔
345
        }
346

347
        /**
348
         * Get the query id.
349
         */
350
        public function get_query_id(): ?string {
746✔
351
                return $this->query_id;
746✔
352
        }
353

354
        /**
355
         * @param \GraphQL\Type\Definition\Type            $type The Type of field
356
         * @param \GraphQL\Type\Definition\FieldDefinition $field_def The field definition the type is for
357
         * @param mixed                                    $parent_type The Parent Type
358
         * @param bool                                     $is_list_type Whether the field is a list type field
359
         *
360
         * @return  \GraphQL\Type\Definition\Type|String|null
361
         */
362
        public static function get_wrapped_field_type( Type $type, FieldDefinition $field_def, $parent_type, bool $is_list_type = false ) {
742✔
363
                if ( ! isset( $parent_type->name ) || 'RootQuery' !== $parent_type->name ) {
742✔
364
                        return null;
736✔
365
                }
366

367
                if ( $type instanceof NonNull || $type instanceof ListOfType ) {
632✔
368
                        if ( $type instanceof ListOfType && ! empty( $parent_type->name ) ) {
14✔
369
                                $is_list_type = true;
3✔
370
                        }
371

372
                        return self::get_wrapped_field_type( $type->getWrappedType(), $field_def, $parent_type, $is_list_type );
14✔
373
                }
374

375
                // Determine if we're dealing with a connection
376
                if ( $type instanceof ObjectType || $type instanceof InterfaceType ) {
632✔
377
                        $interfaces      = $type->getInterfaces();
626✔
378
                        $interface_names = ! empty( $interfaces ) ? array_map(
626✔
379
                                static function ( InterfaceType $interface_obj ) {
626✔
380
                                        return $interface_obj->name;
530✔
381
                                },
626✔
382
                                $interfaces
626✔
383
                        ) : [];
626✔
384

385
                        if ( array_key_exists( 'Connection', $interface_names ) ) {
626✔
386
                                if ( isset( $field_def->config['fromType'] ) && ( 'rootquery' !== strtolower( $field_def->config['fromType'] ) ) ) {
288✔
387
                                        return null;
×
388
                                }
389

390
                                $to_type = $field_def->config['toType'] ?? null;
288✔
391
                                if ( empty( $to_type ) ) {
288✔
392
                                        return null;
×
393
                                }
394

395
                                return $to_type;
288✔
396
                        }
397

398
                        if ( ! $is_list_type ) {
356✔
399
                                return null;
353✔
400
                        }
401

402
                        return $type;
3✔
403
                }
404

405
                return null;
10✔
406
        }
407

408
        /**
409
         * Given the Schema and a query string, return a list of GraphQL Types that are being asked for
410
         * by the query.
411
         *
412
         * @param ?\GraphQL\Type\Schema $schema The WPGraphQL Schema
413
         * @param ?string               $query  The query string
414
         *
415
         * @return string[]
416
         * @throws \GraphQL\Error\SyntaxError|\Exception
417
         */
418
        public function set_list_types( ?Schema $schema, ?string $query ): array {
745✔
419

420
                /**
421
                 * @param string[]|null         $null   Default value for the filter
422
                 * @param ?\GraphQL\Type\Schema $schema The WPGraphQL Schema for the current request
423
                 * @param ?string               $query  The query string being requested
424
                 */
425
                $null               = null;
745✔
426
                $pre_get_list_types = apply_filters( 'graphql_pre_query_analyzer_get_list_types', $null, $schema, $query );
745✔
427

428
                if ( null !== $pre_get_list_types ) {
745✔
429
                        return $pre_get_list_types;
×
430
                }
431

432
                if ( empty( $query ) || null === $schema ) {
745✔
433
                        return [];
×
434
                }
435

436
                try {
437
                        $ast = Parser::parse( $query );
745✔
438
                } catch ( SyntaxError $error ) {
×
439
                        return [];
×
440
                }
441

442
                $type_map  = [];
745✔
443
                $type_info = new TypeInfo( $schema );
745✔
444

445
                Visitor::visit(
745✔
446
                        $ast,
745✔
447
                        Visitor::visitWithTypeInfo(
745✔
448
                                $type_info,
745✔
449
                                [
745✔
450
                                        'enter' => static function ( $node ) use ( $type_info, &$type_map, $schema ) {
745✔
451
                                                $parent_type = $type_info->getParentType();
745✔
452

453
                                                if ( 'Field' !== $node->kind ) {
745✔
454
                                                        Visitor::skipNode();
745✔
455
                                                }
456

457
                                                $type_info->enter( $node );
745✔
458
                                                $field_def = $type_info->getFieldDef();
745✔
459

460
                                                if ( ! $field_def instanceof FieldDefinition ) {
745✔
461
                                                        return;
745✔
462
                                                }
463

464
                                                // Determine the wrapped type, which also determines if it's a listOf
465
                                                $field_type = $field_def->getType();
742✔
466
                                                $field_type = self::get_wrapped_field_type( $field_type, $field_def, $parent_type );
742✔
467

468
                                                if ( null === $field_type ) {
742✔
469
                                                        return;
742✔
470
                                                }
471

472
                                                if ( ! empty( $field_type ) && is_string( $field_type ) ) {
291✔
473
                                                        $field_type = $schema->getType( ucfirst( $field_type ) );
288✔
474
                                                }
475

476
                                                if ( ! $field_type ) {
291✔
NEW
477
                                                        return;
×
478
                                                }
479

480
                                                $field_type = $schema->getType( $field_type );
291✔
481

482
                                                if ( ! $field_type instanceof ObjectType && ! $field_type instanceof InterfaceType ) {
291✔
483
                                                        return;
2✔
484
                                                }
485

486
                                                // If the type being queried is an interface (i.e. ContentNode) the publishing a new
487
                                                // item of any of the possible types (post, page, etc) should invalidate
488
                                                // this query, so we need to tag this query with `list:$possible_type` for each possible type
489
                                                if ( $field_type instanceof InterfaceType ) {
289✔
490
                                                        $possible_types = $schema->getPossibleTypes( $field_type );
12✔
491
                                                        if ( ! empty( $possible_types ) ) {
12✔
492
                                                                foreach ( $possible_types as $possible_type ) {
10✔
493
                                                                        $type_map[] = 'list:' . strtolower( $possible_type );
10✔
494
                                                                }
495
                                                        }
496
                                                } else {
497
                                                        $type_map[] = 'list:' . strtolower( $field_type );
279✔
498
                                                }
499
                                        },
745✔
500
                                        'leave' => static function ( $node ) use ( $type_info ): void {
745✔
501
                                                $type_info->leave( $node );
745✔
502
                                        },
745✔
503
                                ]
745✔
504
                        )
745✔
505
                );
745✔
506

507
                $map = array_values( array_unique( $type_map ) );
745✔
508

509
                return apply_filters( 'graphql_cache_collection_get_list_types', $map, $schema, $query, $type_info );
745✔
510
        }
511

512
        /**
513
         * Given the Schema and a query string, return a list of GraphQL Types that are being asked for
514
         * by the query.
515
         *
516
         * @param ?\GraphQL\Type\Schema $schema The WPGraphQL Schema
517
         * @param ?string               $query  The query string
518
         *
519
         * @return string[]
520
         * @throws \Exception
521
         */
522
        public function set_query_types( ?Schema $schema, ?string $query ): array {
745✔
523

524
                /**
525
                 * @param string[]|null         $null   Default value for the filter
526
                 * @param ?\GraphQL\Type\Schema $schema The WPGraphQL Schema for the current request
527
                 * @param ?string               $query  The query string being requested
528
                 */
529
                $null                = null;
745✔
530
                $pre_get_query_types = apply_filters( 'graphql_pre_query_analyzer_get_query_types', $null, $schema, $query );
745✔
531

532
                if ( null !== $pre_get_query_types ) {
745✔
533
                        return $pre_get_query_types;
×
534
                }
535

536
                if ( empty( $query ) || null === $schema ) {
745✔
537
                        return [];
×
538
                }
539
                try {
540
                        $ast = Parser::parse( $query );
745✔
541
                } catch ( SyntaxError $error ) {
×
542
                        return [];
×
543
                }
544
                $type_map  = [];
745✔
545
                $type_info = new TypeInfo( $schema );
745✔
546

547
                Visitor::visit(
745✔
548
                        $ast,
745✔
549
                        Visitor::visitWithTypeInfo(
745✔
550
                                $type_info,
745✔
551
                                [
745✔
552
                                        'enter' => function ( $node ) use ( $type_info, &$type_map, $schema ): void {
745✔
553
                                                $type_info->enter( $node );
745✔
554
                                                $type = $type_info->getType();
745✔
555
                                                if ( ! $type ) {
745✔
556
                                                        return;
745✔
557
                                                }
558

559
                                                if ( empty( $this->root_operation ) ) {
745✔
560
                                                        if ( $type === $schema->getQueryType() ) {
745✔
561
                                                                $this->root_operation = 'Query';
634✔
562
                                                        }
563

564
                                                        if ( $type === $schema->getMutationType() ) {
745✔
565
                                                                $this->root_operation = 'Mutation';
113✔
566
                                                        }
567

568
                                                        if ( $type === $schema->getSubscriptionType() ) {
745✔
NEW
569
                                                                $this->root_operation = 'Subscription';
×
570
                                                        }
571
                                                }
572

573
                                                $named_type = Type::getNamedType( $type );
745✔
574

575
                                                if ( $named_type instanceof InterfaceType ) {
745✔
576
                                                        $possible_types = $schema->getPossibleTypes( $named_type );
143✔
577
                                                        foreach ( $possible_types as $possible_type ) {
143✔
578
                                                                $type_map[] = strtolower( $possible_type );
139✔
579
                                                        }
580
                                                } elseif ( $named_type instanceof ObjectType ) {
745✔
581
                                                        $type_map[] = strtolower( $named_type );
745✔
582
                                                }
583
                                        },
745✔
584
                                        'leave' => static function ( $node ) use ( $type_info ): void {
745✔
585
                                                $type_info->leave( $node );
745✔
586
                                        },
745✔
587
                                ]
745✔
588
                        )
745✔
589
                );
745✔
590
                $map = array_values( array_unique( array_filter( $type_map ) ) );
745✔
591

592
                return apply_filters( 'graphql_cache_collection_get_query_types', $map, $schema, $query, $type_info );
745✔
593
        }
594

595
        /**
596
         * Given the Schema and a query string, return a list of GraphQL model names that are being
597
         * asked for by the query.
598
         *
599
         * @param ?\GraphQL\Type\Schema $schema The WPGraphQL Schema
600
         * @param ?string               $query  The query string
601
         *
602
         * @return string[]
603
         * @throws \GraphQL\Error\SyntaxError|\Exception
604
         */
605
        public function set_query_models( ?Schema $schema, ?string $query ): array {
745✔
606

607
                /**
608
                 * @param string[]|null         $null   Default value for the filter
609
                 * @param ?\GraphQL\Type\Schema $schema The WPGraphQL Schema for the current request
610
                 * @param ?string               $query  The query string being requested
611
                 */
612
                $null           = null;
745✔
613
                $pre_get_models = apply_filters( 'graphql_pre_query_analyzer_get_models', $null, $schema, $query );
745✔
614

615
                if ( null !== $pre_get_models ) {
745✔
616
                        return $pre_get_models;
×
617
                }
618

619
                if ( empty( $query ) || null === $schema ) {
745✔
620
                        return [];
×
621
                }
622
                try {
623
                        $ast = Parser::parse( $query );
745✔
624
                } catch ( SyntaxError $error ) {
×
625
                        return [];
×
626
                }
627

628
                /**
629
                 * @var array<string> $type_map
630
                 */
631
                $type_map  = [];
745✔
632
                $type_info = new TypeInfo( $schema );
745✔
633
                Visitor::visit(
745✔
634
                        $ast,
745✔
635
                        Visitor::visitWithTypeInfo(
745✔
636
                                $type_info,
745✔
637
                                [
745✔
638
                                        'enter' => static function ( $node ) use ( $type_info, &$type_map, $schema ): void {
745✔
639
                                                $type_info->enter( $node );
745✔
640
                                                $type = $type_info->getType();
745✔
641
                                                if ( ! $type ) {
745✔
642
                                                        return;
745✔
643
                                                }
644

645
                                                $named_type = Type::getNamedType( $type );
745✔
646

647
                                                if ( $named_type instanceof InterfaceType ) {
745✔
648
                                                        $possible_types = $schema->getPossibleTypes( $named_type );
143✔
649
                                                        foreach ( $possible_types as $possible_type ) {
143✔
650
                                                                if ( ! isset( $possible_type->config['model'] ) ) {
139✔
651
                                                                        continue;
19✔
652
                                                                }
653
                                                                $type_map[] = $possible_type->config['model'];
138✔
654
                                                        }
655
                                                } elseif ( $named_type instanceof ObjectType ) {
745✔
656
                                                        if ( ! isset( $named_type->config['model'] ) ) {
745✔
657
                                                                return;
745✔
658
                                                        }
659
                                                        $type_map[] = $named_type->config['model'];
585✔
660
                                                }
661
                                        },
745✔
662
                                        'leave' => static function ( $node ) use ( $type_info ): void {
745✔
663
                                                $type_info->leave( $node );
745✔
664
                                        },
745✔
665
                                ]
745✔
666
                        )
745✔
667
                );
745✔
668

669
                /**
670
                 * @var string[] $filtered
671
                 */
672
                $filtered = array_filter( $type_map );
745✔
673

674
                /** @var string[] $unique */
675
                $unique = array_unique( $filtered );
745✔
676

677
                /** @var string[] $map */
678
                $map = array_values( $unique );
745✔
679

680
                return apply_filters( 'graphql_cache_collection_get_query_models', $map, $schema, $query, $type_info );
745✔
681
        }
682

683
        /**
684
         * Track the nodes that were resolved by ensuring the Node's model
685
         * matches one of the models asked for in the query
686
         *
687
         * @param mixed $model The Model to be returned by the loader
688
         *
689
         * @return mixed
690
         */
691
        public function track_nodes( $model ) {
511✔
692
                if ( isset( $model->id ) && in_array( get_class( $model ), $this->get_query_models(), true ) ) {
511✔
693

694
                        // Is this model type part of the requested/returned data in the asked for query?
695

696
                        /**
697
                         * Filter the node ID before returning to the list of resolved nodes
698
                         *
699
                         * @param int             $model_id      The ID of the model (node) being returned
700
                         * @param object          $model         The Model object being returned
701
                         * @param string[]|int[]  $runtime_nodes The runtimes nodes already collected
702
                         */
703
                        $node_id = apply_filters( 'graphql_query_analyzer_runtime_node', $model->id, $model, $this->runtime_nodes );
494✔
704

705
                        $node_type = Utils::get_node_type_from_id( $node_id );
494✔
706

707
                        if ( empty( $this->runtime_nodes_by_type[ $node_type ] ) || ! in_array( $node_id, $this->runtime_nodes_by_type[ $node_type ], true ) ) {
494✔
708
                                $this->runtime_nodes_by_type[ $node_type ][] = $node_id;
494✔
709
                        }
710

711
                        $this->runtime_nodes[] = $node_id;
494✔
712
                }
713

714
                return $model;
511✔
715
        }
716

717
        /**
718
         * Returns graphql keys for use in debugging and headers.
719
         *
720
         * @return string[]
721
         */
722
        public function get_graphql_keys() {
746✔
723
                if ( ! empty( $this->graphql_keys ) ) {
746✔
724
                        return $this->graphql_keys;
309✔
725
                }
726

727
                $keys        = [];
746✔
728
                $return_keys = '';
746✔
729

730
                if ( $this->get_query_id() ) {
746✔
731
                        $keys[] = $this->get_query_id();
745✔
732
                }
733

734
                if ( ! empty( $this->get_root_operation() ) ) {
746✔
735
                        $keys[] = 'graphql:' . $this->get_root_operation();
745✔
736
                }
737

738
                if ( ! empty( $this->get_operation_name() ) ) {
746✔
739
                        $keys[] = $this->get_operation_name();
513✔
740
                }
741

742
                if ( ! empty( $this->get_list_types() ) ) {
746✔
743
                        $keys = array_merge( $keys, $this->get_list_types() );
287✔
744
                }
745

746
                if ( ! empty( $this->get_runtime_nodes() ) ) {
746✔
747
                        $keys = array_merge( $keys, $this->get_runtime_nodes() );
494✔
748
                }
749

750
                if ( ! empty( $keys ) ) {
746✔
751
                        $all_keys = implode( ' ', array_unique( array_values( $keys ) ) );
745✔
752

753
                        // Use the header_length_limit to wrap the words with a separator
754
                        $wrapped = wordwrap( $all_keys, $this->header_length_limit, '\n' );
745✔
755

756
                        // explode the string at the separator. This creates an array of chunks that
757
                        // can be used to expose the keys in multiple headers, each under the header_length_limit
758
                        $chunks = explode( '\n', $wrapped );
745✔
759

760
                        // Iterate over the chunks
761
                        foreach ( $chunks as $index => $chunk ) {
745✔
762
                                if ( 0 === $index ) {
745✔
763
                                        $return_keys = $chunk;
745✔
764
                                } else {
765
                                        $this->skipped_keys = trim( $this->skipped_keys . ' ' . $chunk );
×
766
                                }
767
                        }
768
                }
769

770
                $skipped_keys_array = ! empty( $this->skipped_keys ) ? explode( ' ', $this->skipped_keys ) : [];
746✔
771
                $return_keys_array  = ! empty( $return_keys ) ? explode( ' ', $return_keys ) : [];
746✔
772
                $skipped_types      = [];
746✔
773

774
                $runtime_node_types = array_keys( $this->runtime_nodes_by_type );
746✔
775

776
                if ( ! empty( $skipped_keys_array ) ) {
746✔
777
                        foreach ( $skipped_keys_array as $skipped_key ) {
×
778
                                foreach ( $runtime_node_types as $node_type ) {
×
779
                                        if ( in_array( 'skipped:' . $node_type, $skipped_types, true ) ) {
×
780
                                                continue;
×
781
                                        }
782
                                        if ( in_array( $skipped_key, $this->runtime_nodes_by_type[ $node_type ], true ) ) {
×
783
                                                $skipped_types[] = 'skipped:' . $node_type;
×
784
                                                break;
×
785
                                        }
786
                                }
787
                        }
788
                }
789

790
                // If there are any skipped types, append them to the GraphQL Keys
791
                if ( ! empty( $skipped_types ) ) {
746✔
792
                        $skipped_types_string = implode( ' ', $skipped_types );
×
793
                        $return_keys         .= ' ' . $skipped_types_string;
×
794
                }
795

796
                /**
797
                 * @param array<string,mixed> $graphql_keys       Information about the keys and skipped keys returned by the Query Analyzer
798
                 * @param string              $return_keys        The keys returned to the X-GraphQL-Keys header
799
                 * @param string              $skipped_keys       The Keys that were skipped (truncated due to size limit) from the X-GraphQL-Keys header
800
                 * @param string[]            $return_keys_array  The keys returned to the X-GraphQL-Keys header, in array instead of string
801
                 * @param string[]            $skipped_keys_array The keys skipped, in array instead of string
802
                 */
803
                $this->graphql_keys = apply_filters(
746✔
804
                        'graphql_query_analyzer_graphql_keys',
746✔
805
                        [
746✔
806
                                'keys'             => $return_keys,
746✔
807
                                'keysLength'       => strlen( $return_keys ),
746✔
808
                                'keysCount'        => ! empty( $return_keys_array ) ? count( $return_keys_array ) : 0,
746✔
809
                                'skippedKeys'      => $this->skipped_keys,
746✔
810
                                'skippedKeysSize'  => strlen( $this->skipped_keys ),
746✔
811
                                'skippedKeysCount' => ! empty( $skipped_keys_array ) ? count( $skipped_keys_array ) : 0,
746✔
812
                                'skippedTypes'     => $skipped_types,
746✔
813
                        ],
746✔
814
                        $return_keys,
746✔
815
                        $this->skipped_keys,
746✔
816
                        $return_keys_array,
746✔
817
                        $skipped_keys_array
746✔
818
                );
746✔
819

820
                return $this->graphql_keys;
746✔
821
        }
822

823
        /**
824
         * Return headers
825
         *
826
         * @param array<string,mixed> $headers The array of headers being returned
827
         *
828
         * @return array<string,mixed>
829
         */
UNCOV
830
        public function get_headers( array $headers = [] ): array {
×
831
                $keys = $this->get_graphql_keys();
×
832

833
                if ( ! empty( $keys ) ) {
×
834
                        $headers['X-GraphQL-Query-ID'] = $this->query_id ?: null;
×
835
                        $headers['X-GraphQL-Keys']     = $keys['keys'] ?: null;
×
836
                }
837

838
                /**
839
                 * @param array<string,mixed> $headers The array of headers being returned
840
                 * @param \WPGraphQL\Utils\QueryAnalyzer $query_analyzer The instance of the query analyzer
841
                 */
842
                return apply_filters( 'graphql_query_analyzer_get_headers', $headers, $this );
×
843
        }
844

845
        /**
846
         * Outputs Query Analyzer data in the extensions response
847
         *
848
         * @param mixed                    $response
849
         * @param \WPGraphQL\WPSchema      $schema         The WPGraphQL Schema
850
         * @param string|null              $operation_name The operation name being executed
851
         * @param string|null              $request        The GraphQL Request being made
852
         * @param array<string,mixed>|null $variables      The variables sent with the request
853
         *
854
         * @return array<string,mixed>|object|null
855
         */
856
        public function show_query_analyzer_in_extensions( $response, WPSchema $schema, ?string $operation_name, ?string $request, ?array $variables ) {
746✔
857
                $should = $this->is_enabled_for_query() && WPGraphQL::debug();
746✔
858

859
                /**
860
                 * @param bool                     $should         Whether the query analyzer output should be displayed in the Extensions output. Defaults to true if the query analyzer is enabled for the request and WPGraphQL Debugging is enabled.
861
                 * @param mixed                    $response       The response of the WPGraphQL Request being executed
862
                 * @param \WPGraphQL\WPSchema      $schema The WPGraphQL Schema
863
                 * @param string|null              $operation_name The operation name being executed
864
                 * @param string|null              $request        The GraphQL Request being made
865
                 * @param array<string,mixed>|null $variables      The variables sent with the request
866
                 */
867
                $should_show_query_analyzer_in_extensions = apply_filters( 'graphql_should_show_query_analyzer_in_extensions', $should, $response, $schema, $operation_name, $request, $variables );
746✔
868

869
                // If the query analyzer output is disabled,
870
                // don't show the output in the response
871
                if ( false === $should_show_query_analyzer_in_extensions ) {
746✔
872
                        return $response;
1✔
873
                }
874

875
                $keys = $this->get_graphql_keys();
746✔
876

877
                if ( ! empty( $response ) ) {
746✔
878
                        if ( is_array( $response ) ) {
745✔
879
                                $response['extensions']['queryAnalyzer'] = $keys ?: null;
745✔
880
                        } elseif ( is_object( $response ) ) {
×
881
                                // @phpstan-ignore-next-line
882
                                $response->extensions['queryAnalyzer'] = $keys ?: null;
×
883
                        }
884
                }
885

886
                return $response;
746✔
887
        }
888
}
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