Skip to content

Commit

Permalink
feat: add suport for resolver-level $query_class
Browse files Browse the repository at this point in the history
See #2821
  • Loading branch information
justlevine committed May 25, 2023
1 parent 95d3bf5 commit aad0106
Show file tree
Hide file tree
Showing 10 changed files with 512 additions and 80 deletions.
12 changes: 1 addition & 11 deletions src/Data/Connection/AbstractConnectionResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -141,18 +141,8 @@ protected function prepare_query_args( array $args ): array {
}

/**
* Method `get_query()` is no longer abstract.
*
* Overloading should be done via `query()`.
*
* {@inheritDoc}
* Method `get_query()` is no longer abstract. Overloading (if necesary) should be done via `query()` and `query_class()`.
*/
protected function query( array $query_args ) {
throw new Exception( sprintf(
__( 'Class %s does not implement a valid method `query()`.', 'wp-graphql' ),
get_class( $this )
) );
}

/**
* Method `should_execute()` is now protected and no longer abstract. It defaults to `true`.
Expand Down
9 changes: 3 additions & 6 deletions src/Data/Connection/CommentConnectionResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -147,15 +147,12 @@ protected function prepare_query_args( array $args ) : array {
*/
return apply_filters( 'graphql_comment_connection_query_args', $query_args, $this );
}

/**
* {@inheritDoc}
*
* @return \WP_Comment_Query
* @throws \Exception
*/
protected function query( array $query_args ) {
return new WP_Comment_Query( $query_args );
protected function query_class() : string {
return 'WP_Comment_Query';
}

/**
Expand Down
177 changes: 164 additions & 13 deletions src/Data/Connection/ConnectionResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace WPGraphQL\Data\Connection;

use GraphQL\Deferred;
use GraphQL\Error\InvariantViolation;
use GraphQL\Error\UserError;
use GraphQL\Type\Definition\ResolveInfo;
use WPGraphQL\AppContext;
Expand Down Expand Up @@ -107,7 +108,16 @@ abstract class ConnectionResolver {
protected $query_args;

/**
* The Query class/array/object used to fetch the data.
* 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 ?string
*/
protected $query_class;

/**
* The instantiated query array/object used to fetch the data.
*
* Examples:
* return new WP_Query( $this->query_args );
Expand Down Expand Up @@ -196,6 +206,9 @@ public function __construct( $source, array $args, AppContext $context, ResolveI
// Get the query args for the connection.
$this->query_args = $this->get_query_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.
}

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

/**
* Executes the query and returns the results.
*
* 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.
*
* @param array $query_args The query args to use to query the data.
*
* @return mixed
*/
abstract protected function query( array $query_args );

/**
* Return an array of ids from the query
*
Expand Down Expand Up @@ -292,6 +294,52 @@ protected function max_query_amount() : int {
return 100;
}

/**
* The default query class to use for the connection. Should `null` if the resolver does not use a query class to fetch the data.
*/
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 $query_args The query args to use to query the data.
*
* @return mixed
*/
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(
// translators: %s is the name of the connection resolver class.
sprintf(
__( '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.
*
Expand Down Expand Up @@ -495,6 +543,32 @@ public function get_query_args() : array {
return $this->query_args;
}

/**
* Gets the query class to be instantiated by the `query()` method.
*/
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 ?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.
* @param self $resolver Instance of the ConnectionResolver
*/
$this->query_class = apply_filters( 'graphql_connection_query_class', $query_class, $this );
}

return $this->query_class;
}

/**
* Returns whether the connection should execute.
*
Expand Down Expand Up @@ -523,7 +597,6 @@ public function get_should_execute() : bool {
*/
public function get_query() {
if ( ! isset( $this->query ) ) {

/**
* When this filter returns anything but false, it will be used as the resolved query, and the default query execution will be skipped.
*
Expand All @@ -533,6 +606,10 @@ public function get_query() {
$query = apply_filters( 'graphql_connection_pre_get_query', false, $this );

if ( false === $query ) {

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

$query = $this->query( $this->get_query_args() );
}

Expand Down Expand Up @@ -685,6 +762,19 @@ public function set_query_arg( $key, $value ) {
return $this;
}

/**
* Overloads the query_class which will be used to instantiate the query.
*
* @param string $query_class The class to use for the query. If empty, this will reset to the default query class.
*
* @return self
*/
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.
*
Expand Down Expand Up @@ -885,6 +975,67 @@ protected function execute_and_get_ids() : array {
return $this->ids;
}

/**
* Validates the $query_class set on the resolver.
*
* This runs before the query is executed to ensure that the query class is valid.
*/
protected function validate_query_class() : void {
$default_query_class = $this->query_class();
$query_class = $this->get_query_class();

// If the default query class is null, then the resolver should not use a query class.
if ( null === $default_query_class ) {
// If the query class is null, then we're good.
if ( null === $query_class ) {
return;
}

throw new InvariantViolation(
// 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.
sprintf(
__( 'Class %1$s should not use a query class, but is attempting to use the %2$s query class.', 'wp-graphql' ),
static::class,
$query_class
)
);
}

// If there's no query class set, throw an error.
if ( null === $query_class ) {
throw new InvariantViolation(
// translators: %s: The connection resolver class name.
sprintf(
__( '%s requires a query class, but no query class is set.', 'wp-graphql' ),
static::class
)
);
}

// If the class is invalid, throw an error.
if ( ! class_exists( $query_class ) ) {
throw new InvariantViolation(
// translators: %s: The name of the query class that is set by the resolver.
sprintf(
__( 'The query class %s does not exist.', 'wp-graphql' ),
$query_class
)
);
}

// If the class is not compatible with our ConnectionResolver::query() method, throw an error.
if ( ! $this->is_valid_query_class( $query_class ) ) {
throw new InvariantViolation(
// translators: %1$s: The name of the query class that is set by the resolver. %2$s: The name of the resolver class.
sprintf(
__( 'The query class %1$s is not compatible with %2$s.', 'wp-graphql' ),
$this->query_class,
static::class
)
);
}
}

/**
* Returns an array slice of IDs, per the Relay Cursor Connection spec.
*
Expand Down
16 changes: 8 additions & 8 deletions src/Data/Connection/PostObjectConnectionResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,13 @@ protected function prepare_query_args( array $args ) : array {
return apply_filters( 'graphql_post_object_connection_query_args', $query_args, $this );
}

/**
* {@inheritDoc}
*/
protected function query_class(): ?string {
return 'WP_Query';
}

/**
* {@inheritDoc}
*
Expand All @@ -356,14 +363,7 @@ protected function prepare_query_args( array $args ) : array {
* @throws \Exception
*/
protected function query( array $query_args ) {
$context = $this->get_context();

// Get query class.
$queryClass = ! empty( $context->queryClass )
? $context->queryClass
: '\WP_Query';

$query = new $queryClass( $this->query_args );
$query = parent::query( $query_args );

if ( isset( $query->query_vars['suppress_filters'] ) && true === $query->query_vars['suppress_filters'] ) {
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' ) );
Expand Down
7 changes: 2 additions & 5 deletions src/Data/Connection/TermObjectConnectionResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -162,12 +162,9 @@ public function prepare_query_args( array $args ) : array {

/**
* {@inheritDoc}
*
* @return \WP_Term_Query
* @throws \Exception
*/
protected function query( array $query_args ) {
return new \WP_Term_Query( $query_args );
protected function query_class(): ?string {
return 'WP_Term_Query';
}

/**
Expand Down
13 changes: 2 additions & 11 deletions src/Data/Connection/UserConnectionResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -165,18 +165,9 @@ public function prepare_query_args( array $args ) : array {

/**
* {@inheritDoc}
*
* @return object|\WP_User_Query
*
* @throws \Exception
*/
protected function query( array $query_args ) {
// Get query class.
$queryClass = ! empty( $this->context->queryClass )
? $this->context->queryClass
: '\WP_User_Query';

return new $queryClass( $query_args );
protected function query_class(): ?string {
return 'WP_User_Query';
}

/**
Expand Down

0 comments on commit aad0106

Please sign in to comment.