/
AbstractConnectionResolver.php
1643 lines (1442 loc) · 51 KB
/
AbstractConnectionResolver.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?php
namespace WPGraphQL\Data\Connection;
use GraphQL\Deferred;
use GraphQL\Error\InvariantViolation;
use GraphQL\Error\UserError;
use GraphQL\Type\Definition\ResolveInfo;
use WPGraphQL\AppContext;
use WPGraphQL\Model\Post;
/**
* Class AbstractConnectionResolver
*
* 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.
*
* @package WPGraphQL\Data\Connection
*
* The template type `TQueryClass` is used by static analysis tools to correctly typehint the query class used by the Connection Resolver.
* Classes that extend `AbstractConnectionResolver` should add `@extends @extends \WPGraphQL\Data\Connection\AbstractConnectionResolver<\MY_QUERY_CLASS>` to the class dockblock to get proper hinting.
* E.g. `@extends \WPGraphQL\Data\Connection\AbstractConnectionResolver<\WP_Term_Query>`
*
* @template TQueryClass
*/
abstract class AbstractConnectionResolver {
/**
* The source from the field calling the connection.
*
* @var \WPGraphQL\Model\Model|mixed[]|mixed
*/
protected $source;
/**
* The args input before it is filtered and prepared by the constructor.
*
* @var array<string,mixed>
*/
protected $unfiltered_args;
/**
* The args input on the field calling the connection.
*
* Filterable by `graphql_connection_args`.
*
* @var ?array<string,mixed>
*/
protected $args;
/**
* The AppContext for the GraphQL Request
*
* @var \WPGraphQL\AppContext
*/
protected $context;
/**
* The ResolveInfo for the GraphQL Request
*
* @var \GraphQL\Type\Definition\ResolveInfo
*/
protected $info;
/**
* The query args used to query for data to resolve the connection.
*
* Filterable by `graphql_connection_query_args`.
*
* @var ?array<string,mixed>
*/
protected $query_args;
/**
* Whether the connection resolver should execute.
*
* If `false`, the connection resolve will short-circuit and return an empty array.
*
* Filterable by `graphql_connection_pre_should_execute` and `graphql_connection_should_execute`.
*
* @var ?bool
*/
protected $should_execute;
/**
* The loader name.
*
* Defaults to `loader_name()` and filterable by `graphql_connection_loader_name`.
*
* @var ?string
*/
protected $loader_name;
/**
* The loader the resolver is configured to use.
*
* @var ?\WPGraphQL\Data\Loader\AbstractDataLoader
*/
protected $loader;
/**
* Whether the connection is a one to one connection. Default false.
*
* @var bool
*/
public $one_to_one = false;
/**
* 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.
*
* Examples `WP_Query`, `WP_Comment_Query`, `WC_Query`, `/My/Namespaced/CustomQuery`, etc.
*
* @var ?class-string<TQueryClass>
*/
protected $query_class;
/**
* The instantiated query array/object used to fetch the data.
*
* Examples:
* return new WP_Query( $this->get_query_args() );
* return new WP_Comment_Query( $this->get_query_args() );
* return new WP_Term_Query( $this->get_query_args() );
*
* Whatever it is will be passed through filters so that fields throughout
* have context from what was queried and can make adjustments as needed, such
* as exposing `totalCount` in pageInfo, etc.
*
* Filterable by `graphql_connection_pre_get_query` and `graphql_connection_query`.
*
* @var ?TQueryClass
*/
protected $query;
/**
* @var mixed[]
*
* @deprecated 1.26.0 This is an artifact and is unused. It will be removed in a future release.
*/
protected $items;
/**
* The IDs returned from the query.
*
* The IDs are sliced to confirm with the pagination args, and overfetched by one.
*
* Filterable by `graphql_connection_ids`.
*
* @var int[]|string[]|null
*/
protected $ids;
/**
* The nodes (usually GraphQL models) returned from the query.
*
* Filterable by `graphql_connection_nodes`.
*
* @var \WPGraphQL\Model\Model[]|mixed[]|null
*/
protected $nodes;
/**
* The edges for the connection.
*
* Filterable by `graphql_connection_edges`.
*
* @var ?array<string,mixed>[]
*/
protected $edges;
/**
* The page info for the connection.
*
* Filterable by `graphql_connection_page_info`.
*
* @var ?array<string,mixed>
*/
protected $page_info;
/**
* The query amount to return for the connection.
*
* @var ?int
*/
protected $query_amount;
/**
* ConnectionResolver constructor.
*
* @param mixed $source Source passed down from the resolve tree
* @param array<string,mixed> $args Array of arguments input in the field as part of the GraphQL query.
* @param \WPGraphQL\AppContext $context The app context that gets passed down the resolve tree.
* @param \GraphQL\Type\Definition\ResolveInfo $info Info about fields passed down the resolve tree.
*/
public function __construct( $source, array $args, AppContext $context, ResolveInfo $info ) {
// Set the source (the root object), context, resolveInfo, and unfiltered args for the resolver.
$this->source = $source;
$this->unfiltered_args = $args;
$this->context = $context;
$this->info = $info;
/**
* @todo This exists for b/c, where extenders may be directly accessing `$this->args` in ::get_loader() or even `::get_args()`.
* We can call it later in the lifecycle once that's no longer the case.
*/
$this->args = $this->get_args();
// Pre-check if the connection should execute so we can skip expensive logic if we already know it shouldn't execute.
if ( ! $this->get_pre_should_execute( $this->source, $this->unfiltered_args, $this->context, $this->info ) ) {
$this->should_execute = false;
}
// Get the loader for the Connection.
$this->loader = $this->get_loader();
/**
* Filters the GraphQL args before they are used in get_query_args().
*
* @todo We reinstantate this here for b/c. Once that is not a concern, we should relocate this filter to ::get_args().
*
* @param array<string,mixed> $args The GraphQL args passed to the resolver.
* @param \WPGraphQL\Data\Connection\AbstractConnectionResolver $connection_resolver Instance of the ConnectionResolver.
* @param array<string,mixed> $unfiltered_args Array of arguments input in the field as part of the GraphQL query.
*
* @since 1.11.0
*/
$this->args = apply_filters( 'graphql_connection_args', $this->args, $this, $this->get_unfiltered_args() );
// Get the query amount for the connection.
$this->query_amount = $this->get_query_amount();
/**
* Filters the query args before they are used in the query.
*
* @todo We reinstantate this here for b/c. Once that is not a concern, we should relocate this filter to ::get_query_args().
*
* @param array<string,mixed> $query_args The query args to be used with the executable query to get data.
* @param \WPGraphQL\Data\Connection\AbstractConnectionResolver $connection_resolver Instance of the ConnectionResolver
* @param array<string,mixed> $unfiltered_args Array of arguments input in the field as part of the GraphQL query.
*/
$this->query_args = apply_filters( 'graphql_connection_query_args', $this->get_query_args(), $this, $this->get_unfiltered_args() );
// Get the query class for the connection.
$this->query_class = $this->get_query_class();
// The rest of the class properties are set when `$this->get_connection()` is called.
}
/**
* ====================
* Required/Abstract Methods
*
* These methods must be implemented or overloaded in the extending class.
*
* The reason not all methods are abstract is to prevent backwards compatibility issues.
* ====================
*/
/**
* The name of the loader to use for this connection.
*
* Filterable by `graphql_connection_loader_name`.
*
* @todo This is protected for backwards compatibility, but should be abstract and implemented by the child classes.
*/
protected function loader_name(): string {
return '';
}
/**
* Prepares the query args used to fetch the data for the connection.
*
* This accepts the GraphQL args and maps them to a format that can be read by our query class.
* For example, if the ConnectionResolver uses WP_Query to fetch the data, this should return $args for use in `new WP_Query( $args );`
*
* @todo This is protected for backwards compatibility, but should be abstract and implemented by the child classes.
*
* @param array<string,mixed> $args The GraphQL input args passed to the connection.
*
* @return array<string,mixed>
*
* @throws \GraphQL\Error\InvariantViolation If the method is not implemented.
*
* @codeCoverageIgnore
*/
protected function prepare_query_args( array $args ): array {
throw new InvariantViolation(
sprintf(
// translators: %s is the name of the connection resolver class.
esc_html__( 'Class %s does not implement a valid method `prepare_query_args()`.', 'wp-graphql' ),
static::class
)
);
}
/**
* Return an array of ids from the query
*
* Each Query class in WP and potential datasource handles this differently,
* so each connection resolver should handle getting the items into a uniform array of items.
*
* @todo: This is not an abstract function to prevent backwards compatibility issues, so it instead throws an exception.
*
* Classes that extend AbstractConnectionResolver should
* override this method instead of ::get_ids().
*
* @since 1.9.0
*
* @throws \GraphQL\Error\InvariantViolation If child class forgot to implement this.
* @return int[]|string[] the array of IDs.
*/
public function get_ids_from_query() {
throw new InvariantViolation(
sprintf(
// translators: %s is the name of the connection resolver class.
esc_html__( 'Class %s does not implement a valid method `get_ids_from_query()`.', 'wp-graphql' ),
static::class
)
);
}
/**
* Determine whether or not the the offset is valid, i.e the item corresponding to the offset exists.
*
* 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.
*
* @param mixed $offset The offset to validate. Typically a WordPress Database ID
*
* @return bool
*/
abstract public function is_valid_offset( $offset );
/**
* ====================
* The following methods handle the underlying behavior of the connection, and are intended to be overloaded by the child class.
*
* These methods are wrapped in getters which apply the filters and set the properties of the class instance.
* ====================
*/
/**
* Used to determine whether the connection query should be executed. This is useful for short-circuiting the connection resolver before executing the query.
*
* When `pre_should_excecute()` 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.
*
* @param mixed $source Source passed down from the resolve tree
* @param array<string,mixed> $args Array of arguments input in the field as part of the GraphQL query.
* @param \WPGraphQL\AppContext $context The app context that gets passed down the resolve tree.
* @param \GraphQL\Type\Definition\ResolveInfo $info Info about fields passed down the resolve tree.
*/
protected function pre_should_execute( $source, array $args, AppContext $context, ResolveInfo $info ): bool {
$should_execute = true;
/**
* 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.
*
* @todo This can probably be abstracted to check if _any_ source is private, and not just `PostObject` models.
*/
if ( $source instanceof Post && empty( $source->ID ) ) {
$should_execute = false;
}
return $should_execute;
}
/**
* Prepares the GraphQL args for use by the connection.
*
* Useful for modifying the $args before they are passed to $this->get_query_args().
*
* @param array<string,mixed> $args The GraphQL input args to prepare.
*
* @return array<string,mixed>
*/
protected function prepare_args( array $args ): array {
return $args;
}
/**
* The maximum number of items that should be returned by the query.
*
* This is filtered by `graphql_connection_max_query_amount` in ::get_query_amount().
*/
protected function max_query_amount(): int {
return 100;
}
/**
* The default query class to use for the connection.
*
* Should return null if the resolver does not use a query class to fetch the data.
*
* @return ?class-string<TQueryClass>
*/
protected function query_class(): ?string {
return null;
}
/**
* Validates the query class. Will be ignored if the Connection Resolver does not use a query class.
*
* 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.
*
* @param string $query_class The query class to validate.
*/
protected function is_valid_query_class( string $query_class ): bool {
return method_exists( $query_class, 'query' );
}
/**
* Executes the query and returns the results.
*
* 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.
*
* If the resolver does not rely on a query class, this should be overloaded to return the data directly.
*
* @param array<string,mixed> $query_args The query args to use to query the data.
*
* @return TQueryClass
*
* @throws \GraphQL\Error\InvariantViolation If the query class is not valid.
*/
protected function query( array $query_args ) {
// If there is no query class, we need the child class to overload this method.
$query_class = $this->get_query_class();
if ( empty( $query_class ) ) {
throw new InvariantViolation(
sprintf(
// translators: %s is the name of the connection resolver class.
esc_html__( 'The %s class does not rely on a query class. Please define a `query()` method to return the data directly.', 'wp-graphql' ),
static::class
)
);
}
return new $query_class( $query_args );
}
/**
* Determine whether or not the query should execute.
*
* Return true to exeucte, return false to prevent execution.
*
* Various criteria can be used to determine whether a Connection Query should be executed.
*
* 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.
*
* Runs only if `pre_should_execute()` returns true.
*
* @todo This is public for b/c but it should be protected.
*
* @return bool
*/
public function should_execute() {
return true;
}
/**
* Returns the offset for a given cursor.
*
* Connections that use a string-based offset should override this method.
*
* @param ?string $cursor The cursor to convert to an offset.
*
* @return int|mixed
*/
public function get_offset_for_cursor( string $cursor = null ) { // phpcs:ignore PHPCompatibility.FunctionDeclarations.RemovedImplicitlyNullableParam.Deprecated -- This is a breaking change to fix.
$offset = false;
// We avoid using ArrayConnection::cursorToOffset() because it assumes an `int` offset.
if ( ! empty( $cursor ) ) {
$offset = substr( base64_decode( $cursor ), strlen( 'arrayconnection:' ) );
}
/**
* We assume a numeric $offset is an integer ID.
* If it isn't this method should be overridden by the child class.
*/
return is_numeric( $offset ) ? absint( $offset ) : $offset;
}
/**
* Validates Model.
*
* If model isn't a class with a `fields` member, this function with have be overridden in
* the Connection class.
*
* @param \WPGraphQL\Model\Model|mixed $model The model being validated.
*
* @return bool
*/
protected function is_valid_model( $model ) {
return isset( $model->fields ) && ! empty( $model->fields );
}
/**
* ====================
* Public Getters
*
* These methods are used to get the properties of the class instance.
*
* 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.
* ====================
*/
/**
* Returns the source of the connection
*
* @return mixed
*/
public function get_source() {
return $this->source;
}
/**
* Returns the AppContext of the connection.
*/
public function get_context(): AppContext {
return $this->context;
}
/**
* Returns the ResolveInfo of the connection.
*/
public function get_info(): ResolveInfo {
return $this->info;
}
/**
* Returns the loader name.
*
* If $loader_name is not initialized, this plugin will initialize it.
*
* @return string
*
* @throws \GraphQL\Error\InvariantViolation
*/
public function get_loader_name() {
// Only initialize the loader_name property once.
if ( ! isset( $this->loader_name ) ) {
$name = $this->loader_name();
// This is a b/c check because `loader_name()` is not abstract.
if ( empty( $name ) ) {
throw new InvariantViolation(
sprintf(
// translators: %s is the name of the connection resolver class.
esc_html__( 'Class %s does not implement a valid method `loader_name()`.', 'wp-graphql' ),
esc_html( static::class )
)
);
}
/**
* Filters the loader name.
* This is the name of the registered DataLoader that will be used to load the data for the connection.
*
* @param string $loader_name The name of the loader.
* @param self $resolver The AbstractConnectionResolver instance.
*/
$name = apply_filters( 'graphql_connection_loader_name', $name, $this );
// Bail if the loader name is invalid.
if ( empty( $name ) || ! is_string( $name ) ) {
throw new InvariantViolation( esc_html__( 'The Connection Resolver needs to define a loader name', 'wp-graphql' ) );
}
$this->loader_name = $name;
}
return $this->loader_name;
}
/**
* Returns the $args passed to the connection, before any modifications.
*
* @return array<string,mixed>
*/
public function get_unfiltered_args(): array {
return $this->unfiltered_args;
}
/**
* Returns the $args passed to the connection.
*
* @return array<string,mixed>
*/
public function get_args(): array {
if ( ! isset( $this->args ) ) {
$this->args = $this->prepare_args( $this->get_unfiltered_args() );
}
return $this->args;
}
/**
* Returns the amount of items to query from the database.
*
* 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.
*
* If the amount requested is greater than the max query amount, a debug message will be included in the GraphQL response.
*
* @return int
*/
public function get_query_amount() {
if ( ! isset( $this->query_amount ) ) {
/**
* Filter the maximum number of posts per page that should be queried. This prevents queries from being exceedingly resource intensive.
*
* The default is 100 - unless overloaded by ::max_query_amount() in the child class.
*
* @param int $max_posts the maximum number of posts per page.
* @param mixed $source source passed down from the resolve tree
* @param array<string,mixed> $args array of arguments input in the field as part of the GraphQL query
* @param \WPGraphQL\AppContext $context Object containing app context that gets passed down the resolve tree
* @param \GraphQL\Type\Definition\ResolveInfo $info Info about fields passed down the resolve tree
*
* @since 0.0.6
*/
$max_query_amount = (int) apply_filters( 'graphql_connection_max_query_amount', $this->max_query_amount(), $this->source, $this->get_args(), $this->context, $this->info );
// We don't want the requested amount to be lower than 0.
$requested_query_amount = (int) max(
0,
/**
* This filter allows to modify the number of nodes the connection should return.
*
* @param int $amount the requested amount
* @param self $resolver Instance of the connection resolver class
*/
apply_filters( 'graphql_connection_amount_requested', $this->get_amount_requested(), $this )
);
if ( $requested_query_amount > $max_query_amount ) {
graphql_debug(
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 ),
[ 'connection' => static::class ]
);
}
$this->query_amount = (int) min( $max_query_amount, $requested_query_amount );
}
return $this->query_amount;
}
/**
* Gets the query args used by the connection to fetch the data.
*
* @return array<string,mixed>
*/
public function get_query_args() {
if ( ! isset( $this->query_args ) ) {
// We pass $this->get_args() to ensure we're using the filtered args.
$this->query_args = $this->prepare_query_args( $this->get_args() );
}
return $this->query_args;
}
/**
* Gets the query class to be instantiated by the `query()` method.
*
* If null, the `query()` method will be overloaded to return the data.
*
* @return ?class-string<TQueryClass>
*/
public function get_query_class(): ?string {
if ( ! isset( $this->query_class ) ) {
$default_query_class = $this->query_class();
// Attempt to get the query class from the context.
$context = $this->get_context();
$query_class = ! empty( $context->queryClass ) ? $context->queryClass : $default_query_class;
/**
* Filters the `$query_class` that will be used to execute the query.
*
* 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`).
*
* @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.
* @param self $resolver Instance of the AbstractConnectionResolver
*/
$this->query_class = apply_filters( 'graphql_connection_query_class', $query_class, $this );
}
return $this->query_class;
}
/**
* Returns whether the connection should execute.
*
* If conditions are met that should prevent the execution, we can bail from resolving early, before the query is executed.
*/
public function get_should_execute(): bool {
// If `pre_should_execute()` or other logic has yet to run, we should run the full `should_execute()` logic.
if ( ! isset( $this->should_execute ) ) {
$this->should_execute = $this->should_execute();
}
return $this->should_execute;
}
/**
* Gets the results of the executed query.
*
* @return TQueryClass
*/
public function get_query() {
if ( ! isset( $this->query ) ) {
/**
* When this filter returns anything but null, it will be used as the resolved query, and the default query execution will be skipped.
*
* @param null $query The query to return. Return null to use the default query execution.
* @param self $resolver The connection resolver instance.
*/
$query = apply_filters( 'graphql_connection_pre_get_query', null, $this );
if ( null === $query ) {
// Validates the query class before it is used in the query() method.
$this->validate_query_class();
$query = $this->query( $this->get_query_args() );
}
$this->query = $query;
}
return $this->query;
}
/**
* Returns an array of IDs for the connection.
*
* These IDs have been fetched from the query with all the query args applied,
* then sliced (overfetching by 1) by pagination args.
*
* @return int[]|string[]
*/
public function get_ids() {
if ( ! isset( $this->ids ) ) {
$this->ids = $this->prepare_ids();
}
return $this->ids;
}
/**
* Get the nodes from the query.
*
* @uses AbstractConnectionResolver::get_ids_for_nodes()
*
* @return array<int|string,mixed|\WPGraphQL\Model\Model|null>
*/
public function get_nodes() {
if ( ! isset( $this->nodes ) ) {
$this->nodes = $this->prepare_nodes();
}
return $this->nodes;
}
/**
* Get the edges from the nodes.
*
* @return array<string,mixed>[]
*/
public function get_edges() {
if ( ! isset( $this->edges ) ) {
$this->edges = $this->prepare_edges( $this->get_nodes() );
}
return $this->edges;
}
/**
* Returns pageInfo for the connection
*
* @return array<string,mixed>
*/
public function get_page_info() {
if ( ! isset( $this->page_info ) ) {
$page_info = $this->prepare_page_info();
/**
* Filter the pageInfo that is returned to the connection.
*
* This filter allows for additional fields to be filtered into the pageInfo
* of a connection, such as "totalCount", etc, because the filter has enough
* context of the query, args, request, etc to be able to calcuate and return
* that information.
*
* example:
*
* You would want to register a "total" field to the PageInfo type, then filter
* the pageInfo to return the total for the query, something to this tune:
*
* add_filter( 'graphql_connection_page_info', function( $page_info, $connection ) {
*
* $page_info['total'] = null;
*
* if ( $connection->query instanceof WP_Query ) {
* if ( isset( $connection->query->found_posts ) {
* $page_info['total'] = (int) $connection->query->found_posts;
* }
* }
*
* return $page_info;
*
* });
*/
$this->page_info = apply_filters( 'graphql_connection_page_info', $page_info, $this );
}
return $this->page_info;
}
/**
* ===============================
* Public setters
*
* These are used to directly modify the instance properties from outside the class.
* ===============================
*/
/**
* Given a key and value, this sets a query_arg which will modify the query_args used by ::get_query();
*
* @param string $key The key of the query arg to set
* @param mixed $value The value of the query arg to set
*
* @return static
*/
public function set_query_arg( $key, $value ) {
$this->query_args[ $key ] = $value;
return $this;
}
/**
* Overloads the query_class which will be used to instantiate the query.
*
* @param class-string<TQueryClass> $query_class The class to use for the query. If empty, this will reset to the default query class.
*
* @return static
*/
public function set_query_class( string $query_class ) {
$this->query_class = $query_class ?: $this->query_class();
return $this;
}
/**
* Whether the connection should resolve as a one-to-one connection.
*
* @return static
*/
public function one_to_one() {
$this->one_to_one = true;
return $this;
}
/**
* Gets whether or not the query should execute, BEFORE any data is fetched or altered, filtered by 'graphql_connection_pre_should_execute'.
*
* @param mixed $source The source that's passed down the GraphQL queries.
* @param array<string,mixed> $args The inputArgs on the field.
* @param \WPGraphQL\AppContext $context The AppContext passed down the GraphQL tree.
* @param \GraphQL\Type\Definition\ResolveInfo $info The ResolveInfo passed down the GraphQL tree.
*/
protected function get_pre_should_execute( $source, array $args, AppContext $context, ResolveInfo $info ): bool {
$should_execute = $this->pre_should_execute( $source, $args, $context, $info );
/**
* Filters whether or not the query should execute, BEFORE any data is fetched or altered.
*
* This is evaluated based solely on the values passed to the constructor, before any data is fetched or altered, and is useful for shortcircuiting the Connection Resolver before any heavy logic is executed.
*
* For more in-depth checks, use the `graphql_connection_should_execute` filter instead.
*
* @param bool $should_execute Whether or not the query should execute.
* @param mixed $source The source that's passed down the GraphQL queries.
* @param array $args The inputArgs on the field.
* @param \WPGraphQL\AppContext $context The AppContext passed down the GraphQL tree.
* @param \GraphQL\Type\Definition\ResolveInfo $info The ResolveInfo passed down the GraphQL tree.
*/
return apply_filters( 'graphql_connection_pre_should_execute', $should_execute, $source, $args, $context, $info );
}
/**
* Returns the loader.
*
* If $loader is not initialized, this method will initialize it.
*
* @return \WPGraphQL\Data\Loader\AbstractDataLoader
*/
protected function get_loader() {
// If the loader isn't set, set it.
if ( ! isset( $this->loader ) ) {
$name = $this->get_loader_name();
$this->loader = $this->context->get_loader( $name );
}
return $this->loader;
}
/**
* Returns the amount of items requested from the connection.
*
* @return int
*
* @throws \GraphQL\Error\UserError If the `first` or `last` args are used together.
*/
public function get_amount_requested() {
/**
* Filters the default query amount for a connection, if no `first` or `last` GraphQL argument is supplied.
*
* @param int $amount_requested The default query amount for a connection.
* @param self $resolver Instance of the Connection Resolver.
*/
$amount_requested = apply_filters( 'graphql_connection_default_query_amount', 10, $this );
// @todo This should use ::get_args() when b/c is not a concern.
$args = $this->args;
/**
* If both first & last are used in the input args, throw an exception.
*/
if ( ! empty( $args['first'] ) && ! empty( $args['last'] ) ) {
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' ) );
}
/**
* Get the key to use for the query amount.
* We avoid a ternary here for unit testing.
*/
$args_key = ! empty( $args['first'] ) && is_int( $args['first'] ) ? 'first' : null;
if ( null === $args_key ) {
$args_key = ! empty( $args['last'] ) && is_int( $args['last'] ) ? 'last' : null;
}
/**
* If the key is set, and is a positive integer, use it for the $amount_requested
* but if it's set to anything that isn't a positive integer, throw an exception
*/
if ( null !== $args_key && isset( $args[ $args_key ] ) ) {
if ( 0 > $args[ $args_key ] ) {
throw new UserError(
sprintf(
// translators: %s: The name of the arg that was invalid
esc_html__( '%s must be a positive integer.', 'wp-graphql' ),
esc_html( $args_key )
)
);
}
$amount_requested = $args[ $args_key ];
}
return (int) $amount_requested;
}
/**
* =====================
* Resolver lifecycle methods
*
* 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.
* =====================
*/
/**
* Get the connection to return to the Connection Resolver
*
* @return \GraphQL\Deferred
*/
public function get_connection() {
$this->execute_and_get_ids();
/**
* Return a Deferred function to load all buffered nodes before
* returning the connection.
*/
return new Deferred(
function () {
// @todo This should use ::get_ids() when b/c is not a concern.
$ids = $this->ids;
if ( ! empty( $ids ) ) {
// Load the ids.
$this->get_loader()->load_many( $ids );
}
/**
* Set the items. These are the "nodes" that make up the connection.
*
* Filters the nodes in the connection
*
* @todo We reinstantate this here for b/c. Once that is not a concern, we should relocate this filter to ::get_nodes().