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

wp-graphql / wp-graphql / 15710056976

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

push

github

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

15925 of 18920 relevant lines covered (84.17%)

258.66 hits per line

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

88.58
/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
 * @phpstan-type AnalyzedGraphQLKeys array{
33
 *  keys: string,
34
 *  keysLength: int,
35
 *  keysCount: int,
36
 *  skippedKeys: string,
37
 *  skippedKeysSize: int,
38
 *  skippedKeysCount: int,
39
 *  skippedTypes: string[]
40
 * }
41
 */
42
class QueryAnalyzer {
43

44
        /**
45
         * @var \WPGraphQL\WPSchema|null
46
         */
47
        protected $schema;
48

49
        /**
50
         * Types that are referenced in the query
51
         *
52
         * @var string[]
53
         */
54
        protected $queried_types = [];
55

56
        /**
57
         * @var string
58
         */
59
        protected $root_operation = '';
60

61
        /**
62
         * Models that are referenced in the query
63
         *
64
         * @var string[]
65
         */
66
        protected $models = [];
67

68
        /**
69
         * Types in the query that are lists
70
         *
71
         * @var string[]
72
         */
73
        protected $list_types = [];
74

75
        /**
76
         * @var string[]|int[]
77
         */
78
        protected $runtime_nodes = [];
79

80
        /**
81
         * @var array<string,int[]|string[]>
82
         */
83
        protected $runtime_nodes_by_type = [];
84

85
        /**
86
         * @var string
87
         */
88
        protected $query_id;
89

90
        /**
91
         * @var \WPGraphQL\Request
92
         */
93
        protected $request;
94

95
        /**
96
         * The character length limit for headers
97
         *
98
         * @var int
99
         */
100
        protected $header_length_limit;
101

102
        /**
103
         * The keys that were skipped from being returned in the X-GraphQL-Keys header.
104
         *
105
         * @var string
106
         */
107
        protected $skipped_keys = '';
108

109
        /**
110
         * The GraphQL keys to return in the X-GraphQL-Keys header.
111
         *
112
         * @var AnalyzedGraphQLKeys|array{}
113
         */
114
        protected $graphql_keys = [];
115

116
        /**
117
         * Track all Types that were queried as a list.
118
         *
119
         * @var mixed[]
120
         */
121
        protected $queried_list_types = [];
122

123
        /**
124
         * @var ?bool Whether the Query Analyzer is enabled for the specific or not.
125
         */
126
        protected $is_enabled_for_query;
127

128
        /**
129
         * @param \WPGraphQL\Request $request The GraphQL request being executed
130
         */
131
        public function __construct( Request $request ) {
760✔
132
                $this->request       = $request;
760✔
133
                $this->runtime_nodes = [];
760✔
134
                $this->models        = [];
760✔
135
                $this->list_types    = [];
760✔
136
                $this->queried_types = [];
760✔
137
        }
138

139
        /**
140
         * Checks whether the Query Analyzer is enabled on the site.
141
         *
142
         * @uses `graphql_query_analyzer_enabled` filter.
143
         */
144
        public static function is_enabled(): bool {
760✔
145
                $is_debug_enabled = WPGraphQL::debug();
760✔
146

147
                // The query analyzer is enabled if WPGraphQL Debugging is enabled
148
                $query_analyzer_enabled = $is_debug_enabled;
760✔
149

150
                // If WPGraphQL Debugging is not enabled, check the setting
151
                if ( ! $is_debug_enabled ) {
760✔
152
                        $query_analyzer_enabled = get_graphql_setting( 'query_analyzer_enabled', 'off' );
2✔
153
                        $query_analyzer_enabled = 'on' === $query_analyzer_enabled;
2✔
154
                }
155

156
                /**
157
                 * Filters whether to analyze queries for all GraphQL requests.
158
                 *
159
                 * @param bool $should_track_types Whether to analyze queries or not. Defaults to `true` if GraphQL Debugging is enabled, otherwise `false`.
160
                 */
161
                return apply_filters( 'graphql_should_analyze_queries', $query_analyzer_enabled );
760✔
162
        }
163

164
        /**
165
         * Get the GraphQL Schema.
166
         * If the schema is not set, it will be set.
167
         *
168
         * @throws \Exception
169
         */
170
        public function get_schema(): ?WPSchema {
751✔
171
                if ( ! $this->schema ) {
751✔
172
                        $this->schema = WPGraphQL::get_schema();
751✔
173
                }
174

175
                return $this->schema;
751✔
176
        }
177

178
        /**
179
         * Gets the request object.
180
         */
181
        public function get_request(): Request {
760✔
182
                return $this->request;
760✔
183
        }
184

185
        /**
186
         * Checks if the Query Analyzer is enabled.
187
         *
188
         * @uses `graphql_should_analyze_queries` filter.
189
         */
190
        public function is_enabled_for_query(): bool {
760✔
191
                if ( ! isset( $this->is_enabled_for_query ) ) {
760✔
192
                        $is_enabled = self::is_enabled();
760✔
193

194
                        /**
195
                         * Filters whether to analyze queries or for a specific GraphQL request.
196
                         *
197
                         * @param bool          $should_analyze_queries Whether to analyze queries for the current request. Defaults to the value of `graphql_query_analyzer_enabled` filter.
198
                         * @param \WPGraphQL\Request $request               The GraphQL request being executed
199
                         */
200
                        $should_analyze_queries = apply_filters( 'graphql_should_analyze_query', $is_enabled, $this->get_request() );
760✔
201

202
                        $this->is_enabled_for_query = true === $should_analyze_queries;
760✔
203
                }
204

205
                return $this->is_enabled_for_query;
760✔
206
        }
207

208
        /**
209
         * Initialize the QueryAnalyzer.
210
         */
211
        public function init(): void {
760✔
212
                $should_analyze_queries = $this->is_enabled_for_query();
760✔
213

214
                // If query analyzer is disabled, bail
215
                if ( true !== $should_analyze_queries ) {
760✔
216
                        return;
2✔
217
                }
218

219
                $this->graphql_keys = [];
759✔
220
                $this->skipped_keys = '';
759✔
221

222
                /**
223
                 * Many clients have an 8k (8192 characters) header length cap.
224
                 *
225
                 * This is the total for ALL headers, not just individual headers.
226
                 *
227
                 * SEE: https://nodejs.org/en/blog/vulnerability/november-2018-security-releases/#denial-of-service-with-large-http-headers-cve-2018-12121
228
                 *
229
                 * In order to respect this, we have a default limit of 4000 characters for the X-GraphQL-Keys header
230
                 * to allow for other headers to total up to 8k.
231
                 *
232
                 * This value can be filtered to be increased or decreased.
233
                 *
234
                 * If you see "Parse Error: Header overflow" errors in your client, you might want to decrease this value.
235
                 *
236
                 * On the other hand, if you've increased your allowed header length in your client
237
                 * (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.
238
                 *
239
                 * @param int $header_length_limit The max limit in (binary) bytes headers should be. Anything longer will be truncated.
240
                 */
241
                $this->header_length_limit = apply_filters( 'graphql_query_analyzer_header_length_limit', 4000 );
759✔
242

243
                // track keys related to the query
244
                add_action( 'do_graphql_request', [ $this, 'determine_graphql_keys' ], 10, 4 );
759✔
245

246
                // Track models loaded during execution
247
                add_filter( 'graphql_dataloader_get_model', [ $this, 'track_nodes' ], 10, 1 );
759✔
248

249
                // Expose query analyzer data in extensions
250
                add_filter(
759✔
251
                        'graphql_request_results',
759✔
252
                        [
759✔
253
                                $this,
759✔
254
                                'show_query_analyzer_in_extensions',
759✔
255
                        ],
759✔
256
                        10,
759✔
257
                        5
759✔
258
                );
759✔
259
        }
260

261
        /**
262
         * Determine the keys associated with the GraphQL document being executed
263
         *
264
         * @param ?string                         $query     The GraphQL query
265
         * @param ?string                         $operation The name of the operation
266
         * @param ?array<string,mixed>            $variables Variables to be passed to your GraphQL request
267
         * @param \GraphQL\Server\OperationParams $params The Operation Params. This includes any extra params,
268
         * such as extensions or any other modifications to the request body
269
         *
270
         * @throws \Exception
271
         */
272
        public function determine_graphql_keys( ?string $query, ?string $operation, ?array $variables, OperationParams $params ): void {
752✔
273

274
                // @todo: support for QueryID?
275

276
                // if the query is empty, get it from the (non-batch) request params
277
                if ( empty( $query ) && isset( $this->request->params->query ) ) {
752✔
278
                        $query = $this->request->params->query ?: null;
×
279
                }
280

281
                if ( empty( $query ) ) {
752✔
282
                        return;
1✔
283
                }
284

285
                $query_id       = Utils::get_query_id( $query );
751✔
286
                $this->query_id = $query_id ?: uniqid( 'gql:', true );
751✔
287

288
                // if there's a query (either saved or part of the request params)
289
                // get the GraphQL Types being asked for by the query
290
                $this->list_types    = $this->set_list_types( $this->get_schema(), $query );
751✔
291
                $this->queried_types = $this->set_query_types( $this->get_schema(), $query );
751✔
292
                $this->models        = $this->set_query_models( $this->get_schema(), $query );
751✔
293

294
                /**
295
                 * @param \WPGraphQL\Utils\QueryAnalyzer $query_analyzer The instance of the query analyzer
296
                 * @param string                         $query          The query string being executed
297
                 */
298
                do_action( 'graphql_determine_graphql_keys', $this, $query );
751✔
299
        }
300

301
        /**
302
         * @return string[]
303
         */
304
        public function get_list_types(): array {
752✔
305
                return array_unique( $this->list_types );
752✔
306
        }
307

308
        /**
309
         * @return string[]
310
         */
311
        public function get_query_types(): array {
1✔
312
                return array_unique( $this->queried_types );
1✔
313
        }
314

315
        /**
316
         * @return string[]
317
         */
318
        public function get_query_models(): array {
498✔
319
                return array_unique( $this->models );
498✔
320
        }
321

322
        /**
323
         * @return string[]|int[]
324
         */
325
        public function get_runtime_nodes(): array {
752✔
326
                /**
327
                 * @param string[]|int[] $runtime_nodes Nodes that were resolved during execution
328
                 */
329
                $runtime_nodes = apply_filters( 'graphql_query_analyzer_get_runtime_nodes', $this->runtime_nodes );
752✔
330

331
                return array_unique( $runtime_nodes );
752✔
332
        }
333

334
        /**
335
         * Get the root operation of the query.
336
         */
337
        public function get_root_operation(): string {
752✔
338
                return $this->root_operation;
752✔
339
        }
340

341
        /**
342
         * Returns the operation name of the query, if there is one
343
         */
344
        public function get_operation_name(): ?string {
752✔
345
                $operation_name = ! empty( $this->request->params->operation ) ? $this->request->params->operation : null;
752✔
346

347
                if ( empty( $operation_name ) ) {
752✔
348

349
                        // If the query is not set on the params, return null
350
                        if ( ! isset( $this->request->params->query ) ) {
752✔
351
                                return null;
2✔
352
                        }
353

354
                        try {
355
                                $ast            = Parser::parse( $this->request->params->query );
751✔
356
                                $operation_name = ! empty( $ast->definitions[0]->name->value ) ? $ast->definitions[0]->name->value : null;
751✔
357
                        } catch ( SyntaxError $error ) {
×
358
                                return null;
×
359
                        }
360
                }
361

362
                return ! empty( $operation_name ) ? 'operation:' . $operation_name : null;
751✔
363
        }
364

365
        /**
366
         * Get the query id.
367
         */
368
        public function get_query_id(): ?string {
752✔
369
                return $this->query_id;
752✔
370
        }
371

372
        /**
373
         * @param \GraphQL\Type\Definition\Type            $type The Type of field
374
         * @param \GraphQL\Type\Definition\FieldDefinition $field_def The field definition the type is for
375
         * @param ?\GraphQL\Type\Definition\CompositeType  $parent_type The Parent Type
376
         * @param bool                                     $is_list_type Whether the field is a list type field
377
         *
378
         * @return \GraphQL\Type\Definition\Type|string|null
379
         */
380
        public static function get_wrapped_field_type( Type $type, FieldDefinition $field_def, $parent_type, bool $is_list_type = false ) {
748✔
381
                if ( ! isset( $parent_type->name ) || 'RootQuery' !== $parent_type->name ) {
748✔
382
                        return null;
742✔
383
                }
384

385
                if ( $type instanceof NonNull || $type instanceof ListOfType ) {
638✔
386
                        if ( $type instanceof ListOfType && ! empty( $parent_type->name ) ) {
14✔
387
                                $is_list_type = true;
3✔
388
                        }
389

390
                        return self::get_wrapped_field_type( $type->getWrappedType(), $field_def, $parent_type, $is_list_type );
14✔
391
                }
392

393
                // Determine if we're dealing with a connection
394
                if ( $type instanceof ObjectType || $type instanceof InterfaceType ) {
638✔
395
                        $interfaces      = $type->getInterfaces();
632✔
396
                        $interface_names = ! empty( $interfaces ) ? array_map(
632✔
397
                                static function ( InterfaceType $interface_obj ) {
632✔
398
                                        return $interface_obj->name;
536✔
399
                                },
632✔
400
                                $interfaces
632✔
401
                        ) : [];
632✔
402

403
                        if ( array_key_exists( 'Connection', $interface_names ) ) {
632✔
404
                                if ( isset( $field_def->config['fromType'] ) && ( 'rootquery' !== strtolower( $field_def->config['fromType'] ) ) ) {
288✔
405
                                        return null;
×
406
                                }
407

408
                                $to_type = $field_def->config['toType'] ?? null;
288✔
409
                                if ( empty( $to_type ) ) {
288✔
410
                                        return null;
×
411
                                }
412

413
                                return $to_type;
288✔
414
                        }
415

416
                        if ( ! $is_list_type ) {
362✔
417
                                return null;
359✔
418
                        }
419

420
                        return $type;
3✔
421
                }
422

423
                return null;
10✔
424
        }
425

426
        /**
427
         * Given the Schema and a query string, return a list of GraphQL Types that are being asked for
428
         * by the query.
429
         *
430
         * @param ?\GraphQL\Type\Schema $schema The WPGraphQL Schema
431
         * @param ?string               $query  The query string
432
         *
433
         * @return string[]
434
         * @throws \GraphQL\Error\SyntaxError|\Exception
435
         */
436
        public function set_list_types( ?Schema $schema, ?string $query ): array {
751✔
437

438
                /**
439
                 * @param string[]|null         $null   Default value for the filter
440
                 * @param ?\GraphQL\Type\Schema $schema The WPGraphQL Schema for the current request
441
                 * @param ?string               $query  The query string being requested
442
                 */
443
                $null               = null;
751✔
444
                $pre_get_list_types = apply_filters( 'graphql_pre_query_analyzer_get_list_types', $null, $schema, $query );
751✔
445

446
                if ( null !== $pre_get_list_types ) {
751✔
447
                        return $pre_get_list_types;
×
448
                }
449

450
                if ( empty( $query ) || null === $schema ) {
751✔
451
                        return [];
×
452
                }
453

454
                try {
455
                        $ast = Parser::parse( $query );
751✔
456
                } catch ( SyntaxError $error ) {
×
457
                        return [];
×
458
                }
459

460
                $type_map  = [];
751✔
461
                $type_info = new TypeInfo( $schema );
751✔
462

463
                Visitor::visit(
751✔
464
                        $ast,
751✔
465
                        Visitor::visitWithTypeInfo(
751✔
466
                                $type_info,
751✔
467
                                [
751✔
468
                                        'enter' => static function ( $node ) use ( $type_info, &$type_map, $schema ) {
751✔
469
                                                $parent_type = $type_info->getParentType();
751✔
470

471
                                                if ( 'Field' !== $node->kind ) {
751✔
472
                                                        Visitor::skipNode();
751✔
473
                                                }
474

475
                                                $type_info->enter( $node );
751✔
476
                                                $field_def = $type_info->getFieldDef();
751✔
477

478
                                                if ( ! $field_def instanceof FieldDefinition ) {
751✔
479
                                                        return;
751✔
480
                                                }
481

482
                                                // Determine the wrapped type, which also determines if it's a listOf
483
                                                $field_type = $field_def->getType();
748✔
484
                                                $field_type = self::get_wrapped_field_type( $field_type, $field_def, $parent_type );
748✔
485

486
                                                if ( null === $field_type ) {
748✔
487
                                                        return;
748✔
488
                                                }
489

490
                                                if ( ! empty( $field_type ) && is_string( $field_type ) ) {
291✔
491
                                                        $field_type = $schema->getType( ucfirst( $field_type ) );
288✔
492
                                                }
493

494
                                                if ( ! $field_type ) {
291✔
495
                                                        return;
×
496
                                                }
497

498
                                                $field_type = $schema->getType( $field_type );
291✔
499

500
                                                if ( ! $field_type instanceof ObjectType && ! $field_type instanceof InterfaceType ) {
291✔
501
                                                        return;
2✔
502
                                                }
503

504
                                                // If the type being queried is an interface (i.e. ContentNode) the publishing a new
505
                                                // item of any of the possible types (post, page, etc) should invalidate
506
                                                // this query, so we need to tag this query with `list:$possible_type` for each possible type
507
                                                if ( $field_type instanceof InterfaceType ) {
289✔
508
                                                        $possible_types = $schema->getPossibleTypes( $field_type );
12✔
509
                                                        if ( ! empty( $possible_types ) ) {
12✔
510
                                                                foreach ( $possible_types as $possible_type ) {
10✔
511
                                                                        $type_map[] = 'list:' . strtolower( $possible_type );
10✔
512
                                                                }
513
                                                        }
514
                                                } else {
515
                                                        $type_map[] = 'list:' . strtolower( $field_type );
279✔
516
                                                }
517
                                        },
751✔
518
                                        'leave' => static function ( $node ) use ( $type_info ): void {
751✔
519
                                                $type_info->leave( $node );
751✔
520
                                        },
751✔
521
                                ]
751✔
522
                        )
751✔
523
                );
751✔
524

525
                $map = array_values( array_unique( $type_map ) );
751✔
526

527
                return apply_filters( 'graphql_cache_collection_get_list_types', $map, $schema, $query, $type_info );
751✔
528
        }
529

530
        /**
531
         * Given the Schema and a query string, return a list of GraphQL Types that are being asked for
532
         * by the query.
533
         *
534
         * @param ?\GraphQL\Type\Schema $schema The WPGraphQL Schema
535
         * @param ?string               $query  The query string
536
         *
537
         * @return string[]
538
         * @throws \Exception
539
         */
540
        public function set_query_types( ?Schema $schema, ?string $query ): array {
751✔
541

542
                /**
543
                 * @param string[]|null         $null   Default value for the filter
544
                 * @param ?\GraphQL\Type\Schema $schema The WPGraphQL Schema for the current request
545
                 * @param ?string               $query  The query string being requested
546
                 */
547
                $null                = null;
751✔
548
                $pre_get_query_types = apply_filters( 'graphql_pre_query_analyzer_get_query_types', $null, $schema, $query );
751✔
549

550
                if ( null !== $pre_get_query_types ) {
751✔
551
                        return $pre_get_query_types;
×
552
                }
553

554
                if ( empty( $query ) || null === $schema ) {
751✔
555
                        return [];
×
556
                }
557
                try {
558
                        $ast = Parser::parse( $query );
751✔
559
                } catch ( SyntaxError $error ) {
×
560
                        return [];
×
561
                }
562
                $type_map  = [];
751✔
563
                $type_info = new TypeInfo( $schema );
751✔
564

565
                Visitor::visit(
751✔
566
                        $ast,
751✔
567
                        Visitor::visitWithTypeInfo(
751✔
568
                                $type_info,
751✔
569
                                [
751✔
570
                                        'enter' => function ( $node ) use ( $type_info, &$type_map, $schema ): void {
751✔
571
                                                $type_info->enter( $node );
751✔
572
                                                $type = $type_info->getType();
751✔
573
                                                if ( ! $type ) {
751✔
574
                                                        return;
751✔
575
                                                }
576

577
                                                if ( empty( $this->root_operation ) ) {
751✔
578
                                                        if ( $type === $schema->getQueryType() ) {
751✔
579
                                                                $this->root_operation = 'Query';
640✔
580
                                                        }
581

582
                                                        if ( $type === $schema->getMutationType() ) {
751✔
583
                                                                $this->root_operation = 'Mutation';
113✔
584
                                                        }
585

586
                                                        if ( $type === $schema->getSubscriptionType() ) {
751✔
587
                                                                $this->root_operation = 'Subscription';
×
588
                                                        }
589
                                                }
590

591
                                                $named_type = Type::getNamedType( $type );
751✔
592

593
                                                if ( $named_type instanceof InterfaceType ) {
751✔
594
                                                        $possible_types = $schema->getPossibleTypes( $named_type );
143✔
595
                                                        foreach ( $possible_types as $possible_type ) {
143✔
596
                                                                $type_map[] = strtolower( $possible_type );
139✔
597
                                                        }
598
                                                } elseif ( $named_type instanceof ObjectType ) {
751✔
599
                                                        $type_map[] = strtolower( $named_type );
751✔
600
                                                }
601
                                        },
751✔
602
                                        'leave' => static function ( $node ) use ( $type_info ): void {
751✔
603
                                                $type_info->leave( $node );
751✔
604
                                        },
751✔
605
                                ]
751✔
606
                        )
751✔
607
                );
751✔
608
                $map = array_values( array_unique( array_filter( $type_map ) ) );
751✔
609

610
                return apply_filters( 'graphql_cache_collection_get_query_types', $map, $schema, $query, $type_info );
751✔
611
        }
612

613
        /**
614
         * Given the Schema and a query string, return a list of GraphQL model names that are being
615
         * asked for by the query.
616
         *
617
         * @param ?\GraphQL\Type\Schema $schema The WPGraphQL Schema
618
         * @param ?string               $query  The query string
619
         *
620
         * @return string[]
621
         * @throws \GraphQL\Error\SyntaxError|\Exception
622
         */
623
        public function set_query_models( ?Schema $schema, ?string $query ): array {
751✔
624

625
                /**
626
                 * @param string[]|null         $null   Default value for the filter
627
                 * @param ?\GraphQL\Type\Schema $schema The WPGraphQL Schema for the current request
628
                 * @param ?string               $query  The query string being requested
629
                 */
630
                $null           = null;
751✔
631
                $pre_get_models = apply_filters( 'graphql_pre_query_analyzer_get_models', $null, $schema, $query );
751✔
632

633
                if ( null !== $pre_get_models ) {
751✔
634
                        return $pre_get_models;
×
635
                }
636

637
                if ( empty( $query ) || null === $schema ) {
751✔
638
                        return [];
×
639
                }
640
                try {
641
                        $ast = Parser::parse( $query );
751✔
642
                } catch ( SyntaxError $error ) {
×
643
                        return [];
×
644
                }
645

646
                /**
647
                 * @var array<string> $type_map
648
                 */
649
                $type_map  = [];
751✔
650
                $type_info = new TypeInfo( $schema );
751✔
651
                Visitor::visit(
751✔
652
                        $ast,
751✔
653
                        Visitor::visitWithTypeInfo(
751✔
654
                                $type_info,
751✔
655
                                [
751✔
656
                                        'enter' => static function ( $node ) use ( $type_info, &$type_map, $schema ): void {
751✔
657
                                                $type_info->enter( $node );
751✔
658
                                                $type = $type_info->getType();
751✔
659
                                                if ( ! $type ) {
751✔
660
                                                        return;
751✔
661
                                                }
662

663
                                                $named_type = Type::getNamedType( $type );
751✔
664

665
                                                if ( $named_type instanceof InterfaceType ) {
751✔
666
                                                        $possible_types = $schema->getPossibleTypes( $named_type );
143✔
667
                                                        foreach ( $possible_types as $possible_type ) {
143✔
668
                                                                if ( ! isset( $possible_type->config['model'] ) ) {
139✔
669
                                                                        continue;
19✔
670
                                                                }
671
                                                                $type_map[] = $possible_type->config['model'];
138✔
672
                                                        }
673
                                                } elseif ( $named_type instanceof ObjectType ) {
751✔
674
                                                        if ( ! isset( $named_type->config['model'] ) ) {
751✔
675
                                                                return;
751✔
676
                                                        }
677
                                                        $type_map[] = $named_type->config['model'];
585✔
678
                                                }
679
                                        },
751✔
680
                                        'leave' => static function ( $node ) use ( $type_info ): void {
751✔
681
                                                $type_info->leave( $node );
751✔
682
                                        },
751✔
683
                                ]
751✔
684
                        )
751✔
685
                );
751✔
686

687
                /**
688
                 * @var string[] $filtered
689
                 */
690
                $filtered = array_filter( $type_map );
751✔
691

692
                /** @var string[] $unique */
693
                $unique = array_unique( $filtered );
751✔
694

695
                /** @var string[] $map */
696
                $map = array_values( $unique );
751✔
697

698
                return apply_filters( 'graphql_cache_collection_get_query_models', $map, $schema, $query, $type_info );
751✔
699
        }
700

701
        /**
702
         * Track the nodes that were resolved by ensuring the Node's model
703
         * matches one of the models asked for in the query
704
         *
705
         * @template T
706
         *
707
         * @param T $model The Model to be returned by the loader
708
         *
709
         * @return T
710
         */
711
        public function track_nodes( $model ) {
511✔
712
                if ( isset( $model->id ) && in_array( get_class( $model ), $this->get_query_models(), true ) ) {
511✔
713

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

716
                        /**
717
                         * Filter the node ID before returning to the list of resolved nodes
718
                         *
719
                         * @param int             $model_id      The ID of the model (node) being returned
720
                         * @param object          $model         The Model object being returned
721
                         * @param string[]|int[]  $runtime_nodes The runtimes nodes already collected
722
                         */
723
                        $node_id = apply_filters( 'graphql_query_analyzer_runtime_node', $model->id, $model, $this->runtime_nodes );
494✔
724

725
                        $node_type = Utils::get_node_type_from_id( $node_id );
494✔
726

727
                        if ( empty( $this->runtime_nodes_by_type[ $node_type ] ) || ! in_array( $node_id, $this->runtime_nodes_by_type[ $node_type ], true ) ) {
494✔
728
                                $this->runtime_nodes_by_type[ $node_type ][] = $node_id;
494✔
729
                        }
730

731
                        $this->runtime_nodes[] = $node_id;
494✔
732
                }
733

734
                return $model;
511✔
735
        }
736

737
        /**
738
         * Returns graphql keys for use in debugging and headers.
739
         *
740
         * @return AnalyzedGraphQLKeys
741
         */
742
        public function get_graphql_keys() {
752✔
743
                if ( ! empty( $this->graphql_keys ) ) {
752✔
744
                        return $this->graphql_keys;
311✔
745
                }
746

747
                $keys        = [];
752✔
748
                $return_keys = '';
752✔
749

750
                if ( $this->get_query_id() ) {
752✔
751
                        $keys[] = $this->get_query_id();
751✔
752
                }
753

754
                if ( ! empty( $this->get_root_operation() ) ) {
752✔
755
                        $keys[] = 'graphql:' . $this->get_root_operation();
751✔
756
                }
757

758
                if ( ! empty( $this->get_operation_name() ) ) {
752✔
759
                        $keys[] = $this->get_operation_name();
513✔
760
                }
761

762
                if ( ! empty( $this->get_list_types() ) ) {
752✔
763
                        $keys = array_merge( $keys, $this->get_list_types() );
287✔
764
                }
765

766
                if ( ! empty( $this->get_runtime_nodes() ) ) {
752✔
767
                        $keys = array_merge( $keys, $this->get_runtime_nodes() );
494✔
768
                }
769

770
                if ( ! empty( $keys ) ) {
752✔
771
                        $all_keys = implode( ' ', array_unique( array_values( $keys ) ) );
751✔
772

773
                        // Use the header_length_limit to wrap the words with a separator
774
                        $wrapped = wordwrap( $all_keys, $this->header_length_limit, '\n' );
751✔
775

776
                        // explode the string at the separator. This creates an array of chunks that
777
                        // can be used to expose the keys in multiple headers, each under the header_length_limit
778
                        $chunks = explode( '\n', $wrapped );
751✔
779

780
                        // Iterate over the chunks
781
                        foreach ( $chunks as $index => $chunk ) {
751✔
782
                                if ( 0 === $index ) {
751✔
783
                                        $return_keys = $chunk;
751✔
784
                                } else {
785
                                        $this->skipped_keys = trim( $this->skipped_keys . ' ' . $chunk );
×
786
                                }
787
                        }
788
                }
789

790
                $skipped_keys_array = ! empty( $this->skipped_keys ) ? explode( ' ', $this->skipped_keys ) : [];
752✔
791
                $return_keys_array  = ! empty( $return_keys ) ? explode( ' ', $return_keys ) : [];
752✔
792
                $skipped_types      = [];
752✔
793

794
                $runtime_node_types = array_keys( $this->runtime_nodes_by_type );
752✔
795

796
                if ( ! empty( $skipped_keys_array ) ) {
752✔
797
                        foreach ( $skipped_keys_array as $skipped_key ) {
×
798
                                foreach ( $runtime_node_types as $node_type ) {
×
799
                                        if ( in_array( 'skipped:' . $node_type, $skipped_types, true ) ) {
×
800
                                                continue;
×
801
                                        }
802
                                        if ( in_array( $skipped_key, $this->runtime_nodes_by_type[ $node_type ], true ) ) {
×
803
                                                $skipped_types[] = 'skipped:' . $node_type;
×
804
                                                break;
×
805
                                        }
806
                                }
807
                        }
808
                }
809

810
                // If there are any skipped types, append them to the GraphQL Keys
811
                if ( ! empty( $skipped_types ) ) {
752✔
812
                        $skipped_types_string = implode( ' ', $skipped_types );
×
813
                        $return_keys         .= ' ' . $skipped_types_string;
×
814
                }
815

816
                /**
817
                 * @param AnalyzedGraphQLKeys $graphql_keys       Information about the keys and skipped keys returned by the Query Analyzer
818
                 * @param string              $return_keys        The keys returned to the X-GraphQL-Keys header
819
                 * @param string              $skipped_keys       The Keys that were skipped (truncated due to size limit) from the X-GraphQL-Keys header
820
                 * @param string[]            $return_keys_array  The keys returned to the X-GraphQL-Keys header, in array instead of string
821
                 * @param string[]            $skipped_keys_array The keys skipped, in array instead of string
822
                 */
823
                $this->graphql_keys = apply_filters(
752✔
824
                        'graphql_query_analyzer_graphql_keys',
752✔
825
                        [
752✔
826
                                'keys'             => $return_keys,
752✔
827
                                'keysLength'       => strlen( $return_keys ),
752✔
828
                                'keysCount'        => ! empty( $return_keys_array ) ? count( $return_keys_array ) : 0,
752✔
829
                                'skippedKeys'      => $this->skipped_keys,
752✔
830
                                'skippedKeysSize'  => strlen( $this->skipped_keys ),
752✔
831
                                'skippedKeysCount' => ! empty( $skipped_keys_array ) ? count( $skipped_keys_array ) : 0,
752✔
832
                                'skippedTypes'     => $skipped_types,
752✔
833
                        ],
752✔
834
                        $return_keys,
752✔
835
                        $this->skipped_keys,
752✔
836
                        $return_keys_array,
752✔
837
                        $skipped_keys_array
752✔
838
                );
752✔
839

840
                return $this->graphql_keys;
752✔
841
        }
842

843
        /**
844
         * Return headers
845
         *
846
         * @param array<string,string> $headers The array of headers being returned
847
         *
848
         * @return array<string,string>
849
         */
850
        public function get_headers( array $headers = [] ): array {
×
851
                $keys = $this->get_graphql_keys();
×
852

853
                if ( ! empty( $keys ) ) {
×
854
                        $headers['X-GraphQL-Query-ID'] = $this->query_id ?: '';
×
855
                        $headers['X-GraphQL-Keys']     = $keys['keys'] ?: '';
×
856
                }
857

858
                /**
859
                 * @param array<string,string>           $headers The array of headers being returned
860
                 * @param \WPGraphQL\Utils\QueryAnalyzer $query_analyzer The instance of the query analyzer
861
                 */
862
                return apply_filters( 'graphql_query_analyzer_get_headers', $headers, $this );
×
863
        }
864

865
        /**
866
         * Outputs Query Analyzer data in the extensions response
867
         *
868
         * @param mixed|array<string,mixed>|object $response
869
         * @param \WPGraphQL\WPSchema              $schema         The WPGraphQL Schema
870
         * @param string|null                      $operation_name The operation name being executed
871
         * @param string|null                      $request        The GraphQL Request being made
872
         * @param array<string,mixed>|null         $variables      The variables sent with the request
873
         *
874
         * @return mixed|array<string,mixed>|object
875
         */
876
        public function show_query_analyzer_in_extensions( $response, WPSchema $schema, ?string $operation_name, ?string $request, ?array $variables ) {
752✔
877
                $should = $this->is_enabled_for_query() && WPGraphQL::debug();
752✔
878

879
                /**
880
                 * @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.
881
                 * @param  mixed|array<string,mixed>|object $response       The response of the WPGraphQL Request being executed
882
                 * @param \WPGraphQL\WPSchema               $schema         The WPGraphQL Schema
883
                 * @param string|null                       $operation_name The operation name being executed
884
                 * @param string|null                       $request        The GraphQL Request being made
885
                 * @param array<string,mixed>|null          $variables      The variables sent with the request
886
                 */
887
                $should_show_query_analyzer_in_extensions = apply_filters( 'graphql_should_show_query_analyzer_in_extensions', $should, $response, $schema, $operation_name, $request, $variables );
752✔
888

889
                // If the query analyzer output is disabled,
890
                // don't show the output in the response
891
                if ( false === $should_show_query_analyzer_in_extensions ) {
752✔
892
                        return $response;
1✔
893
                }
894

895
                $keys = $this->get_graphql_keys();
752✔
896

897
                if ( ! empty( $response ) ) {
752✔
898
                        if ( is_array( $response ) ) {
751✔
899
                                $response['extensions']['queryAnalyzer'] = $keys ?: null;
751✔
900
                        } elseif ( is_object( $response ) ) {
×
901
                                // @phpstan-ignore-next-line
902
                                $response->extensions['queryAnalyzer'] = $keys ?: null;
×
903
                        }
904
                }
905

906
                return $response;
752✔
907
        }
908
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc