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

wp-graphql / wp-graphql / 13659882478

04 Mar 2025 05:56PM UTC coverage: 82.702% (-0.01%) from 82.712%
13659882478

push

github

jasonbahl
- fix changelog and readme.txt

13822 of 16713 relevant lines covered (82.7%)

299.99 hits per line

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

83.57
/src/Data/Connection/AbstractConnectionResolver.php
1
<?php
2

3
namespace WPGraphQL\Data\Connection;
4

5
use GraphQL\Deferred;
6
use GraphQL\Error\InvariantViolation;
7
use GraphQL\Error\UserError;
8
use GraphQL\Type\Definition\ResolveInfo;
9
use WPGraphQL\AppContext;
10
use WPGraphQL\Model\Post;
11

12
/**
13
 * Class AbstractConnectionResolver
14
 *
15
 * Individual Connection Resolvers should extend this to make returning data in proper shape for Relay-compliant connections easier, ensure data is passed through consistent filters, etc.
16
 *
17
 * @package WPGraphQL\Data\Connection
18
 *
19
 * The template type `TQueryClass` is used by static analysis tools to correctly typehint the query class used by the Connection Resolver.
20
 * Classes that extend `AbstractConnectionResolver` should add `@extends @extends \WPGraphQL\Data\Connection\AbstractConnectionResolver<\MY_QUERY_CLASS>` to the class dockblock to get proper hinting.
21
 * E.g. `@extends \WPGraphQL\Data\Connection\AbstractConnectionResolver<\WP_Term_Query>`
22
 *
23
 * @template TQueryClass
24
 */
25
abstract class AbstractConnectionResolver {
26
        /**
27
         * The source from the field calling the connection.
28
         *
29
         * @var \WPGraphQL\Model\Model|mixed[]|mixed
30
         */
31
        protected $source;
32

33
        /**
34
         * The args input before it is filtered and prepared by the constructor.
35
         *
36
         * @var array<string,mixed>
37
         */
38
        protected $unfiltered_args;
39

40
        /**
41
         * The args input on the field calling the connection.
42
         *
43
         * Filterable by `graphql_connection_args`.
44
         *
45
         * @var ?array<string,mixed>
46
         */
47
        protected $args;
48

49
        /**
50
         * The AppContext for the GraphQL Request
51
         *
52
         * @var \WPGraphQL\AppContext
53
         */
54
        protected $context;
55

56
        /**
57
         * The ResolveInfo for the GraphQL Request
58
         *
59
         * @var \GraphQL\Type\Definition\ResolveInfo
60
         */
61
        protected $info;
62

63
        /**
64
         * The query args used to query for data to resolve the connection.
65
         *
66
         * Filterable by `graphql_connection_query_args`.
67
         *
68
         * @var ?array<string,mixed>
69
         */
70
        protected $query_args;
71

72
        /**
73
         * Whether the connection resolver should execute.
74
         *
75
         * If `false`, the connection resolve will short-circuit and return an empty array.
76
         *
77
         * Filterable by `graphql_connection_pre_should_execute` and `graphql_connection_should_execute`.
78
         *
79
         * @var ?bool
80
         */
81
        protected $should_execute;
82

83
        /**
84
         * The loader name.
85
         *
86
         * Defaults to `loader_name()` and filterable by `graphql_connection_loader_name`.
87
         *
88
         * @var ?string
89
         */
90
        protected $loader_name;
91

92
        /**
93
         * The loader the resolver is configured to use.
94
         *
95
         * @var ?\WPGraphQL\Data\Loader\AbstractDataLoader
96
         */
97
        protected $loader;
98

99
        /**
100
         * Whether the connection is a one to one connection. Default false.
101
         *
102
         * @var bool
103
         */
104
        public $one_to_one = false;
105

106
        /**
107
         * The class name of the query to instantiate. Set to `null` if the Connection Resolver does not rely on a query class to fetch data.
108
         *
109
         * Examples `WP_Query`, `WP_Comment_Query`, `WC_Query`, `/My/Namespaced/CustomQuery`, etc.
110
         *
111
         * @var ?class-string<TQueryClass>
112
         */
113
        protected $query_class;
114

115
        /**
116
         * The instantiated query array/object used to fetch the data.
117
         *
118
         * Examples:
119
         *   return new WP_Query( $this->get_query_args() );
120
         *   return new WP_Comment_Query( $this->get_query_args() );
121
         *   return new WP_Term_Query( $this->get_query_args() );
122
         *
123
         * Whatever it is will be passed through filters so that fields throughout
124
         * have context from what was queried and can make adjustments as needed, such
125
         * as exposing `totalCount` in pageInfo, etc.
126
         *
127
         * Filterable by `graphql_connection_pre_get_query` and `graphql_connection_query`.
128
         *
129
         * @var ?TQueryClass
130
         */
131
        protected $query;
132

133
        /**
134
         * @var mixed[]
135
         *
136
         * @deprecated 1.26.0 This is an artifact and is unused. It will be removed in a future release.
137
         */
138
        protected $items;
139

140
        /**
141
         * The IDs returned from the query.
142
         *
143
         * The IDs are sliced to confirm with the pagination args, and overfetched by one.
144
         *
145
         * Filterable by `graphql_connection_ids`.
146
         *
147
         * @var int[]|string[]|null
148
         */
149
        protected $ids;
150

151
        /**
152
         * The nodes (usually GraphQL models) returned from the query.
153
         *
154
         * Filterable by `graphql_connection_nodes`.
155
         *
156
         * @var \WPGraphQL\Model\Model[]|mixed[]|null
157
         */
158
        protected $nodes;
159

160
        /**
161
         * The edges for the connection.
162
         *
163
         * Filterable by `graphql_connection_edges`.
164
         *
165
         * @var ?array<string,mixed>[]
166
         */
167
        protected $edges;
168

169
        /**
170
         * The page info for the connection.
171
         *
172
         * Filterable by `graphql_connection_page_info`.
173
         *
174
         * @var ?array<string,mixed>
175
         */
176
        protected $page_info;
177

178
        /**
179
         * The query amount to return for the connection.
180
         *
181
         * @var ?int
182
         */
183
        protected $query_amount;
184

185
        /**
186
         * ConnectionResolver constructor.
187
         *
188
         * @param mixed                                $source  Source passed down from the resolve tree
189
         * @param array<string,mixed>                  $args    Array of arguments input in the field as part of the GraphQL query.
190
         * @param \WPGraphQL\AppContext                $context The app context that gets passed down the resolve tree.
191
         * @param \GraphQL\Type\Definition\ResolveInfo $info    Info about fields passed down the resolve tree.
192
         */
193
        public function __construct( $source, array $args, AppContext $context, ResolveInfo $info ) {
379✔
194
                // Set the source (the root object), context, resolveInfo, and unfiltered args for the resolver.
195
                $this->source          = $source;
379✔
196
                $this->unfiltered_args = $args;
379✔
197
                $this->context         = $context;
379✔
198
                $this->info            = $info;
379✔
199

200
                /**
201
                 * @todo This exists for b/c, where extenders may be directly accessing `$this->args` in ::get_loader() or even `::get_args()`.
202
                 * We can call it later in the lifecycle once that's no longer the case.
203
                 */
204
                $this->args = $this->get_args();
379✔
205

206
                // Pre-check if the connection should execute so we can skip expensive logic if we already know it shouldn't execute.
207
                if ( ! $this->get_pre_should_execute( $this->source, $this->unfiltered_args, $this->context, $this->info ) ) {
379✔
208
                        $this->should_execute = false;
×
209
                }
210

211
                // Get the loader for the Connection.
212
                $this->loader = $this->get_loader();
379✔
213

214
                /**
215
                 * Filters the GraphQL args before they are used in get_query_args().
216
                 *
217
                 * @todo We reinstantiate this here for b/c. Once that is not a concern, we should relocate this filter to ::get_args().
218
                 *
219
                 * @param array<string,mixed>                                   $args                The GraphQL args passed to the resolver.
220
                 * @param \WPGraphQL\Data\Connection\AbstractConnectionResolver $connection_resolver Instance of the ConnectionResolver.
221
                 * @param array<string,mixed>                                   $unfiltered_args     Array of arguments input in the field as part of the GraphQL query.
222
                 *
223
                 * @since 1.11.0
224
                 */
225
                $this->args = apply_filters( 'graphql_connection_args', $this->args, $this, $this->get_unfiltered_args() );
379✔
226

227
                // Get the query amount for the connection.
228
                $this->query_amount = $this->get_query_amount();
379✔
229

230
                /**
231
                 * Filters the query args before they are used in the query.
232
                 *
233
                 *  @todo We reinstantiate this here for b/c. Once that is not a concern, we should relocate this filter to ::get_query_args().
234
                 *
235
                 * @param array<string,mixed>                                   $query_args          The query args to be used with the executable query to get data.
236
                 * @param \WPGraphQL\Data\Connection\AbstractConnectionResolver $connection_resolver Instance of the ConnectionResolver
237
                 * @param array<string,mixed>                                   $unfiltered_args     Array of arguments input in the field as part of the GraphQL query.
238
                 */
239
                $this->query_args = apply_filters( 'graphql_connection_query_args', $this->get_query_args(), $this, $this->get_unfiltered_args() );
379✔
240

241
                // Get the query class for the connection.
242
                $this->query_class = $this->get_query_class();
376✔
243

244
                // The rest of the class properties are set when `$this->get_connection()` is called.
245
        }
246

247
        /**
248
         * ====================
249
         * Required/Abstract Methods
250
         *
251
         * These methods must be implemented or overloaded in the extending class.
252
         *
253
         * The reason not all methods are abstract is to prevent backwards compatibility issues.
254
         * ====================
255
         */
256

257
        /**
258
         * The name of the loader to use for this connection.
259
         *
260
         * Filterable by `graphql_connection_loader_name`.
261
         *
262
         * @todo This is protected for backwards compatibility, but should be abstract and implemented by the child classes.
263
         */
264
        protected function loader_name(): string {
×
265
                return '';
×
266
        }
267

268
        /**
269
         * Prepares the query args used to fetch the data for the connection.
270
         *
271
         * This accepts the GraphQL args and maps them to a format that can be read by our query class.
272
         * For example, if the ConnectionResolver uses WP_Query to fetch the data, this should return $args for use in `new WP_Query( $args );`
273
         *
274
         * @todo This is protected for backwards compatibility, but should be abstract and implemented by the child classes.
275
         *
276
         * @param array<string,mixed> $args The GraphQL input args passed to the connection.
277
         *
278
         * @return array<string,mixed>
279
         *
280
         * @throws \GraphQL\Error\InvariantViolation If the method is not implemented.
281
         *
282
         * @codeCoverageIgnore
283
         */
284
        protected function prepare_query_args( array $args ): array {
285
                throw new InvariantViolation(
286
                        sprintf(
287
                                // translators: %s is the name of the connection resolver class.
288
                                esc_html__( 'Class %s does not implement a valid method `prepare_query_args()`.', 'wp-graphql' ),
289
                                static::class
290
                        )
291
                );
292
        }
293

294
        /**
295
         * Return an array of ids from the query
296
         *
297
         * Each Query class in WP and potential datasource handles this differently,
298
         * so each connection resolver should handle getting the items into a uniform array of items.
299
         *
300
         * @todo: This is not an abstract function to prevent backwards compatibility issues, so it instead throws an exception.
301
         *
302
         * Classes that extend AbstractConnectionResolver should
303
         * override this method instead of ::get_ids().
304
         *
305
         * @since 1.9.0
306
         *
307
         * @throws \GraphQL\Error\InvariantViolation If child class forgot to implement this.
308
         * @return int[]|string[] the array of IDs.
309
         */
310
        public function get_ids_from_query() {
×
311
                throw new InvariantViolation(
×
312
                        sprintf(
×
313
                                // translators: %s is the name of the connection resolver class.
314
                                esc_html__( 'Class %s does not implement a valid method `get_ids_from_query()`.', 'wp-graphql' ),
×
315
                                static::class
×
316
                        )
×
317
                );
×
318
        }
319
        /**
320
         * Determine whether or not the the offset is valid, i.e the item corresponding to the offset exists.
321
         *
322
         * Offset is equivalent to WordPress ID (e.g post_id, term_id). So this is equivalent to checking if the WordPress object exists for the given ID.
323
         *
324
         * @param mixed $offset The offset to validate. Typically a WordPress Database ID
325
         *
326
         * @return bool
327
         */
328
        abstract public function is_valid_offset( $offset );
329

330
        /**
331
         * ====================
332
         * The following methods handle the underlying behavior of the connection, and are intended to be overloaded by the child class.
333
         *
334
         * These methods are wrapped in getters which apply the filters and set the properties of the class instance.
335
         * ====================
336
         */
337

338
        /**
339
         * Used to determine whether the connection query should be executed. This is useful for short-circuiting the connection resolver before executing the query.
340
         *
341
         * When `pre_should_execute()` returns false, that's a sign the Resolver shouldn't execute the query. Otherwise, the more expensive logic logic in `should_execute()` will run later in the lifecycle.
342
         *
343
         * @param mixed                                $source  Source passed down from the resolve tree
344
         * @param array<string,mixed>                  $args    Array of arguments input in the field as part of the GraphQL query.
345
         * @param \WPGraphQL\AppContext                $context The app context that gets passed down the resolve tree.
346
         * @param \GraphQL\Type\Definition\ResolveInfo $info    Info about fields passed down the resolve tree.
347
         */
348
        protected function pre_should_execute( $source, array $args, AppContext $context, ResolveInfo $info ): bool {
379✔
349
                $should_execute = true;
379✔
350

351
                /**
352
                 * If the source is a Post and the ID is empty (i.e. if the user doesn't have permissions to view the source), we should not execute the query.
353
                 *
354
                 * @todo This can probably be abstracted to check if _any_ source is private, and not just `PostObject` models.
355
                 */
356
                if ( $source instanceof Post && empty( $source->ID ) ) {
379✔
357
                        $should_execute = false;
×
358
                }
359

360
                return $should_execute;
379✔
361
        }
362

363
        /**
364
         * Prepares the GraphQL args for use by the connection.
365
         *
366
         * Useful for modifying the $args before they are passed to $this->get_query_args().
367
         *
368
         * @param array<string,mixed> $args The GraphQL input args to prepare.
369
         *
370
         * @return array<string,mixed>
371
         */
372
        protected function prepare_args( array $args ): array {
166✔
373
                return $args;
166✔
374
        }
375

376
        /**
377
         * The maximum number of items that should be returned by the query.
378
         *
379
         * This is filtered by `graphql_connection_max_query_amount` in ::get_query_amount().
380
         */
381
        protected function max_query_amount(): int {
337✔
382
                return 100;
337✔
383
        }
384

385
        /**
386
         * The default query class to use for the connection.
387
         *
388
         * Should return null if the resolver does not use a query class to fetch the data.
389
         *
390
         * @return ?class-string<TQueryClass>
391
         */
392
        protected function query_class(): ?string {
99✔
393
                return null;
99✔
394
        }
395

396
        /**
397
         * Validates the query class. Will be ignored if the Connection Resolver does not use a query class.
398
         *
399
         * By default this checks if the query class has a `query()` method. If the query class requires the `query()` method to be named something else (e.g. $query_class->get_results()` ) this method should be overloaded.
400
         *
401
         * @param string $query_class The query class to validate.
402
         */
403
        protected function is_valid_query_class( string $query_class ): bool {
296✔
404
                return method_exists( $query_class, 'query' );
296✔
405
        }
406

407
        /**
408
         * Executes the query and returns the results.
409
         *
410
         * Usually, the returned value is an instantiated `$query_class` (e.g. `WP_Query`), but it can be any collection of data. The `get_ids_from_query()` method will be used to extract the IDs from the returned value.
411
         *
412
         * If the resolver does not rely on a query class, this should be overloaded to return the data directly.
413
         *
414
         * @param array<string,mixed> $query_args The query args to use to query the data.
415
         *
416
         * @return TQueryClass
417
         *
418
         * @throws \GraphQL\Error\InvariantViolation If the query class is not valid.
419
         */
420
        protected function query( array $query_args ) {
295✔
421
                // If there is no query class, we need the child class to overload this method.
422
                $query_class = $this->get_query_class();
295✔
423

424
                if ( empty( $query_class ) ) {
295✔
425
                        throw new InvariantViolation(
×
426
                                sprintf(
×
427
                                        // translators: %s is the name of the connection resolver class.
428
                                        esc_html__( 'The %s class does not rely on a query class. Please define a `query()` method to return the data directly.', 'wp-graphql' ),
×
429
                                        static::class
×
430
                                )
×
431
                        );
×
432
                }
433

434
                return new $query_class( $query_args );
295✔
435
        }
436

437
        /**
438
         * Determine whether or not the query should execute.
439
         *
440
         * Return true to execute, return false to prevent execution.
441
         *
442
         * Various criteria can be used to determine whether a Connection Query should be executed.
443
         *
444
         * For example, if a user is requesting revisions of a Post, and the user doesn't have permission to edit the post, they don't have permission to view the revisions, and therefore we can prevent the query to fetch revisions from executing in the first place.
445
         *
446
         * Runs only if `pre_should_execute()` returns true.
447
         *
448
         * @todo This is public for b/c but it should be protected.
449
         *
450
         * @return bool
451
         */
452
        public function should_execute() {
356✔
453
                return true;
356✔
454
        }
455

456
        /**
457
         * Returns the offset for a given cursor.
458
         *
459
         * Connections that use a string-based offset should override this method.
460
         *
461
         * @param ?string $cursor The cursor to convert to an offset.
462
         *
463
         * @return int|mixed
464
         */
465
        public function get_offset_for_cursor( string $cursor = null ) { // phpcs:ignore PHPCompatibility.FunctionDeclarations.RemovedImplicitlyNullableParam.Deprecated,SlevomatCodingStandard.TypeHints.NullableTypeForNullDefaultValue -- This is a breaking change to fix.
109✔
466
                $offset = false;
109✔
467

468
                // We avoid using ArrayConnection::cursorToOffset() because it assumes an `int` offset.
469
                if ( ! empty( $cursor ) ) {
109✔
470
                        $offset = substr( base64_decode( $cursor ), strlen( 'arrayconnection:' ) );
97✔
471
                }
472

473
                /**
474
                 * We assume a numeric $offset is an integer ID.
475
                 * If it isn't this method should be overridden by the child class.
476
                 */
477
                return is_numeric( $offset ) ? absint( $offset ) : $offset;
109✔
478
        }
479

480
        /**
481
         * Validates Model.
482
         *
483
         * If model isn't a class with a `fields` member, this function with have be overridden in
484
         * the Connection class.
485
         *
486
         * @param \WPGraphQL\Model\Model|mixed $model The model being validated.
487
         *
488
         * @return bool
489
         */
490
        protected function is_valid_model( $model ) {
298✔
491
                return isset( $model->fields ) && ! empty( $model->fields );
298✔
492
        }
493

494
        /**
495
         * ====================
496
         * Public Getters
497
         *
498
         * These methods are used to get the properties of the class instance.
499
         *
500
         * You shouldn't need to overload these, but if you do, take care to ensure that the overloaded method applies the same filters and sets the same properties as the methods here.
501
         * ====================
502
         */
503

504
        /**
505
         * Returns the source of the connection
506
         *
507
         * @return mixed
508
         */
509
        public function get_source() {
339✔
510
                return $this->source;
339✔
511
        }
512

513
        /**
514
         * Returns the AppContext of the connection.
515
         */
516
        public function get_context(): AppContext {
376✔
517
                return $this->context;
376✔
518
        }
519

520
        /**
521
         * Returns the ResolveInfo of the connection.
522
         */
523
        public function get_info(): ResolveInfo {
×
524
                return $this->info;
×
525
        }
526

527
        /**
528
         * Returns the loader name.
529
         *
530
         * If $loader_name is not initialized, this plugin will initialize it.
531
         *
532
         * @return string
533
         *
534
         * @throws \GraphQL\Error\InvariantViolation
535
         */
536
        public function get_loader_name() {
379✔
537
                // Only initialize the loader_name property once.
538
                if ( ! isset( $this->loader_name ) ) {
379✔
539
                        $name = $this->loader_name();
379✔
540

541
                        // This is a b/c check because `loader_name()` is not abstract.
542
                        if ( empty( $name ) ) {
379✔
543
                                throw new InvariantViolation(
×
544
                                        sprintf(
×
545
                                                // translators: %s is the name of the connection resolver class.
546
                                                esc_html__( 'Class %s does not implement a valid method `loader_name()`.', 'wp-graphql' ),
×
547
                                                esc_html( static::class )
×
548
                                        )
×
549
                                );
×
550
                        }
551

552
                        /**
553
                         * Filters the loader name.
554
                         * This is the name of the registered DataLoader that will be used to load the data for the connection.
555
                         *
556
                         * @param string $loader_name The name of the loader.
557
                         * @param self   $resolver    The AbstractConnectionResolver instance.
558
                         */
559
                        $name = apply_filters( 'graphql_connection_loader_name', $name, $this );
379✔
560

561
                        // Bail if the loader name is invalid.
562
                        if ( empty( $name ) || ! is_string( $name ) ) {
379✔
563
                                throw new InvariantViolation( esc_html__( 'The Connection Resolver needs to define a loader name', 'wp-graphql' ) );
×
564
                        }
565

566
                        $this->loader_name = $name;
379✔
567
                }
568

569
                return $this->loader_name;
379✔
570
        }
571

572
        /**
573
         * Returns the $args passed to the connection, before any modifications.
574
         *
575
         * @return array<string,mixed>
576
         */
577
        public function get_unfiltered_args(): array {
379✔
578
                return $this->unfiltered_args;
379✔
579
        }
580

581
        /**
582
         * Returns the $args passed to the connection.
583
         *
584
         * @return array<string,mixed>
585
         */
586
        public function get_args(): array {
379✔
587
                if ( ! isset( $this->args ) ) {
379✔
588
                        $this->args = $this->prepare_args( $this->get_unfiltered_args() );
379✔
589
                }
590

591
                return $this->args;
379✔
592
        }
593

594
        /**
595
         * Returns the amount of items to query from the database.
596
         *
597
         * The amount is calculated as the the max between what was requested and what is defined as the $max_query_amount to ensure that queries don't exceed unwanted limits when querying data.
598
         *
599
         * If the amount requested is greater than the max query amount, a debug message will be included in the GraphQL response.
600
         *
601
         * @return int
602
         */
603
        public function get_query_amount() {
379✔
604
                if ( ! isset( $this->query_amount ) ) {
379✔
605
                        /**
606
                         * Filter the maximum number of posts per page that should be queried. This prevents queries from being exceedingly resource intensive.
607
                         *
608
                         * The default is 100 - unless overloaded by ::max_query_amount() in the child class.
609
                         *
610
                         * @param int                                  $max_posts  the maximum number of posts per page.
611
                         * @param mixed                                $source     source passed down from the resolve tree
612
                         * @param array<string,mixed>                  $args       array of arguments input in the field as part of the GraphQL query
613
                         * @param \WPGraphQL\AppContext                $context    Object containing app context that gets passed down the resolve tree
614
                         * @param \GraphQL\Type\Definition\ResolveInfo $info       Info about fields passed down the resolve tree
615
                         *
616
                         * @since 0.0.6
617
                         */
618
                        $max_query_amount = (int) apply_filters( 'graphql_connection_max_query_amount', $this->max_query_amount(), $this->source, $this->get_args(), $this->context, $this->info );
379✔
619

620
                        // We don't want the requested amount to be lower than 0.
621
                        $requested_query_amount = (int) max(
379✔
622
                                0,
379✔
623
                                /**
624
                                 * This filter allows to modify the number of nodes the connection should return.
625
                                 *
626
                                 * @param int                        $amount   the requested amount
627
                                 * @param self $resolver Instance of the connection resolver class
628
                                 */
629
                                apply_filters( 'graphql_connection_amount_requested', $this->get_amount_requested(), $this )
379✔
630
                        );
379✔
631

632
                        if ( $requested_query_amount > $max_query_amount ) {
379✔
633
                                graphql_debug(
1✔
634
                                        sprintf( 'The number of items requested by the connection (%s) exceeds the max query amount. Only the first %s items will be returned.', $requested_query_amount, $max_query_amount ),
1✔
635
                                        [ 'connection' => static::class ]
1✔
636
                                );
1✔
637
                        }
638

639
                        $this->query_amount = (int) min( $max_query_amount, $requested_query_amount );
379✔
640
                }
641

642
                return $this->query_amount;
379✔
643
        }
644

645
        /**
646
         * Gets the query args used by the connection to fetch the data.
647
         *
648
         * @return array<string,mixed>
649
         */
650
        public function get_query_args() {
379✔
651
                if ( ! isset( $this->query_args ) ) {
379✔
652
                        // We pass $this->get_args() to ensure we're using the filtered args.
653
                        $this->query_args = $this->prepare_query_args( $this->get_args() );
379✔
654
                }
655

656
                return $this->query_args;
376✔
657
        }
658

659
        /**
660
         * Gets the query class to be instantiated by the `query()` method.
661
         *
662
         * If null, the `query()` method will be overloaded to return the data.
663
         *
664
         * @return ?class-string<TQueryClass>
665
         */
666
        public function get_query_class(): ?string {
376✔
667
                if ( ! isset( $this->query_class ) ) {
376✔
668
                        $default_query_class = $this->query_class();
376✔
669

670
                        // Attempt to get the query class from the context.
671
                        $context = $this->get_context();
376✔
672

673
                        $query_class = ! empty( $context->queryClass ) ? $context->queryClass : $default_query_class;
376✔
674

675
                        /**
676
                         * Filters the `$query_class` that will be used to execute the query.
677
                         *
678
                         * This is useful for replacing the default query (e.g `WP_Query` ) with a custom one (E.g. `WP_Term_Query` or WooCommerce's `WC_Query`).
679
                         *
680
                         * @param ?class-string<TQueryClass> $query_class The query class to be used with the executable query to get data. `null` if the AbstractConnectionResolver does not use a query class.
681
                         * @param self        $resolver    Instance of the AbstractConnectionResolver
682
                         */
683
                        $this->query_class = apply_filters( 'graphql_connection_query_class', $query_class, $this );
376✔
684
                }
685

686
                return $this->query_class;
376✔
687
        }
688

689
        /**
690
         * Returns whether the connection should execute.
691
         *
692
         * If conditions are met that should prevent the execution, we can bail from resolving early, before the query is executed.
693
         */
694
        public function get_should_execute(): bool {
×
695
                // If `pre_should_execute()` or other logic has yet to run, we should run the full `should_execute()` logic.
696
                if ( ! isset( $this->should_execute ) ) {
×
697
                        $this->should_execute = $this->should_execute();
×
698
                }
699

700
                return $this->should_execute;
×
701
        }
702

703
        /**
704
         * Gets the results of the executed query.
705
         *
706
         * @return TQueryClass
707
         */
708
        public function get_query() {
372✔
709
                if ( ! isset( $this->query ) ) {
372✔
710
                        /**
711
                         * When this filter returns anything but null, it will be used as the resolved query, and the default query execution will be skipped.
712
                         *
713
                         * @param null $query               The query to return. Return null to use the default query execution.
714
                         * @param self $resolver The connection resolver instance.
715
                         */
716
                        $query = apply_filters( 'graphql_connection_pre_get_query', null, $this );
372✔
717

718
                        if ( null === $query ) {
372✔
719

720
                                // Validates the query class before it is used in the query() method.
721
                                $this->validate_query_class();
372✔
722

723
                                $query = $this->query( $this->get_query_args() );
369✔
724
                        }
725

726
                        $this->query = $query;
368✔
727
                }
728

729
                return $this->query;
368✔
730
        }
731

732
        /**
733
         * Returns an array of IDs for the connection.
734
         *
735
         * These IDs have been fetched from the query with all the query args applied,
736
         * then sliced (overfetching by 1) by pagination args.
737
         *
738
         * @return int[]|string[]
739
         */
740
        public function get_ids() {
368✔
741
                if ( ! isset( $this->ids ) ) {
368✔
742
                        $this->ids = $this->prepare_ids();
368✔
743
                }
744

745
                return $this->ids;
368✔
746
        }
747

748
        /**
749
         * Get the nodes from the query.
750
         *
751
         * @uses AbstractConnectionResolver::get_ids_for_nodes()
752
         *
753
         * @return array<int|string,mixed|\WPGraphQL\Model\Model|null>
754
         */
755
        public function get_nodes() {
371✔
756
                if ( ! isset( $this->nodes ) ) {
371✔
757
                        $this->nodes = $this->prepare_nodes();
371✔
758
                }
759

760
                return $this->nodes;
371✔
761
        }
762

763
        /**
764
         * Get the edges from the nodes.
765
         *
766
         * @return array<string,mixed>[]
767
         */
768
        public function get_edges() {
371✔
769
                if ( ! isset( $this->edges ) ) {
371✔
770
                        $this->edges = $this->prepare_edges( $this->get_nodes() );
371✔
771
                }
772

773
                return $this->edges;
371✔
774
        }
775

776
        /**
777
         * Returns pageInfo for the connection
778
         *
779
         * @return array<string,mixed>
780
         */
781
        public function get_page_info() {
349✔
782
                if ( ! isset( $this->page_info ) ) {
349✔
783
                        $page_info = $this->prepare_page_info();
349✔
784

785
                        /**
786
                         * Filter the pageInfo that is returned to the connection.
787
                         *
788
                         * This filter allows for additional fields to be filtered into the pageInfo
789
                         * of a connection, such as "totalCount", etc, because the filter has enough
790
                         * context of the query, args, request, etc to be able to calculate and return
791
                         * that information.
792
                         *
793
                         * example:
794
                         *
795
                         * You would want to register a "total" field to the PageInfo type, then filter
796
                         * the pageInfo to return the total for the query, something to this tune:
797
                         *
798
                         * add_filter( 'graphql_connection_page_info', function( $page_info, $connection ) {
799
                         *
800
                         *   $page_info['total'] = null;
801
                         *
802
                         *   if ( $connection->query instanceof WP_Query ) {
803
                         *      if ( isset( $connection->query->found_posts ) {
804
                         *          $page_info['total'] = (int) $connection->query->found_posts;
805
                         *      }
806
                         *   }
807
                         *
808
                         *   return $page_info;
809
                         *
810
                         * });
811
                         */
812
                        $this->page_info = apply_filters( 'graphql_connection_page_info', $page_info, $this );
349✔
813
                }
814

815
                return $this->page_info;
349✔
816
        }
817

818
        /**
819
         * ===============================
820
         * Public setters
821
         *
822
         * These are used to directly modify the instance properties from outside the class.
823
         * ===============================
824
         */
825

826
        /**
827
         * Given a key and value, this sets a query_arg which will modify the query_args used by ::get_query();
828
         *
829
         * @param string $key   The key of the query arg to set
830
         * @param mixed  $value The value of the query arg to set
831
         *
832
         * @return static
833
         */
834
        public function set_query_arg( $key, $value ) {
109✔
835
                $this->query_args[ $key ] = $value;
109✔
836
                return $this;
109✔
837
        }
838

839
        /**
840
         * Overloads the query_class which will be used to instantiate the query.
841
         *
842
         * @param class-string<TQueryClass> $query_class The class to use for the query. If empty, this will reset to the default query class.
843
         *
844
         * @return static
845
         */
846
        public function set_query_class( string $query_class ) {
5✔
847
                $this->query_class = $query_class ?: $this->query_class();
5✔
848

849
                return $this;
5✔
850
        }
851

852
        /**
853
         * Whether the connection should resolve as a one-to-one connection.
854
         *
855
         * @return static
856
         */
857
        public function one_to_one() {
65✔
858
                $this->one_to_one = true;
65✔
859

860
                return $this;
65✔
861
        }
862

863
        /**
864
         * Gets whether or not the query should execute, BEFORE any data is fetched or altered, filtered by 'graphql_connection_pre_should_execute'.
865
         *
866
         * @param mixed                                $source  The source that's passed down the GraphQL queries.
867
         * @param array<string,mixed>                  $args    The inputArgs on the field.
868
         * @param \WPGraphQL\AppContext                $context The AppContext passed down the GraphQL tree.
869
         * @param \GraphQL\Type\Definition\ResolveInfo $info    The ResolveInfo passed down the GraphQL tree.
870
         */
871
        protected function get_pre_should_execute( $source, array $args, AppContext $context, ResolveInfo $info ): bool {
379✔
872
                $should_execute = $this->pre_should_execute( $source, $args, $context, $info );
379✔
873

874
                /**
875
                 * Filters whether or not the query should execute, BEFORE any data is fetched or altered.
876
                 *
877
                 * This is evaluated based solely on the values passed to the constructor, before any data is fetched or altered, and is useful for short-circuiting the Connection Resolver before any heavy logic is executed.
878
                 *
879
                 * For more in-depth checks, use the `graphql_connection_should_execute` filter instead.
880
                 *
881
                 * @param bool                                 $should_execute Whether or not the query should execute.
882
                 * @param mixed                                $source         The source that's passed down the GraphQL queries.
883
                 * @param array                                $args           The inputArgs on the field.
884
                 * @param \WPGraphQL\AppContext                $context        The AppContext passed down the GraphQL tree.
885
                 * @param \GraphQL\Type\Definition\ResolveInfo $info           The ResolveInfo passed down the GraphQL tree.
886
                 */
887
                return apply_filters( 'graphql_connection_pre_should_execute', $should_execute, $source, $args, $context, $info );
379✔
888
        }
889

890
        /**
891
         * Returns the loader.
892
         *
893
         * If $loader is not initialized, this method will initialize it.
894
         *
895
         * @return \WPGraphQL\Data\Loader\AbstractDataLoader
896
         */
897
        protected function get_loader() {
379✔
898
                // If the loader isn't set, set it.
899
                if ( ! isset( $this->loader ) ) {
379✔
900
                        $name = $this->get_loader_name();
379✔
901

902
                        $this->loader = $this->context->get_loader( $name );
379✔
903
                }
904

905
                return $this->loader;
379✔
906
        }
907

908
        /**
909
         * Returns the amount of items requested from the connection.
910
         *
911
         * @return int
912
         *
913
         * @throws \GraphQL\Error\UserError If the `first` or `last` args are used together.
914
         */
915
        public function get_amount_requested() {
379✔
916
                /**
917
                 * Filters the default query amount for a connection, if no `first` or `last` GraphQL argument is supplied.
918
                 *
919
                 * @param int  $amount_requested The default query amount for a connection.
920
                 * @param self $resolver         Instance of the Connection Resolver.
921
                 */
922
                $amount_requested = apply_filters( 'graphql_connection_default_query_amount', 10, $this );
379✔
923

924
                // @todo This should use  ::get_args() when b/c is not a concern.
925
                $args = $this->args;
379✔
926

927
                /**
928
                 * If both first & last are used in the input args, throw an exception.
929
                 */
930
                if ( ! empty( $args['first'] ) && ! empty( $args['last'] ) ) {
379✔
931
                        throw new UserError( esc_html__( 'The `first` and `last` connection args cannot be used together. For forward pagination, use `first` & `after`. For backward pagination, use `last` & `before`.', 'wp-graphql' ) );
13✔
932
                }
933

934
                /**
935
                 * Get the key to use for the query amount.
936
                 * We avoid a ternary here for unit testing.
937
                 */
938
                $args_key = ! empty( $args['first'] ) && is_int( $args['first'] ) ? 'first' : null;
379✔
939
                if ( null === $args_key ) {
379✔
940
                        $args_key = ! empty( $args['last'] ) && is_int( $args['last'] ) ? 'last' : null;
324✔
941
                }
942

943
                /**
944
                 * If the key is set, and is a positive integer, use it for the $amount_requested
945
                 * but if it's set to anything that isn't a positive integer, throw an exception
946
                 */
947
                if ( null !== $args_key && isset( $args[ $args_key ] ) ) {
379✔
948
                        if ( 0 > $args[ $args_key ] ) {
131✔
949
                                throw new UserError(
×
950
                                        sprintf(
×
951
                                                // translators: %s: The name of the arg that was invalid
952
                                                esc_html__( '%s must be a positive integer.', 'wp-graphql' ),
×
953
                                                esc_html( $args_key )
×
954
                                        )
×
955
                                );
×
956
                        }
957

958
                        $amount_requested = $args[ $args_key ];
131✔
959
                }
960

961
                return (int) $amount_requested;
379✔
962
        }
963

964
        /**
965
         * =====================
966
         * Resolver lifecycle methods
967
         *
968
         * These methods are used internally by the class to resolve the connection. They rarely should be overloaded by the child class, but if you do, make sure to preserve any WordPress hooks included in the parent method.
969
         * =====================
970
         */
971

972
        /**
973
         * Get the connection to return to the Connection Resolver
974
         *
975
         * @return \GraphQL\Deferred
976
         */
977
        public function get_connection() {
375✔
978
                $this->execute_and_get_ids();
375✔
979

980
                /**
981
                 * Return a Deferred function to load all buffered nodes before
982
                 * returning the connection.
983
                 */
984
                return new Deferred(
371✔
985
                        function () {
371✔
986
                                // @todo This should use ::get_ids() when b/c is not a concern.
987
                                $ids = $this->ids;
371✔
988

989
                                if ( ! empty( $ids ) ) {
371✔
990
                                        // Load the ids.
991
                                        $this->get_loader()->load_many( $ids );
339✔
992
                                }
993

994
                                /**
995
                                 * Set the items. These are the "nodes" that make up the connection.
996
                                 *
997
                                 * Filters the nodes in the connection
998
                                 *
999
                                 * @todo We reinstantiate this here for b/c. Once that is not a concern, we should relocate this filter to ::get_nodes().
1000
                                 *
1001
                                 * @param \WPGraphQL\Model\Model[]|mixed[]|null $nodes   The nodes in the connection
1002
                                 * @param self                                 $resolver Instance of the Connection Resolver
1003
                                 */
1004
                                $this->nodes = apply_filters( 'graphql_connection_nodes', $this->get_nodes(), $this );
371✔
1005

1006
                                /**
1007
                                 * Filters the edges in the connection.
1008
                                 *
1009
                                 * @todo We reinstantiate this here for b/c. Once that is not a concern, we should relocate this filter to ::get_edges().
1010
                                 *
1011
                                 * @param array<string,mixed> $edges    The edges in the connection
1012
                                 * @param self                $resolver Instance of the Connection Resolver
1013
                                 */
1014
                                $this->edges = apply_filters( 'graphql_connection_edges', $this->get_edges(), $this );
371✔
1015

1016
                                // @todo: we should also short-circuit fetching/populating the actual nodes/edges if we only need one result.
1017
                                if ( true === $this->one_to_one ) {
371✔
1018
                                        // For one to one connections, return the first edge.
1019
                                        $first_edge_key = array_key_first( $this->edges );
65✔
1020
                                        $connection     = isset( $first_edge_key ) && ! empty( $this->edges[ $first_edge_key ] ) ? $this->edges[ $first_edge_key ] : null;
65✔
1021
                                } else {
1022
                                        // For plural connections (default) return edges/nodes/pageInfo
1023
                                        $connection = [
349✔
1024
                                                'nodes'    => $this->nodes,
349✔
1025
                                                'edges'    => $this->edges,
349✔
1026
                                                'pageInfo' => $this->get_page_info(),
349✔
1027
                                        ];
349✔
1028
                                }
1029

1030
                                /**
1031
                                 * Filter the connection. In some cases, connections will want to provide
1032
                                 * additional information other than edges, nodes, and pageInfo
1033
                                 *
1034
                                 * This filter allows additional fields to be returned to the connection resolver
1035
                                 *
1036
                                 * @param ?array<string,mixed> $connection The connection data being returned. A single edge or null if the connection is one-to-one.
1037
                                 * @param self                 $resolver   The instance of the connection resolver
1038
                                 */
1039
                                return apply_filters( 'graphql_connection', $connection, $this );
371✔
1040
                        }
371✔
1041
                );
371✔
1042
        }
1043

1044
        /**
1045
         * Execute the resolver query and get the data for the connection
1046
         *
1047
         * @return int[]|string[]
1048
         */
1049
        public function execute_and_get_ids() {
375✔
1050
                /**
1051
                 * If should_execute is explicitly set to false already, we can prevent execution quickly.
1052
                 * If it's not, we need to call the should_execute() method to execute any situational logic to determine if the connection query should execute.
1053
                 */
1054
                $should_execute = false === $this->should_execute ? false : $this->should_execute();
375✔
1055

1056
                /**
1057
                 * Check if the connection should execute. If conditions are met that should prevent
1058
                 * the execution, we can bail from resolving early, before the query is executed.
1059
                 *
1060
                 * Filter whether the connection should execute.
1061
                 *
1062
                 * @param bool                       $should_execute      Whether the connection should execute
1063
                 * @param \WPGraphQL\Data\Connection\AbstractConnectionResolver $connection_resolver Instance of the Connection Resolver
1064
                 */
1065
                $this->should_execute = apply_filters( 'graphql_connection_should_execute', $should_execute, $this );
375✔
1066

1067
                if ( false === $this->should_execute ) {
375✔
1068
                        return [];
6✔
1069
                }
1070

1071
                /**
1072
                 * Set the query for the resolver, for use as reference in filters, etc
1073
                 *
1074
                 * Filter the query. For core data, the query is typically an instance of:
1075
                 *
1076
                 *   WP_Query
1077
                 *   WP_Comment_Query
1078
                 *   WP_User_Query
1079
                 *   WP_Term_Query
1080
                 *   ...
1081
                 *
1082
                 * But in some cases, the actual mechanism for querying data should be overridden. For
1083
                 * example, perhaps you're using ElasticSearch or Solr (hypothetical) and want to offload
1084
                 * the query to that instead of a native WP_Query class. You could override this with a
1085
                 * query to that datasource instead.
1086
                 *
1087
                 *  @todo We reinstantiate this here for b/c. Once that is not a concern, we should relocate this filter to ::get_query_args().
1088
                 *
1089
                 * @param mixed                      $query               Instance of the Query for the resolver
1090
                 * @param \WPGraphQL\Data\Connection\AbstractConnectionResolver $connection_resolver Instance of the Connection Resolver
1091
                 */
1092
                $this->query = apply_filters( 'graphql_connection_query', $this->get_query(), $this );
372✔
1093

1094
                /**
1095
                 * Filter the connection IDs
1096
                 *
1097
                 * @todo We filter the IDs here for b/c. Once that is not a concern, we should relocate this filter to ::get_ids().
1098
                 *
1099
                 * @param int[]|string[]                                        $ids                 Array of IDs this connection will be resolving
1100
                 * @param \WPGraphQL\Data\Connection\AbstractConnectionResolver $connection_resolver Instance of the Connection Resolver
1101
                 */
1102
                $this->ids = apply_filters( 'graphql_connection_ids', $this->get_ids(), $this );
368✔
1103

1104
                if ( empty( $this->ids ) ) {
368✔
1105
                        return [];
85✔
1106
                }
1107

1108
                /**
1109
                 * Buffer the IDs for deferred resolution
1110
                 */
1111
                $this->get_loader()->buffer( $this->ids );
339✔
1112

1113
                return $this->ids;
339✔
1114
        }
1115

1116
        /**
1117
         * Validates the $query_class set on the resolver.
1118
         *
1119
         * This runs before the query is executed to ensure that the query class is valid.
1120
         *
1121
         * @throws \GraphQL\Error\InvariantViolation If the query class is invalid.
1122
         */
1123
        protected function validate_query_class(): void {
372✔
1124
                $default_query_class = $this->query_class();
372✔
1125
                $query_class         = $this->get_query_class();
372✔
1126

1127
                // If the default query class is null, then the resolver should not use a query class.
1128
                if ( null === $default_query_class ) {
372✔
1129
                        // If the query class is null, then we're good.
1130
                        if ( null === $query_class ) {
95✔
1131
                                return;
94✔
1132
                        }
1133

1134
                        throw new InvariantViolation(
1✔
1135
                                sprintf(
1✔
1136
                                        // translators: %1$s: The name of the class that should not use a query class. %2$s: The name of the query class that is set by the resolver.
1137
                                        esc_html__( 'Class %1$s should not use a query class, but is attempting to use the %2$s query class.', 'wp-graphql' ),
1✔
1138
                                        static::class,
1✔
1139
                                        esc_html( $query_class )
1✔
1140
                                )
1✔
1141
                        );
1✔
1142
                }
1143

1144
                // If there's no query class set, throw an error.
1145
                if ( null === $query_class ) {
297✔
1146
                        throw new InvariantViolation(
×
1147
                                sprintf(
×
1148
                                        // translators: %s: The connection resolver class name.
1149
                                        esc_html__( '%s requires a query class, but no query class is set.', 'wp-graphql' ),
×
1150
                                        static::class
×
1151
                                )
×
1152
                        );
×
1153
                }
1154

1155
                // If the class is invalid, throw an error.
1156
                if ( ! class_exists( $query_class ) ) {
297✔
1157
                        throw new InvariantViolation(
1✔
1158
                                sprintf(
1✔
1159
                                        // translators: %s: The name of the query class that is set by the resolver.
1160
                                        esc_html__( 'The query class %s does not exist.', 'wp-graphql' ),
1✔
1161
                                        esc_html( $query_class )
1✔
1162
                                )
1✔
1163
                        );
1✔
1164
                }
1165

1166
                // If the class is not compatible with our AbstractConnectionResolver::query() method, throw an error.
1167
                if ( ! $this->is_valid_query_class( $query_class ) ) {
296✔
1168
                        throw new InvariantViolation(
1✔
1169
                                sprintf(
1✔
1170
                                        // translators: %1$s: The name of the query class that is set by the resolver. %2$s: The name of the resolver class.
1171
                                        esc_html__( 'The query class %1$s is not compatible with %2$s.', 'wp-graphql' ),
1✔
1172
                                        esc_html( $this->query_class ?? 'unknown-class' ),
1✔
1173
                                        static::class
1✔
1174
                                )
1✔
1175
                        );
1✔
1176
                }
1177
        }
1178

1179
        /**
1180
         * Returns an array slice of IDs, per the Relay Cursor Connection spec.
1181
         *
1182
         * The resulting array should be overfetched by 1.
1183
         *
1184
         * @see https://relay.dev/graphql/connections.htm#sec-Pagination-algorithm
1185
         *
1186
         * @param int[]|string[] $ids The array of IDs from the query to slice, ordered as expected by the GraphQL query.
1187
         *
1188
         * @since 1.9.0
1189
         *
1190
         * @return int[]|string[]
1191
         */
1192
        public function apply_cursors_to_ids( array $ids ) {
368✔
1193
                if ( empty( $ids ) ) {
368✔
1194
                        return [];
85✔
1195
                }
1196

1197
                // @todo This should use ::get_args() when b/c is not a concern.
1198
                $args = $this->args;
339✔
1199

1200
                // First we slice the array from the front.
1201
                if ( ! empty( $args['after'] ) ) {
339✔
1202
                        $offset = $this->get_offset_for_cursor( $args['after'] );
76✔
1203
                        $index  = $this->get_array_index_for_offset( $offset, $ids );
76✔
1204

1205
                        if ( false !== $index ) {
76✔
1206
                                // We want to start with the first id after the index.
1207
                                $ids = array_slice( $ids, $index + 1, null, true );
14✔
1208
                        }
1209
                }
1210

1211
                // Then we slice the array from the back.
1212
                if ( ! empty( $args['before'] ) ) {
339✔
1213
                        $offset = $this->get_offset_for_cursor( $args['before'] );
42✔
1214
                        $index  = $this->get_array_index_for_offset( $offset, $ids );
42✔
1215

1216
                        if ( false !== $index ) {
42✔
1217
                                // Because array indexes start at 0, we can overfetch without adding 1 to $index.
1218
                                $ids = array_slice( $ids, 0, $index, true );
14✔
1219
                        }
1220
                }
1221

1222
                return $ids;
339✔
1223
        }
1224

1225
        /**
1226
         * Gets the array index for the given offset.
1227
         *
1228
         * @param int|string|false $offset The cursor pagination offset.
1229
         * @param int[]|string[]   $ids    The array of ids from the query.
1230
         *
1231
         * @return int|false $index The array index of the offset.
1232
         */
1233
        public function get_array_index_for_offset( $offset, $ids ) {
97✔
1234
                if ( false === $offset ) {
97✔
1235
                        return false;
×
1236
                }
1237

1238
                // We use array_values() to ensure we're getting a positional index, and not a key.
1239
                return array_search( $offset, array_values( $ids ), true );
97✔
1240
        }
1241

1242
        /**
1243
         * Prepares the nodes for the connection.
1244
         *
1245
         * @used-by self::get_nodes()
1246
         *
1247
         * @return array<int|string,mixed|\WPGraphQL\Model\Model|null>
1248
         */
1249
        protected function prepare_nodes(): array {
371✔
1250
                $nodes = [];
371✔
1251

1252
                // These are already sliced and ordered, we're just populating node data.
1253
                $ids = $this->get_ids_for_nodes();
371✔
1254

1255
                foreach ( $ids as $id ) {
371✔
1256
                        $model = $this->get_node_by_id( $id );
339✔
1257
                        if ( true === $this->get_is_valid_model( $model ) ) {
339✔
1258
                                $nodes[ $id ] = $model;
339✔
1259
                        }
1260
                }
1261

1262
                return $nodes;
371✔
1263
        }
1264

1265
        /**
1266
         * Prepares the IDs for the connection.
1267
         *
1268
         * @used-by self::get_ids()
1269
         *
1270
         * @return int[]|string[]
1271
         */
1272
        protected function prepare_ids(): array {
368✔
1273
                $ids = $this->get_ids_from_query();
368✔
1274

1275
                return $this->apply_cursors_to_ids( $ids );
368✔
1276
        }
1277

1278
        /**
1279
         * Gets the IDs for the currently-paginated slice of nodes.
1280
         *
1281
         * We slice the array to match the amount of items that was asked for, as we over-fetched by 1 item to calculate pageInfo.
1282
         *
1283
         * @used-by AbstractConnectionResolver::get_nodes()
1284
         *
1285
         * @return int[]|string[]
1286
         */
1287
        public function get_ids_for_nodes() {
371✔
1288
                // @todo This should use ::get_ids() and get_args() when b/c is not a concern.
1289
                $ids = $this->ids;
371✔
1290

1291
                if ( empty( $ids ) ) {
371✔
1292
                        return [];
90✔
1293
                }
1294

1295
                $args = $this->args;
339✔
1296

1297
                // If we're going backwards then our overfetched ID is at the front.
1298
                if ( ! empty( $args['last'] ) && count( $ids ) > absint( $args['last'] ) ) {
339✔
1299
                        return array_slice( $ids, count( $ids ) - absint( $args['last'] ), $this->get_query_amount(), true );
46✔
1300
                }
1301

1302
                // If we're going forwards, our overfetched ID is at the back.
1303
                return array_slice( $ids, 0, $this->get_query_amount(), true );
338✔
1304
        }
1305

1306
        /**
1307
         * Given an ID, return the model for the entity or null
1308
         *
1309
         * @param int|string|mixed $id The ID to identify the object by. Could be a database ID or an in-memory ID (like post_type name)
1310
         *
1311
         * @return mixed|\WPGraphQL\Model\Model|null
1312
         */
1313
        public function get_node_by_id( $id ) {
339✔
1314
                return $this->get_loader()->load( $id );
339✔
1315
        }
1316

1317
        /**
1318
         * Gets whether or not the model is valid.
1319
         *
1320
         * @param mixed $model The model being validated.
1321
         */
1322
        protected function get_is_valid_model( $model ): bool {
339✔
1323
                $is_valid = $this->is_valid_model( $model );
339✔
1324

1325
                /**
1326
                 * Filters whether or not the model is valid.
1327
                 *
1328
                 * This is useful when the dataloader is overridden and uses a different model than expected by default.
1329
                 *
1330
                 * @param bool  $is_valid Whether or not the model is valid.
1331
                 * @param mixed $model    The model being validated
1332
                 * @param self  $resolver The connection resolver instance
1333
                 */
1334
                return apply_filters( 'graphql_connection_is_valid_model', $is_valid, $model, $this );
339✔
1335
        }
1336

1337
        /**
1338
         * Prepares the edges for the connection.
1339
         *
1340
         * @used-by self::get_edges()
1341
         *
1342
         * @param array<int|string,mixed|\WPGraphQL\Model\Model|null> $nodes The nodes for the connection.
1343
         *
1344
         * @return array<string,mixed>[]
1345
         */
1346
        protected function prepare_edges( array $nodes ): array {
371✔
1347
                // Bail early if there are no nodes.
1348
                if ( empty( $nodes ) ) {
371✔
1349
                        return [];
97✔
1350
                }
1351

1352
                // The nodes are already ordered, sliced, and populated. What's left is to populate the edge data for each one.
1353
                $edges = [];
339✔
1354
                foreach ( $nodes as $id => $node ) {
339✔
1355
                        $edge = $this->prepare_edge( $id, $node );
339✔
1356

1357
                        /**
1358
                         * Filter the edge within the connection.
1359
                         *
1360
                         * @param array<string,mixed> $edge     The edge within the connection
1361
                         * @param self                $resolver Instance of the connection resolver class
1362
                         */
1363
                        $edge = apply_filters( 'graphql_connection_edge', $edge, $this );
339✔
1364

1365
                        $edges[] = $edge;
339✔
1366
                }
1367

1368
                return $edges;
339✔
1369
        }
1370

1371
        /**
1372
         * Prepares a single edge for the connection.
1373
         *
1374
         * @used-by self::prepare_edges()
1375
         *
1376
         * @param int|string                        $id   The ID of the node.
1377
         * @param mixed|\WPGraphQL\Model\Model|null $node The node for the edge.
1378
         *
1379
         * @return array<string,mixed>
1380
         */
1381
        protected function prepare_edge( $id, $node ): array {
339✔
1382
                return [
339✔
1383
                        'cursor'     => $this->get_cursor_for_node( $id ),
339✔
1384
                        'node'       => $node,
339✔
1385
                        'source'     => $this->get_source(),
339✔
1386
                        'connection' => $this,
339✔
1387
                ];
339✔
1388
        }
1389

1390
        /**
1391
         * Given an ID, a cursor is returned.
1392
         *
1393
         * @param int|string $id The ID to get the cursor for.
1394
         *
1395
         * @return string
1396
         */
1397
        protected function get_cursor_for_node( $id ) {
339✔
1398
                return base64_encode( 'arrayconnection:' . (string) $id );
339✔
1399
        }
1400

1401
        /**
1402
         * Prepares the page info for the connection.
1403
         *
1404
         * @used-by self::get_page_info()
1405
         *
1406
         * @return array<string,mixed>
1407
         */
1408
        protected function prepare_page_info(): array {
349✔
1409
                return [
349✔
1410
                        'startCursor'     => $this->get_start_cursor(),
349✔
1411
                        'endCursor'       => $this->get_end_cursor(),
349✔
1412
                        'hasNextPage'     => $this->has_next_page(),
349✔
1413
                        'hasPreviousPage' => $this->has_previous_page(),
349✔
1414
                ];
349✔
1415
        }
1416

1417
        /**
1418
         * Determine the start cursor from the connection
1419
         *
1420
         * @return mixed|string|null
1421
         */
1422
        public function get_start_cursor() {
349✔
1423
                $first_edge = $this->edges && ! empty( $this->edges ) ? $this->edges[0] : null;
349✔
1424

1425
                return isset( $first_edge['cursor'] ) ? $first_edge['cursor'] : null;
349✔
1426
        }
1427

1428
        /**
1429
         * Determine the end cursor from the connection
1430
         *
1431
         * @return mixed|string|null
1432
         */
1433
        public function get_end_cursor() {
349✔
1434
                $last_edge = ! empty( $this->edges ) ? $this->edges[ count( $this->edges ) - 1 ] : null;
349✔
1435

1436
                return isset( $last_edge['cursor'] ) ? $last_edge['cursor'] : null;
349✔
1437
        }
1438

1439
        /**
1440
         * Gets the offset for the `after` cursor.
1441
         *
1442
         * @return int|string|null
1443
         */
1444
        public function get_after_offset() {
377✔
1445
                // @todo This should use ::get_args() when b/c is not a concern.
1446
                $args = $this->args;
377✔
1447

1448
                if ( ! empty( $args['after'] ) ) {
377✔
1449
                        return $this->get_offset_for_cursor( $args['after'] );
76✔
1450
                }
1451

1452
                return null;
377✔
1453
        }
1454

1455
        /**
1456
         * Gets the offset for the `before` cursor.
1457
         *
1458
         * @return int|string|null
1459
         */
1460
        public function get_before_offset() {
377✔
1461
                // @todo This should use ::get_args() when b/c is not a concern.
1462
                $args = $this->args;
377✔
1463

1464
                if ( ! empty( $args['before'] ) ) {
377✔
1465
                        return $this->get_offset_for_cursor( $args['before'] );
42✔
1466
                }
1467

1468
                return null;
370✔
1469
        }
1470

1471
        /**
1472
         * Whether there is a next page in the connection.
1473
         *
1474
         * If there are more "items" than were asked for in the "first" argument or if there are more "items" after the "before" argument, has_next_page() will be set to true.
1475
         *
1476
         * @return bool
1477
         */
1478
        public function has_next_page() {
349✔
1479
                // @todo This should use ::get_ids() and ::get_args() when b/c is not a concern.
1480
                $args = $this->args;
349✔
1481

1482
                if ( ! empty( $args['first'] ) ) {
349✔
1483
                        $ids = $this->ids;
129✔
1484

1485
                        return ! empty( $ids ) && count( $ids ) > $this->get_query_amount();
129✔
1486
                }
1487

1488
                $before_offset = $this->get_before_offset();
289✔
1489

1490
                if ( $before_offset ) {
289✔
1491
                        return $this->is_valid_offset( $before_offset );
42✔
1492
                }
1493

1494
                return false;
277✔
1495
        }
1496

1497
        /**
1498
         * Whether there is a previous page in the connection.
1499
         *
1500
         * If there are more "items" than were asked for in the "last" argument or if there are more "items" before the "after" argument, has_previous_page() will be set to true.
1501
         *
1502
         * @return bool
1503
         */
1504
        public function has_previous_page() {
349✔
1505
                // @todo This should use ::get_ids() and ::get_args() when b/c is not a concern.
1506
                $args = $this->args;
349✔
1507

1508
                if ( ! empty( $args['last'] ) ) {
349✔
1509
                        $ids = $this->ids;
62✔
1510

1511
                        return ! empty( $ids ) && count( $ids ) > $this->get_query_amount();
62✔
1512
                }
1513

1514
                $after_offset = $this->get_after_offset();
348✔
1515
                if ( $after_offset ) {
348✔
1516
                        return $this->is_valid_offset( $after_offset );
76✔
1517
                }
1518

1519
                return false;
348✔
1520
        }
1521

1522
        /**
1523
         * DEPRECATED METHODS
1524
         *
1525
         * These methods are deprecated and will be removed in a future release.
1526
         */
1527

1528
        /**
1529
         * Returns the $args passed to the connection
1530
         *
1531
         * @deprecated Deprecated since v1.11.0 in favor of $this->get_args();
1532
         *
1533
         * @return array<string,mixed>
1534
         *
1535
         * @codeCoverageIgnore
1536
         */
1537
        public function getArgs(): array {
1538
                _deprecated_function( __METHOD__, '1.11.0', static::class . '::get_args()' );
1539
                return $this->get_args();
1540
        }
1541

1542
        /**
1543
         * @param string $key   The key of the query arg to set
1544
         * @param mixed  $value The value of the query arg to set
1545
         *
1546
         * @return static
1547
         *
1548
         * @deprecated 0.3.0
1549
         *
1550
         * @codeCoverageIgnore
1551
         */
1552
        public function setQueryArg( $key, $value ) {
1553
                _deprecated_function( __METHOD__, '0.3.0', static::class . '::set_query_arg()' );
1554

1555
                return $this->set_query_arg( $key, $value );
1556
        }
1557

1558
        /**
1559
         * Get_offset
1560
         *
1561
         * This returns the offset to be used in the $query_args based on the $args passed to the
1562
         * GraphQL query.
1563
         *
1564
         * @deprecated 1.9.0
1565
         *
1566
         * @codeCoverageIgnore
1567
         *
1568
         * @return int|mixed
1569
         */
1570
        public function get_offset() {
1571
                _deprecated_function( __METHOD__, '1.9.0', static::class . '::get_offset_for_cursor()' );
1572

1573
                // Using shorthand since this is for deprecated code.
1574
                $cursor = $this->args['after'] ?? null;
1575
                $cursor = $cursor ?: ( $this->args['before'] ?? null );
1576

1577
                return $this->get_offset_for_cursor( $cursor );
1578
        }
1579

1580
        /**
1581
         * Returns the source of the connection.
1582
         *
1583
         * @deprecated 1.24.0 in favor of $this->get_source().
1584
         *
1585
         * @return mixed
1586
         */
1587
        public function getSource() {
×
1588
                _deprecated_function( __METHOD__, '1.24.0', static::class . '::get_source()' );
×
1589

1590
                return $this->get_source();
×
1591
        }
1592

1593
        /**
1594
         * Returns the AppContext of the connection.
1595
         *
1596
         * @deprecated 1.24.0 in favor of $this->get_context().
1597
         */
1598
        public function getContext(): AppContext {
×
1599
                _deprecated_function( __METHOD__, '1.24.0', static::class . '::get_context()' );
×
1600

1601
                return $this->get_context();
×
1602
        }
1603

1604
        /**
1605
         * Returns the ResolveInfo of the connection.
1606
         *
1607
         * @deprecated 1.24.0 in favor of $this->get_info().
1608
         */
1609
        public function getInfo(): ResolveInfo {
×
1610
                _deprecated_function( __METHOD__, '1.24.0', static::class . '::get_info()' );
×
1611

1612
                return $this->get_info();
×
1613
        }
1614

1615
        /**
1616
         * Returns whether the connection should execute.
1617
         *
1618
         * @deprecated 1.24.0 in favor of $this->get_should_execute().
1619
         */
1620
        public function getShouldExecute(): bool {
×
1621
                _deprecated_function( __METHOD__, '1.24.0', static::class . '::should_execute()' );
×
1622

1623
                return $this->get_should_execute();
×
1624
        }
1625

1626
        /**
1627
         * Returns the loader.
1628
         *
1629
         * @deprecated 1.24.0 in favor of $this->get_loader().
1630
         *
1631
         * @return \WPGraphQL\Data\Loader\AbstractDataLoader
1632
         */
1633
        protected function getLoader() {
×
1634
                _deprecated_function( __METHOD__, '1.24.0', static::class . '::get_loader()' );
×
1635

1636
                return $this->get_loader();
×
1637
        }
1638
}
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