diff --git a/CHANGELOG.md b/CHANGELOG.md index 898f544..cf731b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Added + +- Add support for custom taxonomies filtering ([#42](https://github.com/studiometa/wp-toolkit/issues/42), [#43](https://github.com/studiometa/wp-toolkit/pull/43), [ae541b5](https://github.com/studiometa/wp-toolkit/commit/ae541b5)) + ## v2.2.2 - 2024.04.19 ### Fixed @@ -49,9 +53,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Add an `EmailManager` to configure `PHPMailer` via environment variables ([#22](https://github.com/studiometa/wp-toolkit/pull/22)) - Add `enqueue_script($handle, $path)` and `enqueue_style($handle, $path)` method to the `AssetsManager` class ([#23](https://github.com/studiometa/wp-toolkit/pull/23)) - Add a `Plugin::disable` method to the `Plugin` helper class ([#26](https://github.com/studiometa/wp-toolkit/pull/26)) -- Add a `request` helper function ([#26](https://github.com/studiometa/wp-toolkit/pull/26)) +- Add a `request` helper function ([#26](https://github.com/studiometa/wp-toolkit/pull/26)) - Add a `Request` helper class ([#26](https://github.com/studiometa/wp-toolkit/pull/26)) -- Add a `env` helper function ([#26](https://github.com/studiometa/wp-toolkit/pull/26)) +- Add a `env` helper function ([#26](https://github.com/studiometa/wp-toolkit/pull/26)) - Add a `Env` helper class ([#26](https://github.com/studiometa/wp-toolkit/pull/26)) ### Changed diff --git a/src/Managers/FacetsManager.php b/src/Managers/FacetsManager.php index db4e90c..5dc74d7 100644 --- a/src/Managers/FacetsManager.php +++ b/src/Managers/FacetsManager.php @@ -8,7 +8,6 @@ namespace Studiometa\WPToolkit\Managers; use WP_Query; -use Timber\Timber; use Twig\Environment; use Twig\TwigFunction; use Studiometa\WPToolkit\Managers\ManagerInterface; @@ -63,9 +62,106 @@ public function add_facets_to_query(WP_Query &$query): void return; } + $tax_queries = []; + $regular_vars = []; + foreach ($this->facets as $query_var => $value) { - $query->query_vars[ $query_var ] = $value; + $taxonomy_data = $this->parse_taxonomy_query_var($query_var); + + if ($taxonomy_data) { + $tax_queries[] = $this->build_tax_query($taxonomy_data, $value); + } else { + $regular_vars[$query_var] = $value; + } + } + + // Apply regular query vars + foreach ($regular_vars as $query_var => $value) { + $query->query_vars[$query_var] = $value; + } + + // Apply taxonomy queries + if (!empty($tax_queries)) { + $existing_tax_query = $query->get('tax_query', []); + if (!empty($existing_tax_query)) { + $tax_queries[] = $existing_tax_query; + } + $query->set('tax_query', $tax_queries); + } + } + + /** + * Parse a query variable to determine if it's a taxonomy query. + * + * @param string $query_var The query variable name. + * @return array|null Array with taxonomy and operator data, or null if not a taxonomy query. + */ + private function parse_taxonomy_query_var(string $query_var): ?array + { + $taxonomies = get_taxonomies(['public' => true]); + + // Direct taxonomy match + if (array_key_exists($query_var, $taxonomies)) { + return ['taxonomy' => $query_var, 'operator' => 'IN']; + } + + // Check for suffixed taxonomy queries + $suffixes = ['__in', '__not_in', '__and', '__exists']; + foreach ($suffixes as $suffix) { + if (str_ends_with($query_var, $suffix)) { + $taxonomy = substr($query_var, 0, -strlen($suffix)); + if (array_key_exists($taxonomy, $taxonomies)) { + $operator = match ($suffix) { + '__in' => 'IN', + '__not_in' => 'NOT IN', + '__and' => 'AND', + '__exists' => 'EXISTS', + }; + return ['taxonomy' => $taxonomy, 'operator' => $operator]; + } + } } + + return null; + } + + /** + * Build a tax_query array for a given taxonomy and value. + * + * @param array $taxonomy_data Array with taxonomy and operator info. + * @param string|array $value The query value. + * @return array The tax_query array. + */ + private function build_tax_query(array $taxonomy_data, string|array $value): array + { + $tax_query = [ + 'taxonomy' => $taxonomy_data['taxonomy'], + 'operator' => $taxonomy_data['operator'], + ]; + + if ($taxonomy_data['operator'] === 'EXISTS') { + // EXISTS queries don't need terms/field + return $tax_query; + } + + // Handle different value types + if (is_array($value)) { + $terms = $value; + } else { + $terms = array_map('trim', explode(',', $value)); + } + + // Determine if we're dealing with term IDs or slugs + $first_term = reset($terms); + if (is_numeric($first_term)) { + $tax_query['field'] = 'term_id'; + $tax_query['terms'] = array_map('intval', $terms); + } else { + $tax_query['field'] = 'slug'; + $tax_query['terms'] = $terms; + } + + return $tax_query; } /** diff --git a/tests/Managers/FacetsManagerTest.php b/tests/Managers/FacetsManagerTest.php index 195811b..25e6236 100644 --- a/tests/Managers/FacetsManagerTest.php +++ b/tests/Managers/FacetsManagerTest.php @@ -51,4 +51,163 @@ public function test_it_should_add_a_twig_helper_function() $this->assertTrue($function instanceof TwigFunction); $this->assertTrue($function->getCallable()('cat') === 2); } + + public function test_it_should_handle_taxonomy_queries_with_slugs() + { + // Register a test taxonomy + register_taxonomy('test_taxonomy', 'post', ['public' => true]); + + request()->query->set('facets', ['test_taxonomy' => 'slug1,slug2']); + $manager = new FacetsManager(); + $manager->run(); + + $this->go_to('/?facets[test_taxonomy]=slug1,slug2'); + global $wp_query; + + $tax_query = $wp_query->get('tax_query'); + $this->assertIsArray($tax_query); + $this->assertCount(1, $tax_query); + $this->assertEquals('test_taxonomy', $tax_query[0]['taxonomy']); + $this->assertEquals('IN', $tax_query[0]['operator']); + $this->assertEquals('slug', $tax_query[0]['field']); + $this->assertEquals(['slug1', 'slug2'], $tax_query[0]['terms']); + } + + public function test_it_should_handle_taxonomy_queries_with_ids() + { + register_taxonomy('test_taxonomy', 'post', ['public' => true]); + + request()->query->set('facets', ['test_taxonomy' => '1,2,3']); + $manager = new FacetsManager(); + $manager->run(); + + $this->go_to('/?facets[test_taxonomy]=1,2,3'); + global $wp_query; + + $tax_query = $wp_query->get('tax_query'); + $this->assertEquals('term_id', $tax_query[0]['field']); + $this->assertEquals([1, 2, 3], $tax_query[0]['terms']); + } + + public function test_it_should_handle_taxonomy_not_in_queries() + { + register_taxonomy('test_taxonomy', 'post', ['public' => true]); + + request()->query->set('facets', ['test_taxonomy__not_in' => 'excluded-slug']); + $manager = new FacetsManager(); + $manager->run(); + + $this->go_to('/?facets[test_taxonomy__not_in]=excluded-slug'); + global $wp_query; + + $tax_query = $wp_query->get('tax_query'); + $this->assertEquals('test_taxonomy', $tax_query[0]['taxonomy']); + $this->assertEquals('NOT IN', $tax_query[0]['operator']); + $this->assertEquals(['excluded-slug'], $tax_query[0]['terms']); + } + + public function test_it_should_handle_taxonomy_and_queries() + { + register_taxonomy('test_taxonomy', 'post', ['public' => true]); + + request()->query->set('facets', ['test_taxonomy__and' => 'slug1,slug2']); + $manager = new FacetsManager(); + $manager->run(); + + $this->go_to('/?facets[test_taxonomy__and]=slug1,slug2'); + global $wp_query; + + $tax_query = $wp_query->get('tax_query'); + $this->assertEquals('AND', $tax_query[0]['operator']); + } + + public function test_it_should_handle_taxonomy_exists_queries() + { + register_taxonomy('test_taxonomy', 'post', ['public' => true]); + + request()->query->set('facets', ['test_taxonomy__exists' => '1']); + $manager = new FacetsManager(); + $manager->run(); + + $this->go_to('/?facets[test_taxonomy__exists]=1'); + global $wp_query; + + $tax_query = $wp_query->get('tax_query'); + $this->assertEquals('test_taxonomy', $tax_query[0]['taxonomy']); + $this->assertEquals('EXISTS', $tax_query[0]['operator']); + $this->assertArrayNotHasKey('terms', $tax_query[0]); + $this->assertArrayNotHasKey('field', $tax_query[0]); + } + + public function test_it_should_combine_taxonomy_and_regular_queries() + { + register_taxonomy('test_taxonomy', 'post', ['public' => true]); + + request()->query->set('facets', [ + 'test_taxonomy' => 'test-slug', + 'meta_key' => 'custom_field', + 'posts_per_page' => 5 + ]); + $manager = new FacetsManager(); + $manager->run(); + + $this->go_to('/?facets[test_taxonomy]=test-slug&facets[meta_key]=custom_field&facets[posts_per_page]=5'); + global $wp_query; + + // Check taxonomy query + $tax_query = $wp_query->get('tax_query'); + $this->assertIsArray($tax_query); + $this->assertEquals('test_taxonomy', $tax_query[0]['taxonomy']); + + // Check regular query vars + $this->assertEquals('custom_field', $wp_query->query_vars['meta_key']); + $this->assertEquals(5, $wp_query->query_vars['posts_per_page']); + } + + public function test_it_should_handle_existing_tax_query() + { + register_taxonomy('test_taxonomy', 'post', ['public' => true]); + + request()->query->set('facets', ['test_taxonomy' => 'new-slug']); + $manager = new FacetsManager(); + $manager->run(); + + // Set up existing tax_query + $existing_tax_query = [ + [ + 'taxonomy' => 'category', + 'field' => 'slug', + 'terms' => ['existing-category'], + 'operator' => 'IN' + ] + ]; + + $this->go_to('/?facets[test_taxonomy]=new-slug'); + global $wp_query; + $wp_query->set('tax_query', $existing_tax_query); + + // Trigger the facets query modification + $manager->add_facets_to_query($wp_query); + + $tax_query = $wp_query->get('tax_query'); + $this->assertCount(2, $tax_query); + $this->assertEquals('test_taxonomy', $tax_query[0]['taxonomy']); + $this->assertEquals('category', $tax_query[1][0]['taxonomy']); + } + + public function test_it_should_handle_array_values_in_facets() + { + register_taxonomy('test_taxonomy', 'post', ['public' => true]); + + request()->query->set('facets', ['test_taxonomy' => ['slug1', 'slug2']]); + $manager = new FacetsManager(); + $manager->run(); + + $this->go_to('/?facets[test_taxonomy][]=slug1&facets[test_taxonomy][]=slug2'); + global $wp_query; + + $tax_query = $wp_query->get('tax_query'); + $this->assertEquals(['slug1', 'slug2'], $tax_query[0]['terms']); + $this->assertEquals('slug', $tax_query[0]['field']); + } }