PluginProbe
WPGraphQL / trunk
WPGraphQL vtrunk
2.22.3 2.22.2 2.22.1 2.22.0 2.21.1 2.21.0 2.20.0 2.19.0 2.18.0 2.17.0 2.16.0 2.15.1 2.15.0 2.14.1 2.14.0 2.13.0 2.2.0 2.3.0 2.3.3 2.3.6 2.3.8 2.5.0 2.5.1 2.5.2 2.5.3 All 177 releases
wp-graphql / src / Data / Connection / AbstractConnectionResolver.php

AbstractConnectionResolver.php in WPGraphQL trunk, at src/Data/Connection/AbstractConnectionResolver.php

1,733 lines 53.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 ) {
194 // Set the source (the root object), context, resolveInfo, and unfiltered args for the resolver.
195 $this->source = $source;
196 $this->unfiltered_args = $args;
197 $this->context = $context;
198 $this->info = $info;
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();
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 ) ) {
208 $this->should_execute = false;
209 }
210
211 // Get the loader for the Connection.
212 $this->loader = $this->get_loader();
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 * @hookGroup connections
224 * @since 1.11.0
225 */
226 $this->args = apply_filters( 'graphql_connection_args', $this->args, $this, $this->get_unfiltered_args() );
227
228 // Get the query amount for the connection.
229 $this->query_amount = $this->get_query_amount();
230
231 /**
232 * Filters the query args before they are used in the query.
233 *
234 * @todo We reinstantiate this here for b/c. Once that is not a concern, we should relocate this filter to ::get_query_args().
235 *
236 * @param array<string,mixed> $query_args The query args to be used with the executable query to get data.
237 * @param \WPGraphQL\Data\Connection\AbstractConnectionResolver $connection_resolver Instance of the ConnectionResolver
238 * @param array<string,mixed> $unfiltered_args Array of arguments input in the field as part of the GraphQL query.
239 * @hookGroup connections
240 * @since 0.0.6
241 */
242 $this->query_args = apply_filters( 'graphql_connection_query_args', $this->get_query_args(), $this, $this->get_unfiltered_args() );
243
244 // Get the query class for the connection.
245 $this->query_class = $this->get_query_class();
246
247 // The rest of the class properties are set when `$this->get_connection()` is called.
248 }
249
250 /**
251 * ====================
252 * Required/Abstract Methods
253 *
254 * These methods must be implemented or overloaded in the extending class.
255 *
256 * The reason not all methods are abstract is to prevent backwards compatibility issues.
257 * ====================
258 */
259
260 /**
261 * The name of the loader to use for this connection.
262 *
263 * Filterable by `graphql_connection_loader_name`.
264 *
265 * @todo This is protected for backwards compatibility, but should be abstract and implemented by the child classes.
266 */
267 protected function loader_name(): string {
268 return '';
269 }
270
271 /**
272 * Prepares the query args used to fetch the data for the connection.
273 *
274 * This accepts the GraphQL args and maps them to a format that can be read by our query class.
275 * For example, if the ConnectionResolver uses WP_Query to fetch the data, this should return $args for use in `new WP_Query( $args );`
276 *
277 * @todo This is protected for backwards compatibility, but should be abstract and implemented by the child classes.
278 *
279 * @param array<string,mixed> $args The GraphQL input args passed to the connection.
280 *
281 * @return array<string,mixed>
282 *
283 * @throws \GraphQL\Error\InvariantViolation If the method is not implemented.
284 *
285 * @codeCoverageIgnore
286 */
287 protected function prepare_query_args( array $args ): array {
288 throw new InvariantViolation(
289 sprintf(
290 // translators: %s is the name of the connection resolver class.
291 esc_html__( 'Class %s does not implement a valid method `prepare_query_args()`.', 'wp-graphql' ),
292 static::class
293 )
294 );
295 }
296
297 /**
298 * Return an array of ids from the query
299 *
300 * Each Query class in WP and potential datasource handles this differently,
301 * so each connection resolver should handle getting the items into a uniform array of items.
302 *
303 * @todo: This is not an abstract function to prevent backwards compatibility issues, so it instead throws an exception.
304 *
305 * Classes that extend AbstractConnectionResolver should
306 * override this method instead of ::get_ids().
307 *
308 * @since 1.9.0
309 *
310 * @throws \GraphQL\Error\InvariantViolation If child class forgot to implement this.
311 * @return int[]|string[] the array of IDs.
312 */
313 public function get_ids_from_query() {
314 throw new InvariantViolation(
315 sprintf(
316 // translators: %s is the name of the connection resolver class.
317 esc_html__( 'Class %s does not implement a valid method `get_ids_from_query()`.', 'wp-graphql' ),
318 static::class
319 )
320 );
321 }
322 /**
323 * Determine whether or not the the offset is valid, i.e the item corresponding to the offset exists.
324 *
325 * 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.
326 *
327 * @param mixed $offset The offset to validate. Typically a WordPress Database ID
328 *
329 * @return bool
330 */
331 abstract public function is_valid_offset( $offset );
332
333 /**
334 * ====================
335 * The following methods handle the underlying behavior of the connection, and are intended to be overloaded by the child class.
336 *
337 * These methods are wrapped in getters which apply the filters and set the properties of the class instance.
338 * ====================
339 */
340
341 /**
342 * Used to determine whether the connection query should be executed. This is useful for short-circuiting the connection resolver before executing the query.
343 *
344 * 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.
345 *
346 * @param mixed $source Source passed down from the resolve tree
347 * @param array<string,mixed> $args Array of arguments input in the field as part of the GraphQL query.
348 * @param \WPGraphQL\AppContext $context The app context that gets passed down the resolve tree.
349 * @param \GraphQL\Type\Definition\ResolveInfo $info Info about fields passed down the resolve tree.
350 */
351 protected function pre_should_execute( $source, array $args, AppContext $context, ResolveInfo $info ): bool {
352 $should_execute = true;
353
354 /**
355 * 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.
356 *
357 * @todo This can probably be abstracted to check if _any_ source is private, and not just `PostObject` models.
358 */
359 if ( $source instanceof Post && empty( $source->ID ) ) {
360 $should_execute = false;
361 }
362
363 return $should_execute;
364 }
365
366 /**
367 * Prepares the GraphQL args for use by the connection.
368 *
369 * Useful for modifying the $args before they are passed to $this->get_query_args().
370 *
371 * @param array<string,mixed> $args The GraphQL input args to prepare.
372 *
373 * @return array<string,mixed>
374 */
375 protected function prepare_args( array $args ): array {
376 return $args;
377 }
378
379 /**
380 * The maximum number of items that should be returned by the query.
381 *
382 * This is filtered by `graphql_connection_max_query_amount` in ::get_query_amount().
383 */
384 protected function max_query_amount(): int {
385 return 100;
386 }
387
388 /**
389 * The default query class to use for the connection.
390 *
391 * Should return null if the resolver does not use a query class to fetch the data.
392 *
393 * @return ?class-string<TQueryClass>
394 */
395 protected function query_class(): ?string {
396 return null;
397 }
398
399 /**
400 * Validates the query class. Will be ignored if the Connection Resolver does not use a query class.
401 *
402 * 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.
403 *
404 * @param string $query_class The query class to validate.
405 */
406 protected function is_valid_query_class( string $query_class ): bool {
407 return method_exists( $query_class, 'query' );
408 }
409
410 /**
411 * Executes the query and returns the results.
412 *
413 * 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.
414 *
415 * If the resolver does not rely on a query class, this should be overloaded to return the data directly.
416 *
417 * @param array<string,mixed> $query_args The query args to use to query the data.
418 *
419 * @return TQueryClass
420 *
421 * @throws \GraphQL\Error\InvariantViolation If the query class is not valid.
422 */
423 protected function query( array $query_args ) {
424 // If there is no query class, we need the child class to overload this method.
425 $query_class = $this->get_query_class();
426
427 if ( empty( $query_class ) ) {
428 throw new InvariantViolation(
429 sprintf(
430 // translators: %s is the name of the connection resolver class.
431 esc_html__( 'The %s class does not rely on a query class. Please define a `query()` method to return the data directly.', 'wp-graphql' ),
432 static::class
433 )
434 );
435 }
436
437 return new $query_class( $query_args );
438 }
439
440 /**
441 * Determine whether or not the query should execute.
442 *
443 * Return true to execute, return false to prevent execution.
444 *
445 * Various criteria can be used to determine whether a Connection Query should be executed.
446 *
447 * 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.
448 *
449 * Runs only if `pre_should_execute()` returns true.
450 *
451 * @todo This is public for b/c but it should be protected.
452 *
453 * @return bool
454 */
455 public function should_execute() {
456 return true;
457 }
458
459 /**
460 * Returns the offset for a given cursor.
461 *
462 * Connections that use a string-based offset should override this method.
463 *
464 * @param ?string $cursor The cursor to convert to an offset.
465 *
466 * @return int|mixed
467 */
468 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.
469 $offset = false;
470
471 // We avoid using ArrayConnection::cursorToOffset() because it assumes an `int` offset.
472 if ( ! empty( $cursor ) ) {
473 $offset = substr( base64_decode( $cursor ), strlen( 'arrayconnection:' ) );
474 }
475
476 /**
477 * We assume a numeric $offset is an integer ID.
478 * If it isn't this method should be overridden by the child class.
479 */
480 return is_numeric( $offset ) ? absint( $offset ) : $offset;
481 }
482
483 /**
484 * Validates Model.
485 *
486 * If model isn't a class with a `fields` member, this function with have be overridden in
487 * the Connection class.
488 *
489 * @param \WPGraphQL\Model\Model|mixed $model The model being validated.
490 *
491 * @return bool
492 */
493 protected function is_valid_model( $model ) {
494 return isset( $model->fields ) && ! empty( $model->fields );
495 }
496
497 /**
498 * ====================
499 * Public Getters
500 *
501 * These methods are used to get the properties of the class instance.
502 *
503 * 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.
504 * ====================
505 */
506
507 /**
508 * Returns the source of the connection
509 *
510 * @return mixed
511 */
512 public function get_source() {
513 return $this->source;
514 }
515
516 /**
517 * Returns the AppContext of the connection.
518 */
519 public function get_context(): AppContext {
520 return $this->context;
521 }
522
523 /**
524 * Returns the ResolveInfo of the connection.
525 */
526 public function get_info(): ResolveInfo {
527 return $this->info;
528 }
529
530 /**
531 * Returns the loader name.
532 *
533 * If $loader_name is not initialized, this plugin will initialize it.
534 *
535 * @return string
536 *
537 * @throws \GraphQL\Error\InvariantViolation
538 */
539 public function get_loader_name() {
540 // Only initialize the loader_name property once.
541 if ( ! isset( $this->loader_name ) ) {
542 $name = $this->loader_name();
543
544 // This is a b/c check because `loader_name()` is not abstract.
545 if ( empty( $name ) ) {
546 throw new InvariantViolation(
547 sprintf(
548 // translators: %s is the name of the connection resolver class.
549 esc_html__( 'Class %s does not implement a valid method `loader_name()`.', 'wp-graphql' ),
550 esc_html( static::class )
551 )
552 );
553 }
554
555 /**
556 * Filters the loader name.
557 * This is the name of the registered DataLoader that will be used to load the data for the connection.
558 *
559 * @param string $loader_name The name of the loader.
560 * @param self $resolver The AbstractConnectionResolver instance.
561 * @hookGroup connections
562 * @since 0.0.6
563 */
564 $name = apply_filters( 'graphql_connection_loader_name', $name, $this );
565
566 // Bail if the loader name is invalid.
567 if ( empty( $name ) || ! is_string( $name ) ) {
568 throw new InvariantViolation( esc_html__( 'The Connection Resolver needs to define a loader name', 'wp-graphql' ) );
569 }
570
571 $this->loader_name = $name;
572 }
573
574 return $this->loader_name;
575 }
576
577 /**
578 * Returns the $args passed to the connection, before any modifications.
579 *
580 * @return array<string,mixed>
581 */
582 public function get_unfiltered_args(): array {
583 return $this->unfiltered_args;
584 }
585
586 /**
587 * Returns the $args passed to the connection.
588 *
589 * @return array<string,mixed>
590 */
591 public function get_args(): array {
592 if ( ! isset( $this->args ) ) {
593 $this->args = $this->prepare_args( $this->get_unfiltered_args() );
594 }
595
596 return $this->args;
597 }
598
599 /**
600 * Returns the amount of items to query from the database.
601 *
602 * 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.
603 *
604 * If the amount requested is greater than the max query amount, a debug message will be included in the GraphQL response.
605 *
606 * @return int
607 */
608 public function get_query_amount() {
609 if ( ! isset( $this->query_amount ) ) {
610 /**
611 * Filter the maximum number of posts per page that should be queried. This prevents queries from being exceedingly resource intensive.
612 *
613 * The default is 100 - unless overloaded by ::max_query_amount() in the child class.
614 *
615 * @param int $max_posts the maximum number of posts per page.
616 * @param mixed $source source passed down from the resolve tree
617 * @param array<string,mixed> $args array of arguments input in the field as part of the GraphQL query
618 * @param \WPGraphQL\AppContext $context Object containing app context that gets passed down the resolve tree
619 * @param \GraphQL\Type\Definition\ResolveInfo $info Info about fields passed down the resolve tree
620 *
621 * @hookGroup connections
622 * @since 0.0.6
623 */
624 $max_query_amount = (int) apply_filters( 'graphql_connection_max_query_amount', $this->max_query_amount(), $this->source, $this->get_args(), $this->context, $this->info );
625
626 // We don't want the requested amount to be lower than 0.
627 $requested_query_amount = (int) max(
628 0,
629 /**
630 * This filter allows to modify the number of nodes the connection should return.
631 *
632 * @param int $amount the requested amount
633 * @param self $resolver Instance of the connection resolver class
634 */
635 apply_filters( 'graphql_connection_amount_requested', $this->get_amount_requested(), $this )
636 );
637
638 if ( $requested_query_amount > $max_query_amount ) {
639 graphql_debug(
640 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 ),
641 [ 'connection' => static::class ]
642 );
643 }
644
645 $this->query_amount = (int) min( $max_query_amount, $requested_query_amount );
646 }
647
648 return $this->query_amount;
649 }
650
651 /**
652 * Gets the query args used by the connection to fetch the data.
653 *
654 * @return array<string,mixed>
655 */
656 public function get_query_args() {
657 if ( ! isset( $this->query_args ) ) {
658 // We pass $this->get_args() to ensure we're using the filtered args.
659 $this->query_args = $this->prepare_query_args( $this->get_args() );
660 }
661
662 return $this->query_args;
663 }
664
665 /**
666 * Gets the query class to be instantiated by the `query()` method.
667 *
668 * If null, the `query()` method will be overloaded to return the data.
669 *
670 * @return ?class-string<TQueryClass>
671 */
672 public function get_query_class(): ?string {
673 if ( ! isset( $this->query_class ) ) {
674 $default_query_class = $this->query_class();
675
676 // Attempt to get the query class from the context.
677 $context = $this->get_context();
678
679 $query_class = ! empty( $context->queryClass ) ? $context->queryClass : $default_query_class;
680
681 /**
682 * Filters the `$query_class` that will be used to execute the query.
683 *
684 * 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`).
685 *
686 * @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.
687 * @param self $resolver Instance of the AbstractConnectionResolver
688 * @hookGroup connections
689 * @since 0.0.6
690 */
691 $this->query_class = apply_filters( 'graphql_connection_query_class', $query_class, $this );
692 }
693
694 return $this->query_class;
695 }
696
697 /**
698 * Returns whether the connection should execute.
699 *
700 * If conditions are met that should prevent the execution, we can bail from resolving early, before the query is executed.
701 */
702 public function get_should_execute(): bool {
703 // If `pre_should_execute()` or other logic has yet to run, we should run the full `should_execute()` logic.
704 if ( ! isset( $this->should_execute ) ) {
705 $this->should_execute = $this->should_execute();
706 }
707
708 return $this->should_execute;
709 }
710
711 /**
712 * Gets the results of the executed query.
713 *
714 * @return TQueryClass
715 */
716 public function get_query() {
717 if ( ! isset( $this->query ) ) {
718 /**
719 * When this filter returns anything but null, it will be used as the resolved query, and the default query execution will be skipped.
720 *
721 * @param null $query The query to return. Return null to use the default query execution.
722 * @param self $resolver The connection resolver instance.
723 * @hookGroup connections
724 * @since 0.0.6
725 */
726 $query = apply_filters( 'graphql_connection_pre_get_query', null, $this );
727
728 if ( null === $query ) {
729
730 // Validates the query class before it is used in the query() method.
731 $this->validate_query_class();
732
733 $query = $this->query( $this->get_query_args() );
734 }
735
736 $this->query = $query;
737 }
738
739 return $this->query;
740 }
741
742 /**
743 * Returns an array of IDs for the connection.
744 *
745 * These IDs have been fetched from the query with all the query args applied,
746 * then sliced (overfetching by 1) by pagination args.
747 *
748 * @return int[]|string[]
749 */
750 public function get_ids() {
751 if ( ! isset( $this->ids ) ) {
752 $this->ids = $this->prepare_ids();
753 }
754
755 return $this->ids;
756 }
757
758 /**
759 * Get the nodes from the query.
760 *
761 * @uses AbstractConnectionResolver::get_ids_for_nodes()
762 *
763 * @return array<int|string,mixed|\WPGraphQL\Model\Model|null>
764 */
765 public function get_nodes() {
766 if ( ! isset( $this->nodes ) ) {
767 $this->nodes = $this->prepare_nodes();
768 }
769
770 return $this->nodes;
771 }
772
773 /**
774 * Get the edges from the nodes.
775 *
776 * @return array<string,mixed>[]
777 */
778 public function get_edges() {
779 if ( ! isset( $this->edges ) ) {
780 $this->edges = $this->prepare_edges( $this->get_nodes() );
781 }
782
783 return $this->edges;
784 }
785
786 /**
787 * Returns pageInfo for the connection
788 *
789 * @return array<string,mixed>
790 */
791 public function get_page_info() {
792 if ( ! isset( $this->page_info ) ) {
793 $page_info = $this->prepare_page_info();
794
795 /**
796 * Filter the pageInfo that is returned to the connection.
797 *
798 * This filter allows for additional fields to be filtered into the pageInfo
799 * of a connection, such as "totalCount", etc, because the filter has enough
800 * context of the query, args, request, etc to be able to calculate and return
801 * that information.
802 *
803 * example:
804 *
805 * You would want to register a "total" field to the PageInfo type, then filter
806 * the pageInfo to return the total for the query, something to this tune:
807 *
808 * add_filter( 'graphql_connection_page_info', function( $page_info, $connection ) {
809 *
810 * $page_info['total'] = null;
811 *
812 * if ( $connection->query instanceof WP_Query ) {
813 * if ( isset( $connection->query->found_posts ) {
814 * $page_info['total'] = (int) $connection->query->found_posts;
815 * }
816 * }
817 *
818 * return $page_info;
819 *
820 * });
821 *
822 * @param array<string,mixed> $page_info The pageInfo payload for the connection.
823 * @param self $resolver Instance of the connection resolver.
824 * @hookGroup connections
825 * @since 0.0.6
826 */
827 $this->page_info = apply_filters( 'graphql_connection_page_info', $page_info, $this );
828 }
829
830 return $this->page_info;
831 }
832
833 /**
834 * ===============================
835 * Public setters
836 *
837 * These are used to directly modify the instance properties from outside the class.
838 * ===============================
839 */
840
841 /**
842 * Given a key and value, this sets a query_arg which will modify the query_args used by ::get_query();
843 *
844 * @param string $key The key of the query arg to set
845 * @param mixed $value The value of the query arg to set
846 *
847 * @return static
848 */
849 public function set_query_arg( $key, $value ) {
850 $this->query_args[ $key ] = $value;
851 return $this;
852 }
853
854 /**
855 * Overloads the query_class which will be used to instantiate the query.
856 *
857 * @param class-string<TQueryClass> $query_class The class to use for the query. If empty, this will reset to the default query class.
858 *
859 * @return static
860 */
861 public function set_query_class( string $query_class ) {
862 $this->query_class = $query_class ?: $this->query_class();
863
864 return $this;
865 }
866
867 /**
868 * Whether the connection should resolve as a one-to-one connection.
869 *
870 * @return static
871 */
872 public function one_to_one() {
873 $this->one_to_one = true;
874
875 return $this;
876 }
877
878 /**
879 * Gets whether or not the query should execute, BEFORE any data is fetched or altered, filtered by 'graphql_connection_pre_should_execute'.
880 *
881 * @param mixed $source The source that's passed down the GraphQL queries.
882 * @param array<string,mixed> $args The inputArgs on the field.
883 * @param \WPGraphQL\AppContext $context The AppContext passed down the GraphQL tree.
884 * @param \GraphQL\Type\Definition\ResolveInfo $info The ResolveInfo passed down the GraphQL tree.
885 */
886 protected function get_pre_should_execute( $source, array $args, AppContext $context, ResolveInfo $info ): bool {
887 $should_execute = $this->pre_should_execute( $source, $args, $context, $info );
888
889 /**
890 * Filters whether or not the query should execute, BEFORE any data is fetched or altered.
891 *
892 * 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.
893 *
894 * For more in-depth checks, use the `graphql_connection_should_execute` filter instead.
895 *
896 * @param bool $should_execute Whether or not the query should execute.
897 * @param mixed $source The source that's passed down the GraphQL queries.
898 * @param array<string,mixed> $args The inputArgs on the field.
899 * @param \WPGraphQL\AppContext $context The AppContext passed down the GraphQL tree.
900 * @param \GraphQL\Type\Definition\ResolveInfo $info The ResolveInfo passed down the GraphQL tree.
901 * @hookGroup connections
902 * @since 0.0.6
903 */
904 return apply_filters( 'graphql_connection_pre_should_execute', $should_execute, $source, $args, $context, $info );
905 }
906
907 /**
908 * Returns the loader.
909 *
910 * If $loader is not initialized, this method will initialize it.
911 *
912 * @return \WPGraphQL\Data\Loader\AbstractDataLoader
913 */
914 protected function get_loader() {
915 // If the loader isn't set, set it.
916 if ( ! isset( $this->loader ) ) {
917 $name = $this->get_loader_name();
918
919 $this->loader = $this->context->get_loader( $name );
920 }
921
922 return $this->loader;
923 }
924
925 /**
926 * Returns the amount of items requested from the connection.
927 *
928 * @return int
929 *
930 * @throws \GraphQL\Error\UserError If the `first` or `last` args are used together.
931 */
932 public function get_amount_requested() {
933 /**
934 * Filters the default query amount for a connection, if no `first` or `last` GraphQL argument is supplied.
935 *
936 * @param int $amount_requested The default query amount for a connection.
937 * @param self $resolver Instance of the Connection Resolver.
938 * @hookGroup connections
939 * @since 0.0.6
940 */
941 $amount_requested = apply_filters( 'graphql_connection_default_query_amount', 10, $this );
942
943 // @todo This should use ::get_args() when b/c is not a concern.
944 $args = $this->args;
945
946 /**
947 * If both first & last are used in the input args, throw an exception.
948 */
949 if ( ! empty( $args['first'] ) && ! empty( $args['last'] ) ) {
950 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' ) );
951 }
952
953 /**
954 * Get the key to use for the query amount.
955 * We avoid a ternary here for unit testing.
956 */
957 $args_key = ! empty( $args['first'] ) && is_int( $args['first'] ) ? 'first' : null;
958 if ( null === $args_key ) {
959 $args_key = ! empty( $args['last'] ) && is_int( $args['last'] ) ? 'last' : null;
960 }
961
962 /**
963 * If the key is set, and is a positive integer, use it for the $amount_requested
964 * but if it's set to anything that isn't a positive integer, throw an exception
965 */
966 if ( null !== $args_key && isset( $args[ $args_key ] ) ) {
967 if ( 0 > $args[ $args_key ] ) {
968 throw new UserError(
969 sprintf(
970 // translators: %s: The name of the arg that was invalid
971 esc_html__( '%s must be a positive integer.', 'wp-graphql' ),
972 esc_html( $args_key )
973 )
974 );
975 }
976
977 $amount_requested = $args[ $args_key ];
978 }
979
980 return (int) $amount_requested;
981 }
982
983 /**
984 * =====================
985 * Resolver lifecycle methods
986 *
987 * 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.
988 * =====================
989 */
990
991 /**
992 * Get the connection to return to the Connection Resolver
993 *
994 * @return \GraphQL\Deferred
995 */
996 public function get_connection() {
997 $this->execute_and_get_ids();
998
999 /**
1000 * Return a Deferred function to load all buffered nodes before
1001 * returning the connection.
1002 */
1003 return new Deferred(
1004 function () {
1005 // @todo This should use ::get_ids() when b/c is not a concern.
1006 $ids = $this->ids;
1007
1008 if ( ! empty( $ids ) ) {
1009 // Load the ids.
1010 $this->get_loader()->load_many( $ids );
1011 }
1012
1013 /**
1014 * Set the items. These are the "nodes" that make up the connection.
1015 *
1016 * Filters the nodes in the connection
1017 *
1018 * @todo We reinstantiate this here for b/c. Once that is not a concern, we should relocate this filter to ::get_nodes().
1019 *
1020 * @param \WPGraphQL\Model\Model[]|mixed[]|null $nodes The nodes in the connection
1021 * @param self $resolver Instance of the Connection Resolver
1022 * @hookGroup connections
1023 * @since 0.0.6
1024 */
1025 $this->nodes = apply_filters( 'graphql_connection_nodes', $this->get_nodes(), $this );
1026
1027 /**
1028 * Filters the edges in the connection.
1029 *
1030 * @todo We reinstantiate this here for b/c. Once that is not a concern, we should relocate this filter to ::get_edges().
1031 *
1032 * @param array<string,mixed> $edges The edges in the connection
1033 * @param self $resolver Instance of the Connection Resolver
1034 * @hookGroup connections
1035 * @since 0.0.6
1036 */
1037 $this->edges = apply_filters( 'graphql_connection_edges', $this->get_edges(), $this );
1038
1039 // @todo: we should also short-circuit fetching/populating the actual nodes/edges if we only need one result.
1040 if ( true === $this->one_to_one ) {
1041 // For one to one connections, return the first edge.
1042 $first_edge_key = array_key_first( $this->edges );
1043 $connection = isset( $first_edge_key ) && ! empty( $this->edges[ $first_edge_key ] ) ? $this->edges[ $first_edge_key ] : null;
1044 } else {
1045 // For plural connections (default) return edges/nodes/pageInfo
1046 $connection = [
1047 'nodes' => $this->nodes,
1048 'edges' => $this->edges,
1049 'pageInfo' => $this->get_page_info(),
1050 ];
1051 }
1052
1053 /**
1054 * Filter the connection. In some cases, connections will want to provide
1055 * additional information other than edges, nodes, and pageInfo
1056 *
1057 * This filter allows additional fields to be returned to the connection resolver
1058 *
1059 * @param ?array<string,mixed> $connection The connection data being returned. A single edge or null if the connection is one-to-one.
1060 * @param self $resolver The instance of the connection resolver
1061 * @hookGroup connections
1062 * @since 0.3.0
1063 */
1064 $connection = apply_filters( 'graphql_connection', $connection, $this );
1065 return $connection;
1066 }
1067 );
1068 }
1069
1070 /**
1071 * Execute the resolver query and get the data for the connection
1072 *
1073 * @return int[]|string[]
1074 */
1075 public function execute_and_get_ids() {
1076 /**
1077 * If should_execute is explicitly set to false already, we can prevent execution quickly.
1078 * 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.
1079 */
1080 $should_execute = false === $this->should_execute ? false : $this->should_execute();
1081
1082 /**
1083 * Check if the connection should execute. If conditions are met that should prevent
1084 * the execution, we can bail from resolving early, before the query is executed.
1085 *
1086 * Filter whether the connection should execute.
1087 *
1088 * @param bool $should_execute Whether the connection should execute
1089 * @param \WPGraphQL\Data\Connection\AbstractConnectionResolver $connection_resolver Instance of the Connection Resolver
1090 * @hookGroup connections
1091 * @since 0.0.6
1092 */
1093 $this->should_execute = apply_filters( 'graphql_connection_should_execute', $should_execute, $this );
1094
1095 if ( false === $this->should_execute ) {
1096 return [];
1097 }
1098
1099 /**
1100 * Set the query for the resolver, for use as reference in filters, etc
1101 *
1102 * Filter the query. For core data, the query is typically an instance of:
1103 *
1104 * WP_Query
1105 * WP_Comment_Query
1106 * WP_User_Query
1107 * WP_Term_Query
1108 * ...
1109 *
1110 * But in some cases, the actual mechanism for querying data should be overridden. For
1111 * example, perhaps you're using ElasticSearch or Solr (hypothetical) and want to offload
1112 * the query to that instead of a native WP_Query class. You could override this with a
1113 * query to that datasource instead.
1114 *
1115 * @todo We reinstantiate this here for b/c. Once that is not a concern, we should relocate this filter to ::get_query_args().
1116 *
1117 * @param mixed $query Instance of the Query for the resolver
1118 * @param \WPGraphQL\Data\Connection\AbstractConnectionResolver $connection_resolver Instance of the Connection Resolver
1119 * @hookGroup connections
1120 * @since 0.0.6
1121 */
1122 $this->query = apply_filters( 'graphql_connection_query', $this->get_query(), $this );
1123
1124 /**
1125 * Filter the connection IDs
1126 *
1127 * @todo We filter the IDs here for b/c. Once that is not a concern, we should relocate this filter to ::get_ids().
1128 *
1129 * @param int[]|string[] $ids Array of IDs this connection will be resolving
1130 * @param \WPGraphQL\Data\Connection\AbstractConnectionResolver $connection_resolver Instance of the Connection Resolver
1131 * @hookGroup connections
1132 * @since 0.0.6
1133 */
1134 $this->ids = apply_filters( 'graphql_connection_ids', $this->get_ids(), $this );
1135
1136 if ( empty( $this->ids ) ) {
1137 return [];
1138 }
1139
1140 /**
1141 * Buffer the IDs for deferred resolution
1142 */
1143 $this->get_loader()->buffer( $this->ids );
1144
1145 return $this->ids;
1146 }
1147
1148 /**
1149 * Validates the $query_class set on the resolver.
1150 *
1151 * This runs before the query is executed to ensure that the query class is valid.
1152 *
1153 * @throws \GraphQL\Error\InvariantViolation If the query class is invalid.
1154 */
1155 protected function validate_query_class(): void {
1156 $default_query_class = $this->query_class();
1157 $query_class = $this->get_query_class();
1158
1159 // If the default query class is null, then the resolver should not use a query class.
1160 if ( null === $default_query_class ) {
1161 // If the query class is null, then we're good.
1162 if ( null === $query_class ) {
1163 return;
1164 }
1165
1166 throw new InvariantViolation(
1167 sprintf(
1168 // 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.
1169 esc_html__( 'Class %1$s should not use a query class, but is attempting to use the %2$s query class.', 'wp-graphql' ),
1170 static::class,
1171 esc_html( $query_class )
1172 )
1173 );
1174 }
1175
1176 // If there's no query class set, throw an error.
1177 if ( null === $query_class ) {
1178 throw new InvariantViolation(
1179 sprintf(
1180 // translators: %s: The connection resolver class name.
1181 esc_html__( '%s requires a query class, but no query class is set.', 'wp-graphql' ),
1182 static::class
1183 )
1184 );
1185 }
1186
1187 // If the class is invalid, throw an error.
1188 if ( ! class_exists( $query_class ) ) {
1189 throw new InvariantViolation(
1190 sprintf(
1191 // translators: %s: The name of the query class that is set by the resolver.
1192 esc_html__( 'The query class %s does not exist.', 'wp-graphql' ),
1193 esc_html( $query_class )
1194 )
1195 );
1196 }
1197
1198 // If the class is not compatible with our AbstractConnectionResolver::query() method, throw an error.
1199 if ( ! $this->is_valid_query_class( $query_class ) ) {
1200 throw new InvariantViolation(
1201 sprintf(
1202 // translators: %1$s: The name of the query class that is set by the resolver. %2$s: The name of the resolver class.
1203 esc_html__( 'The query class %1$s is not compatible with %2$s.', 'wp-graphql' ),
1204 esc_html( $this->query_class ?? 'unknown-class' ),
1205 static::class
1206 )
1207 );
1208 }
1209 }
1210
1211 /**
1212 * Returns an array slice of IDs, per the Relay Cursor Connection spec.
1213 *
1214 * The resulting array should be overfetched by 1.
1215 *
1216 * @see https://relay.dev/graphql/connections.htm#sec-Pagination-algorithm
1217 *
1218 * @param int[]|string[] $ids The array of IDs from the query to slice, ordered as expected by the GraphQL query.
1219 *
1220 * @since 1.9.0
1221 *
1222 * @return int[]|string[]
1223 */
1224 public function apply_cursors_to_ids( array $ids ) {
1225 if ( empty( $ids ) ) {
1226 return [];
1227 }
1228
1229 // @todo This should use ::get_args() when b/c is not a concern.
1230 $args = $this->args;
1231
1232 // First we slice the array from the front.
1233 if ( ! empty( $args['after'] ) ) {
1234 $offset = $this->get_offset_for_cursor( $args['after'] );
1235 $index = $this->get_array_index_for_offset( $offset, $ids );
1236
1237 if ( false !== $index ) {
1238 // We want to start with the first id after the index.
1239 $ids = array_slice( $ids, $index + 1, null, true );
1240 }
1241 }
1242
1243 // Then we slice the array from the back.
1244 if ( ! empty( $args['before'] ) ) {
1245 $offset = $this->get_offset_for_cursor( $args['before'] );
1246 $index = $this->get_array_index_for_offset( $offset, $ids );
1247
1248 if ( false !== $index ) {
1249 // Because array indexes start at 0, we can overfetch without adding 1 to $index.
1250 $ids = array_slice( $ids, 0, $index, true );
1251 }
1252 }
1253
1254 return $ids;
1255 }
1256
1257 /**
1258 * Gets the array index for the given offset.
1259 *
1260 * @param int|string|false $offset The cursor pagination offset.
1261 * @param int[]|string[] $ids The array of ids from the query.
1262 *
1263 * @return int|false $index The array index of the offset.
1264 */
1265 public function get_array_index_for_offset( $offset, $ids ) {
1266 if ( false === $offset ) {
1267 return false;
1268 }
1269
1270 // We use array_values() to ensure we're getting a positional index, and not a key.
1271 return array_search( $offset, array_values( $ids ), true );
1272 }
1273
1274 /**
1275 * Prepares the nodes for the connection.
1276 *
1277 * @used-by self::get_nodes()
1278 *
1279 * @return array<int|string,mixed|\WPGraphQL\Model\Model|null>
1280 */
1281 protected function prepare_nodes(): array {
1282 $nodes = [];
1283
1284 // These are already sliced and ordered, we're just populating node data.
1285 $ids = $this->get_ids_for_nodes();
1286
1287 foreach ( $ids as $id ) {
1288 $model = $this->get_node_by_id( $id );
1289 if ( true === $this->get_is_valid_model( $model ) ) {
1290 $nodes[ $id ] = $model;
1291 }
1292 }
1293
1294 return $nodes;
1295 }
1296
1297 /**
1298 * Prepares the IDs for the connection.
1299 *
1300 * @used-by self::get_ids()
1301 *
1302 * @return int[]|string[]
1303 */
1304 protected function prepare_ids(): array {
1305 $ids = $this->get_ids_from_query();
1306
1307 return $this->apply_cursors_to_ids( $ids );
1308 }
1309
1310 /**
1311 * Gets the IDs for the currently-paginated slice of nodes.
1312 *
1313 * We slice the array to match the amount of items that was asked for, as we over-fetched by 1 item to calculate pageInfo.
1314 *
1315 * @used-by AbstractConnectionResolver::get_nodes()
1316 *
1317 * @return int[]|string[]
1318 */
1319 public function get_ids_for_nodes() {
1320 // @todo This should use ::get_ids() and get_args() when b/c is not a concern.
1321 $ids = $this->ids;
1322
1323 if ( empty( $ids ) ) {
1324 return [];
1325 }
1326
1327 $args = $this->args;
1328
1329 // If we're going backwards then our overfetched ID is at the front.
1330 if ( ! empty( $args['last'] ) && count( $ids ) > absint( $args['last'] ) ) {
1331 return array_slice( $ids, count( $ids ) - absint( $args['last'] ), $this->get_query_amount(), true );
1332 }
1333
1334 // If we're going forwards, our overfetched ID is at the back.
1335 return array_slice( $ids, 0, $this->get_query_amount(), true );
1336 }
1337
1338 /**
1339 * Given an ID, return the model for the entity or null
1340 *
1341 * @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)
1342 *
1343 * @return mixed|\WPGraphQL\Model\Model|null
1344 */
1345 public function get_node_by_id( $id ) {
1346 return $this->get_loader()->load( $id );
1347 }
1348
1349 /**
1350 * Gets whether or not the model is valid.
1351 *
1352 * @param mixed $model The model being validated.
1353 */
1354 protected function get_is_valid_model( $model ): bool {
1355 $is_valid = $this->is_valid_model( $model );
1356
1357 /**
1358 * Filters whether or not the model is valid.
1359 *
1360 * This is useful when the dataloader is overridden and uses a different model than expected by default.
1361 *
1362 * @param bool $is_valid Whether or not the model is valid.
1363 * @param mixed $model The model being validated
1364 * @param self $resolver The connection resolver instance
1365 * @hookGroup connections
1366 * @since 0.0.6
1367 */
1368 return apply_filters( 'graphql_connection_is_valid_model', $is_valid, $model, $this );
1369 }
1370
1371 /**
1372 * Prepares the edges for the connection.
1373 *
1374 * @used-by self::get_edges()
1375 *
1376 * @param array<int|string,mixed|\WPGraphQL\Model\Model|null> $nodes The nodes for the connection.
1377 *
1378 * @return array<string,mixed>[]
1379 */
1380 protected function prepare_edges( array $nodes ): array {
1381 // Bail early if there are no nodes.
1382 if ( empty( $nodes ) ) {
1383 return [];
1384 }
1385
1386 // The nodes are already ordered, sliced, and populated. What's left is to populate the edge data for each one.
1387 $edges = [];
1388 foreach ( $nodes as $id => $node ) {
1389 $edge = $this->prepare_edge( $id, $node );
1390
1391 /**
1392 * Filter the edge within the connection.
1393 *
1394 * @param array<string,mixed> $edge The edge within the connection
1395 * @param self $resolver Instance of the connection resolver class
1396 * @hookGroup connections
1397 * @since 0.0.6
1398 */
1399 $edge = apply_filters( 'graphql_connection_edge', $edge, $this );
1400
1401 $edges[] = $edge;
1402 }
1403
1404 return $edges;
1405 }
1406
1407 /**
1408 * Prepares a single edge for the connection.
1409 *
1410 * @used-by self::prepare_edges()
1411 *
1412 * @param int|string $id The ID of the node.
1413 * @param mixed|\WPGraphQL\Model\Model|null $node The node for the edge.
1414 *
1415 * @return array<string,mixed>
1416 */
1417 protected function prepare_edge( $id, $node ): array {
1418 return [
1419 'cursor' => $this->get_cursor_for_node( $id ),
1420 'node' => $node,
1421 'source' => $this->get_source(),
1422 'connection' => $this,
1423 ];
1424 }
1425
1426 /**
1427 * Given an ID, a cursor is returned.
1428 *
1429 * @param int|string $id The ID to get the cursor for.
1430 *
1431 * @return string
1432 */
1433 protected function get_cursor_for_node( $id ) {
1434 return base64_encode( 'arrayconnection:' . (string) $id );
1435 }
1436
1437 /**
1438 * Prepares the page info for the connection.
1439 *
1440 * @used-by self::get_page_info()
1441 *
1442 * @return array<string,mixed>
1443 */
1444 protected function prepare_page_info(): array {
1445 return [
1446 'startCursor' => $this->get_start_cursor(),
1447 'endCursor' => $this->get_end_cursor(),
1448 'hasNextPage' => $this->has_next_page(),
1449 'hasPreviousPage' => $this->has_previous_page(),
1450 ];
1451 }
1452
1453 /**
1454 * Determine the start cursor from the connection
1455 *
1456 * @return mixed|string|null
1457 */
1458 public function get_start_cursor() {
1459 $first_edge = $this->edges && ! empty( $this->edges ) ? $this->edges[0] : null;
1460
1461 return isset( $first_edge['cursor'] ) ? $first_edge['cursor'] : null;
1462 }
1463
1464 /**
1465 * Determine the end cursor from the connection
1466 *
1467 * @return mixed|string|null
1468 */
1469 public function get_end_cursor() {
1470 $last_edge = ! empty( $this->edges ) ? $this->edges[ count( $this->edges ) - 1 ] : null;
1471
1472 return isset( $last_edge['cursor'] ) ? $last_edge['cursor'] : null;
1473 }
1474
1475 /**
1476 * Gets the offset for the `after` cursor.
1477 *
1478 * @return int|string|null
1479 */
1480 public function get_after_offset() {
1481 // @todo This should use ::get_args() when b/c is not a concern.
1482 $args = $this->args;
1483
1484 if ( ! empty( $args['after'] ) ) {
1485 return $this->get_offset_for_cursor( $args['after'] );
1486 }
1487
1488 return null;
1489 }
1490
1491 /**
1492 * Gets the offset for the `before` cursor.
1493 *
1494 * @return int|string|null
1495 */
1496 public function get_before_offset() {
1497 // @todo This should use ::get_args() when b/c is not a concern.
1498 $args = $this->args;
1499
1500 if ( ! empty( $args['before'] ) ) {
1501 return $this->get_offset_for_cursor( $args['before'] );
1502 }
1503
1504 return null;
1505 }
1506
1507 /**
1508 * Whether there is a next page in the connection.
1509 *
1510 * 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.
1511 *
1512 * @return bool
1513 */
1514 public function has_next_page() {
1515 // @todo This should use ::get_ids() and ::get_args() when b/c is not a concern.
1516 $args = $this->args;
1517
1518 if ( ! empty( $args['first'] ) ) {
1519 $ids = $this->ids;
1520
1521 return ! empty( $ids ) && count( $ids ) > $this->get_query_amount();
1522 }
1523
1524 $before_offset = $this->get_before_offset();
1525
1526 if ( $before_offset ) {
1527 return $this->is_valid_offset( $before_offset );
1528 }
1529
1530 return false;
1531 }
1532
1533 /**
1534 * Whether there is a previous page in the connection.
1535 *
1536 * 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.
1537 *
1538 * @return bool
1539 */
1540 public function has_previous_page() {
1541 // @todo This should use ::get_ids() and ::get_args() when b/c is not a concern.
1542 $args = $this->args;
1543
1544 if ( ! empty( $args['last'] ) ) {
1545 $ids = $this->ids;
1546
1547 return ! empty( $ids ) && count( $ids ) > $this->get_query_amount();
1548 }
1549
1550 $after_offset = $this->get_after_offset();
1551 if ( $after_offset ) {
1552 return $this->is_valid_offset( $after_offset );
1553 }
1554
1555 return false;
1556 }
1557
1558 /**
1559 * DEPRECATED METHODS
1560 *
1561 * These methods are deprecated and will be removed in a future release.
1562 */
1563
1564 /**
1565 * @todo remove in 3.0.0
1566 * @deprecated 1.11.0 in favor of $this->get_args();
1567 * @codeCoverageIgnore
1568 *
1569 * @return array<string,mixed>
1570 */
1571 public function getArgs(): array {
1572 _doing_it_wrong(
1573 __METHOD__,
1574 sprintf(
1575 // translators: %s: The name of the method that should be used instead.
1576 esc_html__( 'This will be removed in the next major release. Use %s instead.', 'wp-graphql' ),
1577 static::class . '::get_args()'
1578 ),
1579 '1.11.0'
1580 );
1581
1582 return $this->get_args();
1583 }
1584
1585 /**
1586 * @todo remove in 3.0.0
1587 * @deprecated 0.3.0
1588 * @codeCoverageIgnore
1589 *
1590 * @param string $key The key of the query arg to set
1591 * @param mixed $value The value of the query arg to set
1592 *
1593 * @return static
1594 */
1595 public function setQueryArg( $key, $value ) {
1596 _doing_it_wrong(
1597 __METHOD__,
1598 sprintf(
1599 // translators: %s: The name of the method that should be used instead.
1600 esc_html__( 'This will be removed in the next major release. Use %s instead.', 'wp-graphql' ),
1601 static::class . '::set_query_arg()'
1602 ),
1603 '0.3.0'
1604 );
1605
1606 return $this->set_query_arg( $key, $value );
1607 }
1608
1609 /**
1610 * @todo remove in 3.0.0
1611 * @deprecated 1.9.0
1612 * @codeCoverageIgnore
1613 *
1614 * @return int|mixed
1615 */
1616 public function get_offset() {
1617 _doing_it_wrong(
1618 __METHOD__,
1619 sprintf(
1620 // translators: %s: The name of the method that should be used instead.
1621 esc_html__( 'This will be removed in the next major release. Use %s instead.', 'wp-graphql' ),
1622 static::class . '::get_offset_for_cursor()'
1623 ),
1624 '1.9.0'
1625 );
1626
1627 // Using shorthand since this is for deprecated code.
1628 $cursor = $this->args['after'] ?? null;
1629 $cursor = $cursor ?: ( $this->args['before'] ?? null );
1630
1631 return $this->get_offset_for_cursor( $cursor );
1632 }
1633
1634 /**
1635 * @todo remove in 3.0.0
1636 * @deprecated 1.24.0 in favor of $this->get_source().
1637 * @codeCoverageIgnore
1638 *
1639 * @return mixed
1640 */
1641 public function getSource() {
1642 _doing_it_wrong(
1643 __METHOD__,
1644 sprintf(
1645 // translators: %s: The name of the method that should be used instead.
1646 esc_html__( 'This will be removed in the next major release. Use %s instead.', 'wp-graphql' ),
1647 static::class . '::get_source()'
1648 ),
1649 '1.24.0'
1650 );
1651
1652 return $this->get_source();
1653 }
1654
1655 /**
1656 * @todo remove in 3.0.0
1657 * @deprecated 1.24.0 in favor of $this->get_context().
1658 * @codeCoverageIgnore
1659 */
1660 public function getContext(): AppContext {
1661 _doing_it_wrong(
1662 __METHOD__,
1663 sprintf(
1664 // translators: %s: The name of the method that should be used instead.
1665 esc_html__( 'This will be removed in the next major release. Use %s instead.', 'wp-graphql' ),
1666 static::class . '::get_context()'
1667 ),
1668 '1.24.0'
1669 );
1670
1671 return $this->get_context();
1672 }
1673
1674 /**
1675 * @todo remove in 3.0.0
1676 * @deprecated 1.24.0 in favor of $this->get_info().
1677 * @codeCoverageIgnore
1678 */
1679 public function getInfo(): ResolveInfo {
1680 _doing_it_wrong(
1681 __METHOD__,
1682 sprintf(
1683 // translators: %s: The name of the method that should be used instead.
1684 esc_html__( 'This will be removed in the next major release. Use %s instead.', 'wp-graphql' ),
1685 static::class . '::get_info()'
1686 ),
1687 '1.24.0'
1688 );
1689
1690 return $this->get_info();
1691 }
1692
1693 /**
1694 * @todo remove in 3.0.0
1695 * @deprecated 1.24.0 in favor of $this->get_should_execute().
1696 * @codeCoverageIgnore
1697 */
1698 public function getShouldExecute(): bool {
1699 _doing_it_wrong(
1700 __METHOD__,
1701 sprintf(
1702 // translators: %s: The name of the method that should be used instead.
1703 esc_html__( 'This will be removed in the next major release. Use %s instead.', 'wp-graphql' ),
1704 static::class . '::get_should_execute()'
1705 ),
1706 '1.24.0'
1707 );
1708
1709 return $this->get_should_execute();
1710 }
1711
1712 /**
1713 * @todo remove in 3.0.0
1714 * @deprecated 1.24.0 in favor of $this->get_loader().
1715 * @codeCoverageIgnore
1716 *
1717 * @return \WPGraphQL\Data\Loader\AbstractDataLoader
1718 */
1719 protected function getLoader() {
1720 _doing_it_wrong(
1721 __METHOD__,
1722 sprintf(
1723 // translators: %s: The name of the method that should be used instead.
1724 esc_html__( 'This will be removed in the next major release. Use %s instead.', 'wp-graphql' ),
1725 static::class . '::get_loader()'
1726 ),
1727 '1.24.0'
1728 );
1729
1730 return $this->get_loader();
1731 }
1732 }
1733