Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add support for custom types and custom taxonomies #38

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
ARG PHP_VERSION=7.4
ARG WORDPRESS_VERSION=5.9.3
ARG PHP_VERSION=8.0
ARG WORDPRESS_VERSION=6.2

FROM wordpress:${WORDPRESS_VERSION}-php${PHP_VERSION}-apache

Expand Down
126 changes: 75 additions & 51 deletions src/filter-query.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ class FilterQuery {
*/
public function add_hooks(): void {
add_action( 'graphql_register_types', [ $this, 'extend_wp_graphql_fields' ] );

add_filter( 'graphql_RootQuery_fields', [ $this, 'apply_filters_input' ], 20 );
add_filter( 'graphql_connection_query_args', [ $this, 'apply_recursive_filter_resolver' ], 10, 2 );
}
Expand All @@ -56,7 +55,7 @@ public function apply_filters_input( array $fields ): array {
$args = is_array( $fields[ $post_type['plural_name'] ]['args'] ) ? $fields[ $post_type['plural_name'] ]['args'] : [];

$args['filter'] = [
'type' => 'TaxonomyFilter',
'type' => ucfirst( $post_type['name'] ) . 'TaxonomyFilter',
'description' => __( 'Filtering Queried Results By Taxonomy Objects', 'wp-graphql-filter-query' ),
];

Expand All @@ -81,13 +80,6 @@ public function apply_filters_input( array $fields ): array {
'notLike' => 'NOT IN',
);

/**
* $taxonomy_keys.
*
* @var array
*/
public $taxonomy_keys = [ 'tag', 'category' ];

/**
* $relation_keys.
*
Expand All @@ -98,15 +90,16 @@ public function apply_filters_input( array $fields ): array {
/**
* Check if operator is like or notLike
*
* @param array $filter_obj A Filter object, for wpQuery access, to build upon within each recursive call.
* @param int $depth A depth-counter to track recusrive call depth.
* @param string $post_type the current post type.
* @param array $filter_obj A Filter object, for wpQuery access, to build upon within each recursive call.
* @param int $depth A depth-counter to track recursive call depth.
*
* @throws FilterException Throws max nested filter depth exception, caught by wpgraphql response.
* @throws FilterException Throws and/or not allowed as siblings exception, caught by wpgraphql response.
* @throws FilterException Throws empty relation (and/or) exception, caught by wpgraphql response.
* @throws FilterException Throws max nested filter depth exception, caught by WPGraphQL response.
* @throws FilterException Throws and/or not allowed as siblings exception, caught by WPGraphQL response.
* @throws FilterException Throws empty relation (and/or) exception, caught by WPGraphQL response.
* @return array
*/
private function resolve_taxonomy( array $filter_obj, int $depth ): array {
private function resolve_taxonomy( string $post_type, array $filter_obj, int $depth ): array {
if ( $depth > $this->max_nesting_depth ) {
throw new FilterException( 'The Filter\'s relation allowable depth nesting has been exceeded. Please reduce to allowable (' . $this->max_nesting_depth . ') depth to proceed' );
} elseif ( array_key_exists( 'and', $filter_obj ) && array_key_exists( 'or', $filter_obj ) ) {
Expand All @@ -115,24 +108,37 @@ private function resolve_taxonomy( array $filter_obj, int $depth ): array {

$temp_query = [];
foreach ( $filter_obj as $root_obj_key => $value ) {
if ( in_array( $root_obj_key, $this->taxonomy_keys, true ) ) {
if ( in_array( $root_obj_key, $this->relation_keys, true ) ) {
$nested_obj_array = $value;
$wp_query_array = [];

if ( count( $nested_obj_array ) === 0 ) {
throw new FilterException( 'The Filter relation array specified has no children. Please remove the relation key or add one or more appropriate objects to proceed.' );
}
foreach ( $nested_obj_array as $nested_obj_index => $nested_obj_value ) {
$wp_query_array[ $nested_obj_index ] = $this->resolve_taxonomy( $post_type, $nested_obj_value, ++$depth );
$wp_query_array[ $nested_obj_index ]['relation'] = 'AND';
}
$wp_query_array['relation'] = strtoupper( $root_obj_key );
$temp_query[] = $wp_query_array;
} else {
$attribute_array = $value;
foreach ( $attribute_array as $field_key => $field_kvp ) {
foreach ( $field_kvp as $operator => $terms ) {
$mapped_operator = $this->operator_mappings[ $operator ] ?? 'IN';
$is_like_operator = $this->is_like_operator( $operator );
$taxonomy = $root_obj_key === 'tag' ? 'post_tag' : 'category';
$taxonomy_slug = $this->get_taxonomy_slug( $post_type, $root_obj_key );

$terms = ! $is_like_operator ? $terms : get_terms(
[
'taxonomy' => $taxonomy,
'taxonomy' => $taxonomy_slug,
'fields' => 'ids',
'name__like' => esc_attr( $terms ),
]
);

$result = [
'taxonomy' => $taxonomy,
'taxonomy' => $taxonomy_slug,
'field' => ( $field_key === 'id' ) || $is_like_operator ? 'term_id' : 'name',
'terms' => $terms,
'operator' => $mapped_operator,
Expand All @@ -141,24 +147,31 @@ private function resolve_taxonomy( array $filter_obj, int $depth ): array {
$temp_query[] = $result;
}
}
} elseif ( in_array( $root_obj_key, $this->relation_keys, true ) ) {
$nested_obj_array = $value;
$wp_query_array = [];

if ( count( $nested_obj_array ) === 0 ) {
throw new FilterException( 'The Filter relation array specified has no children. Please remove the relation key or add one or more appropriate objects to proceed.' );
}
foreach ( $nested_obj_array as $nested_obj_index => $nested_obj_value ) {
$wp_query_array[ $nested_obj_index ] = $this->resolve_taxonomy( $nested_obj_value, ++$depth );
$wp_query_array[ $nested_obj_index ]['relation'] = 'AND';
}
$wp_query_array['relation'] = strtoupper( $root_obj_key );
$temp_query[] = $wp_query_array;
}
}
return $temp_query;
}

/**
* Retrieves the slug of the taxonomy associated with the specified post type and GraphQL singular name.
*
* @param string $post_type The post type for which to retrieve the taxonomy slug.
* @param string $graphql_singular_name The GraphQL singular name of the taxonomy.
* @return string|false The slug of the associated taxonomy, or false if no taxonomy is found.
*/
private function get_taxonomy_slug( string $post_type, string $graphql_singular_name ) {
$taxonomies = get_object_taxonomies(
$post_type,
'objects'
);

foreach ( $taxonomies as $taxonomy ) {
if ( strtolower( $taxonomy->graphql_single_name ) === strtolower( $graphql_singular_name ) ) {
return $taxonomy->name;
}
}
}

/**
* Apply facet filters using graphql_connection_query_args filter hook.
*
Expand All @@ -177,7 +190,8 @@ public function apply_recursive_filter_resolver( array $query_args, AbstractConn

$filter_args_root = $args['filter'];

$query_args['tax_query'][] = $this->resolve_taxonomy( $filter_args_root, 0, [] );
// TODO: handle multiple post types here.
$query_args['tax_query'][] = $this->resolve_taxonomy( $query_args['post_type'][0], $filter_args_root, 0, [] );

self::$query_args = $query_args;
return $query_args;
Expand Down Expand Up @@ -272,30 +286,40 @@ public function extend_wp_graphql_fields() {
]
);

register_graphql_input_type(
'TaxonomyFilter',
[
foreach ( filter_query_get_supported_post_types() as $post_type ) {
$filter_obj_name = ucfirst( $post_type['name'] ) . 'TaxonomyFilter';

$config = [
'description' => __( 'Taxonomies Where Filtering Supported', 'wp-graphql-filter-query' ),
'fields' => [
'tag' => [
'type' => 'TaxonomyFilterFields',
'description' => __( 'Tags Object Fields Allowable For Filtering', 'wp-graphql-filter-query' ),
],
'category' => [
'type' => 'TaxonomyFilterFields',
'description' => __( 'Category Object Fields Allowable For Filtering', 'wp-graphql-filter-query' ),
],
'and' => [
'type' => [ 'list_of' => 'TaxonomyFilter' ],
'and' => [
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you h8 linting? 😛

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it was the linter that complained :D

'type' => [ 'list_of' => $filter_obj_name ],
'description' => __( '\'AND\' Array of Taxonomy Objects Allowable For Filtering', 'wp-graphql-filter-query' ),
],
'or' => [
'type' => [ 'list_of' => 'TaxonomyFilter' ],
'description' => __( '\'OR\' Array of Taxonomy Objects Allowable For Filterin', 'wp-graphql-filter-query' ),
'or' => [
'type' => [ 'list_of' => $filter_obj_name ],
'description' => __( '\'OR\' Array of Taxonomy Objects Allowable For Filtering', 'wp-graphql-filter-query' ),
],
],
]
);
];

$taxonomies = get_object_taxonomies(
$post_type['name'],
'objects'
);

foreach ( $taxonomies as $taxonomy ) {
$config['fields'][ $taxonomy->graphql_single_name ] = [
'type' => 'TaxonomyFilterFields',
'description' => __( 'Object Fields Allowable For Filtering', 'wp-graphql-filter-query' ),
];
}

register_graphql_input_type(
$filter_obj_name,
$config
);
}
}

/**
Expand Down
102 changes: 34 additions & 68 deletions tests/src/aggregate-query.test.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,40 @@ protected function setUp(): void {
'graphql_plural_name' => 'zombies',
)
);

// Set up the labels for the custom taxonomy.
$labels = array(
'name' => __( 'Sports', 'textdomain' ),
'singular_name' => __( 'Sport', 'textdomain' ),
'search_items' => __( 'Search Sports', 'textdomain' ),
'all_items' => __( 'All Sports', 'textdomain' ),
'parent_item' => __( 'Parent Sport', 'textdomain' ),
'parent_item_colon' => __( 'Parent Sport:', 'textdomain' ),
'edit_item' => __( 'Edit Sport', 'textdomain' ),
'update_item' => __( 'Update Sport', 'textdomain' ),
'add_new_item' => __( 'Add New Sport', 'textdomain' ),
'new_item_name' => __( 'New Sport Name', 'textdomain' ),
'menu_name' => __( 'Sports', 'textdomain' ),
);

// Set up the arguments for the custom taxonomy.
$args = array(
'labels' => $labels,
'hierarchical' => true,
'public' => true,
'show_ui' => true,
'show_in_menu' => true,
'show_admin_column' => true,
'query_var' => true,
'rewrite' => array( 'slug' => 'sport' ),
'show_in_graphql' => true,
'graphql_single_name' => 'sport',
'graphql_plural_name' => 'sports',

);

// Register the custom taxonomy.
register_taxonomy( 'sport', 'zombie', $args );
}

public function data_for_schema_exists_for_aggregations(): array {
Expand Down Expand Up @@ -1193,74 +1227,6 @@ public function filter_aggregations_data_provider(): array {
}',
'{"data": { "posts": {"aggregations" : { "tags" : [] }}}}',
],
'pages_accept_valid_tax_filter_args' => [
'query {
pages(
filter: {
category: {
id: {
eq: 10
},
name: {
eq: "foo"
}
},
tag: {
name: {
in: ["foo", "bar"],
like: "tst"
}
}
}
) {
aggregations {
tags {
key
count
},
categories {
key
count
}
}
}
}',
'{"data": { "pages": {"aggregations" : { "tags" : [], "categories" : [] }}}}',
],
'zombies_accept_valid_tax_filter_args' => [
'query {
zombies(
filter: {
category: {
id: {
eq: 10
},
name: {
eq: "foo"
}
},
tag: {
name: {
in: ["foo", "bar"],
like: "tst"
}
}
}
) {
aggregations {
tags {
key
count
},
categories {
key
count
}
}
}
}',
'{"data": { "zombies": {"aggregations" : { "tags" : [], "categories" : [] }}}}',
],
];
}
}
Loading