Skip to content

Commit aad0106

Browse files
committed
feat: add suport for resolver-level $query_class
See #2821
1 parent 95d3bf5 commit aad0106

10 files changed

+512
-80
lines changed

src/Data/Connection/AbstractConnectionResolver.php

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -141,18 +141,8 @@ protected function prepare_query_args( array $args ): array {
141141
}
142142

143143
/**
144-
* Method `get_query()` is no longer abstract.
145-
*
146-
* Overloading should be done via `query()`.
147-
*
148-
* {@inheritDoc}
144+
* Method `get_query()` is no longer abstract. Overloading (if necesary) should be done via `query()` and `query_class()`.
149145
*/
150-
protected function query( array $query_args ) {
151-
throw new Exception( sprintf(
152-
__( 'Class %s does not implement a valid method `query()`.', 'wp-graphql' ),
153-
get_class( $this )
154-
) );
155-
}
156146

157147
/**
158148
* Method `should_execute()` is now protected and no longer abstract. It defaults to `true`.

src/Data/Connection/CommentConnectionResolver.php

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -147,15 +147,12 @@ protected function prepare_query_args( array $args ) : array {
147147
*/
148148
return apply_filters( 'graphql_comment_connection_query_args', $query_args, $this );
149149
}
150-
150+
151151
/**
152152
* {@inheritDoc}
153-
*
154-
* @return \WP_Comment_Query
155-
* @throws \Exception
156153
*/
157-
protected function query( array $query_args ) {
158-
return new WP_Comment_Query( $query_args );
154+
protected function query_class() : string {
155+
return 'WP_Comment_Query';
159156
}
160157

161158
/**

src/Data/Connection/ConnectionResolver.php

Lines changed: 164 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace WPGraphQL\Data\Connection;
44

55
use GraphQL\Deferred;
6+
use GraphQL\Error\InvariantViolation;
67
use GraphQL\Error\UserError;
78
use GraphQL\Type\Definition\ResolveInfo;
89
use WPGraphQL\AppContext;
@@ -107,7 +108,16 @@ abstract class ConnectionResolver {
107108
protected $query_args;
108109

109110
/**
110-
* The Query class/array/object used to fetch the data.
111+
* 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.
112+
*
113+
* Examples `WP_Query`, `WP_Comment_Query`, `WC_Query`, `/My/Namespaced/CustomQuery`, etc.
114+
*
115+
* @var ?string
116+
*/
117+
protected $query_class;
118+
119+
/**
120+
* The instantiated query array/object used to fetch the data.
111121
*
112122
* Examples:
113123
* return new WP_Query( $this->query_args );
@@ -196,6 +206,9 @@ public function __construct( $source, array $args, AppContext $context, ResolveI
196206
// Get the query args for the connection.
197207
$this->query_args = $this->get_query_args();
198208

209+
// Get the query class for the connection.
210+
$this->query_class = $this->get_query_class();
211+
199212
// The rest of the class properties are set when `$this->get_connection()` is called.
200213
}
201214

@@ -220,17 +233,6 @@ abstract protected function loader_name() : string;
220233
*/
221234
abstract protected function prepare_query_args( array $args ) : array;
222235

223-
/**
224-
* Executes the query and returns the results.
225-
*
226-
* Usually, the returned value is an instantiated WP_Query class, 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.
227-
*
228-
* @param array $query_args The query args to use to query the data.
229-
*
230-
* @return mixed
231-
*/
232-
abstract protected function query( array $query_args );
233-
234236
/**
235237
* Return an array of ids from the query
236238
*
@@ -292,6 +294,52 @@ protected function max_query_amount() : int {
292294
return 100;
293295
}
294296

297+
/**
298+
* The default query class to use for the connection. Should `null` if the resolver does not use a query class to fetch the data.
299+
*/
300+
protected function query_class() : ?string {
301+
return null;
302+
}
303+
304+
/**
305+
* Validates the query class. Will be ignored if the Connection Resolver does not use a query class.
306+
*
307+
* 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.
308+
*
309+
* @param string $query_class The query class to validate.
310+
*/
311+
protected function is_valid_query_class( string $query_class ) : bool {
312+
return method_exists( $query_class, 'query' );
313+
}
314+
315+
/**
316+
* Executes the query and returns the results.
317+
*
318+
* 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.
319+
*
320+
* If the resolver does not rely on a query class, this should be overloaded to return the data directly.
321+
*
322+
* @param array $query_args The query args to use to query the data.
323+
*
324+
* @return mixed
325+
*/
326+
protected function query( array $query_args ) {
327+
// If there is no query class, we need the child class to overload this method.
328+
$query_class = $this->get_query_class();
329+
330+
if ( empty( $query_class ) ) {
331+
throw new InvariantViolation(
332+
// translators: %s is the name of the connection resolver class.
333+
sprintf(
334+
__( 'The %s class does not rely on a query class. Please define a `query()` method to return the data directly.', 'wp-graphql' ),
335+
static::class
336+
)
337+
);
338+
}
339+
340+
return new $query_class( $query_args );
341+
}
342+
295343
/**
296344
* Determine whether or not the query should execute.
297345
*
@@ -495,6 +543,32 @@ public function get_query_args() : array {
495543
return $this->query_args;
496544
}
497545

546+
/**
547+
* Gets the query class to be instantiated by the `query()` method.
548+
*/
549+
public function get_query_class() : ?string {
550+
if ( ! isset( $this->query_class ) ) {
551+
$default_query_class = $this->query_class();
552+
553+
// Attempt to get the query class from the context.
554+
$context = $this->get_context();
555+
556+
$query_class = ! empty( $context->queryClass ) ? $context->queryClass : $default_query_class;
557+
558+
/**
559+
* Filters the `$query_class` that will be used to execute the query.
560+
*
561+
* 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`).
562+
*
563+
* @param ?string $query_class The query class to be used with the executable query to get data. `null` if the ConnectionResolver does not use a query class.
564+
* @param self $resolver Instance of the ConnectionResolver
565+
*/
566+
$this->query_class = apply_filters( 'graphql_connection_query_class', $query_class, $this );
567+
}
568+
569+
return $this->query_class;
570+
}
571+
498572
/**
499573
* Returns whether the connection should execute.
500574
*
@@ -523,7 +597,6 @@ public function get_should_execute() : bool {
523597
*/
524598
public function get_query() {
525599
if ( ! isset( $this->query ) ) {
526-
527600
/**
528601
* When this filter returns anything but false, it will be used as the resolved query, and the default query execution will be skipped.
529602
*
@@ -533,6 +606,10 @@ public function get_query() {
533606
$query = apply_filters( 'graphql_connection_pre_get_query', false, $this );
534607

535608
if ( false === $query ) {
609+
610+
// Validates the query class before it is used in the query() method.
611+
$this->validate_query_class();
612+
536613
$query = $this->query( $this->get_query_args() );
537614
}
538615

@@ -685,6 +762,19 @@ public function set_query_arg( $key, $value ) {
685762
return $this;
686763
}
687764

765+
/**
766+
* Overloads the query_class which will be used to instantiate the query.
767+
*
768+
* @param string $query_class The class to use for the query. If empty, this will reset to the default query class.
769+
*
770+
* @return self
771+
*/
772+
public function set_query_class( string $query_class ) {
773+
$this->query_class = $query_class ?: $this->query_class();
774+
775+
return $this;
776+
}
777+
688778
/**
689779
* Whether the connection should resolve as a one-to-one connection.
690780
*
@@ -885,6 +975,67 @@ protected function execute_and_get_ids() : array {
885975
return $this->ids;
886976
}
887977

978+
/**
979+
* Validates the $query_class set on the resolver.
980+
*
981+
* This runs before the query is executed to ensure that the query class is valid.
982+
*/
983+
protected function validate_query_class() : void {
984+
$default_query_class = $this->query_class();
985+
$query_class = $this->get_query_class();
986+
987+
// If the default query class is null, then the resolver should not use a query class.
988+
if ( null === $default_query_class ) {
989+
// If the query class is null, then we're good.
990+
if ( null === $query_class ) {
991+
return;
992+
}
993+
994+
throw new InvariantViolation(
995+
// 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.
996+
sprintf(
997+
__( 'Class %1$s should not use a query class, but is attempting to use the %2$s query class.', 'wp-graphql' ),
998+
static::class,
999+
$query_class
1000+
)
1001+
);
1002+
}
1003+
1004+
// If there's no query class set, throw an error.
1005+
if ( null === $query_class ) {
1006+
throw new InvariantViolation(
1007+
// translators: %s: The connection resolver class name.
1008+
sprintf(
1009+
__( '%s requires a query class, but no query class is set.', 'wp-graphql' ),
1010+
static::class
1011+
)
1012+
);
1013+
}
1014+
1015+
// If the class is invalid, throw an error.
1016+
if ( ! class_exists( $query_class ) ) {
1017+
throw new InvariantViolation(
1018+
// translators: %s: The name of the query class that is set by the resolver.
1019+
sprintf(
1020+
__( 'The query class %s does not exist.', 'wp-graphql' ),
1021+
$query_class
1022+
)
1023+
);
1024+
}
1025+
1026+
// If the class is not compatible with our ConnectionResolver::query() method, throw an error.
1027+
if ( ! $this->is_valid_query_class( $query_class ) ) {
1028+
throw new InvariantViolation(
1029+
// translators: %1$s: The name of the query class that is set by the resolver. %2$s: The name of the resolver class.
1030+
sprintf(
1031+
__( 'The query class %1$s is not compatible with %2$s.', 'wp-graphql' ),
1032+
$this->query_class,
1033+
static::class
1034+
)
1035+
);
1036+
}
1037+
}
1038+
8881039
/**
8891040
* Returns an array slice of IDs, per the Relay Cursor Connection spec.
8901041
*

src/Data/Connection/PostObjectConnectionResolver.php

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,13 @@ protected function prepare_query_args( array $args ) : array {
348348
return apply_filters( 'graphql_post_object_connection_query_args', $query_args, $this );
349349
}
350350

351+
/**
352+
* {@inheritDoc}
353+
*/
354+
protected function query_class(): ?string {
355+
return 'WP_Query';
356+
}
357+
351358
/**
352359
* {@inheritDoc}
353360
*
@@ -356,14 +363,7 @@ protected function prepare_query_args( array $args ) : array {
356363
* @throws \Exception
357364
*/
358365
protected function query( array $query_args ) {
359-
$context = $this->get_context();
360-
361-
// Get query class.
362-
$queryClass = ! empty( $context->queryClass )
363-
? $context->queryClass
364-
: '\WP_Query';
365-
366-
$query = new $queryClass( $this->query_args );
366+
$query = parent::query( $query_args );
367367

368368
if ( isset( $query->query_vars['suppress_filters'] ) && true === $query->query_vars['suppress_filters'] ) {
369369
throw new InvariantViolation( __( 'WP_Query has been modified by a plugin or theme to suppress_filters, which will cause issues with WPGraphQL Execution. If you need to suppress filters for a specific reason within GraphQL, consider registering a custom field to the WPGraphQL Schema with a custom resolver.', 'wp-graphql' ) );

src/Data/Connection/TermObjectConnectionResolver.php

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -162,12 +162,9 @@ public function prepare_query_args( array $args ) : array {
162162

163163
/**
164164
* {@inheritDoc}
165-
*
166-
* @return \WP_Term_Query
167-
* @throws \Exception
168165
*/
169-
protected function query( array $query_args ) {
170-
return new \WP_Term_Query( $query_args );
166+
protected function query_class(): ?string {
167+
return 'WP_Term_Query';
171168
}
172169

173170
/**

src/Data/Connection/UserConnectionResolver.php

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -165,18 +165,9 @@ public function prepare_query_args( array $args ) : array {
165165

166166
/**
167167
* {@inheritDoc}
168-
*
169-
* @return object|\WP_User_Query
170-
*
171-
* @throws \Exception
172168
*/
173-
protected function query( array $query_args ) {
174-
// Get query class.
175-
$queryClass = ! empty( $this->context->queryClass )
176-
? $this->context->queryClass
177-
: '\WP_User_Query';
178-
179-
return new $queryClass( $query_args );
169+
protected function query_class(): ?string {
170+
return 'WP_User_Query';
180171
}
181172

182173
/**

0 commit comments

Comments
 (0)