Commit c32fac0c30 for woocommerce
commit c32fac0c30a60c13d4554df24b407517df12681a
Author: Bhavik Kalpesh <53536925+bhavz-10@users.noreply.github.com>
Date: Mon Feb 2 11:20:42 2026 +0530
Product Collection: implement support for Taxonomy filters on posts and pages (#62860)
* fix: ensure product block collection respects taxonomy filter
* test: add unit tests for taxonomy filter function
* feat: add changelog file
* feat: add changelog file
* test: resolve merge conflicts
* test: resolve tests overlapping in QueryBuilder tests file
* test: resolve test functions for taxonomy filters
* fix: add is_string check and use wc_get_container
The is_string check prevents array values as that is not a valid input.
* feat: add the '@since' tag to the docblock
Signed-off-by: Bhavik Kalpesh <bhaviktank1999@gmail.com>
* test: add type to the function parameter
Signed-off-by: Bhavik Kalpesh <bhaviktank1999@gmail.com>
* fix: address linting issues and comment numbering
* test: fix linting issues
* fix: add full-stops to comment lines
---------
Signed-off-by: Bhavik Kalpesh <bhaviktank1999@gmail.com>
Co-authored-by: Karol Manijak <20098064+kmanijak@users.noreply.github.com>
diff --git a/plugins/woocommerce/changelog/61744-product-collection-taxonomy-filters b/plugins/woocommerce/changelog/61744-product-collection-taxonomy-filters
new file mode 100644
index 0000000000..0781a3d439
--- /dev/null
+++ b/plugins/woocommerce/changelog/61744-product-collection-taxonomy-filters
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Product Collection: implement support for Taxonomy filters on posts and pages
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection/QueryBuilder.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection/QueryBuilder.php
index 2b35459bb1..af0a7982f6 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection/QueryBuilder.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection/QueryBuilder.php
@@ -651,6 +651,66 @@ class QueryBuilder {
);
}
+ /**
+ * Return a query that filters products by taxonomy terms.
+ *
+ * @since 10.6.0
+ *
+ * @return array
+ */
+ private function get_filter_by_taxonomy_query() {
+
+ $container = wc_get_container();
+ $params_handler = $container->get( \Automattic\WooCommerce\Internal\ProductFilters\Params::class );
+ $taxonomy_params = $params_handler->get_param( 'taxonomy' );
+
+ if ( empty( $taxonomy_params ) ) {
+ return array();
+ }
+
+ $tax_queries = array();
+
+ foreach ( $taxonomy_params as $taxonomy_slug => $param_key ) {
+ $param_value = get_query_var( $param_key );
+
+ // Adding is_string check to avoid invalid query parameters for the taxonomy.
+ if ( ! is_string( $param_value ) || empty( $param_value ) ) {
+ continue;
+ }
+
+ // Define $term_values by exploding the string.
+ $term_values = explode( ',', $param_value );
+
+ // Sanitize and filter (removes empty strings).
+ $term_slugs = array_values( array_filter( array_map( 'sanitize_title', $term_values ) ) );
+
+ if ( empty( $term_slugs ) ) {
+ continue;
+ }
+
+ $tax_queries[] = array(
+ 'taxonomy' => $taxonomy_slug,
+ 'field' => 'slug',
+ 'terms' => $term_slugs,
+ 'operator' => 'IN',
+ );
+ }
+
+ if ( empty( $tax_queries ) ) {
+ return array();
+ }
+
+ return array(
+ // phpcs:ignore WordPress.DB.SlowDBQuery
+ 'tax_query' => array(
+ array(
+ 'relation' => 'AND',
+ ...$tax_queries,
+ ),
+ ),
+ );
+ }
+
/**
* Merge two array recursively but replace the non-array values instead of
* merging them. The merging strategy:
@@ -727,6 +787,7 @@ class QueryBuilder {
'attributes_filter' => $this->get_filter_by_attributes_query(),
'stock_status_filter' => $this->get_filter_by_stock_status_query(),
'rating_filter' => $this->get_filter_by_rating_query(),
+ 'taxonomy_filter' => $this->get_filter_by_taxonomy_query(),
);
}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection/QueryBuilder.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection/QueryBuilder.php
index 568470b118..5c370f5c5a 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection/QueryBuilder.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection/QueryBuilder.php
@@ -1059,4 +1059,176 @@ class QueryBuilder extends \WP_UnitTestCase {
wp_delete_term( $nike_brand_id, 'product_brand' );
wp_delete_term( $adidas_brand_id, 'product_brand' );
}
+
+ /**
+ * Test merging filter queries by Category Slug (e.g. ?categories=accessories).
+ */
+ public function test_merging_filter_by_category_slug() {
+ // Set the URL query variables.
+ set_query_var( 'categories', 'accessories' );
+
+ // Execute the query builder.
+ $merged_query = Utils::initialize_merged_query( $this->block_instance );
+ $filter_clauses = $this->extract_filter_clauses( $merged_query['tax_query'] );
+
+ // Assertions.
+ $this->assertContainsEquals(
+ array(
+ 'taxonomy' => 'product_cat',
+ 'field' => 'slug',
+ 'terms' => array( 'accessories' ),
+ 'operator' => 'IN',
+ ),
+ $filter_clauses,
+ 'Should contain correct product_cat tax query using slug.'
+ );
+
+ // Clean up.
+ set_query_var( 'categories', '' );
+ }
+
+ /**
+ * Test merging filter queries specifically for Tags.
+ * Scenario: ?tags=tag-new (Slug)
+ */
+ public function test_merging_filter_by_tags() {
+ // Set the URL query variables.
+ set_query_var( 'tags', 'tag-new' );
+
+ // Execute the query builder.
+ $merged_query = Utils::initialize_merged_query( $this->block_instance );
+ $filter_clauses = $this->extract_filter_clauses( $merged_query['tax_query'] );
+
+ // Assertions.
+ $this->assertContainsEquals(
+ array(
+ 'taxonomy' => 'product_tag',
+ 'field' => 'slug',
+ 'terms' => array( 'tag-new' ),
+ 'operator' => 'IN',
+ ),
+ $filter_clauses,
+ 'Should contain correct product_tag tax query with IN operator.'
+ );
+
+ // Clean up.
+ set_query_var( 'tags', '' );
+ }
+
+ /**
+ * Test merging filter queries for Categories, Tags, and Brands simultaneously.
+ * Scenario: ?categories=accessories&tags=tag-new&brands=nike
+ */
+ public function test_merging_filter_by_all_taxonomies_together() {
+ // Set the URL query variables.
+ set_query_var( 'categories', 'accessories' );
+ set_query_var( 'tags', 'tag-new' );
+ set_query_var( 'brands', 'nike' );
+
+ // Execute the query builder.
+ $merged_query = Utils::initialize_merged_query( $this->block_instance );
+ $filter_clauses = $this->extract_filter_clauses( $merged_query['tax_query'] );
+
+ // Assertions.
+ // Verify Category.
+ $this->assertContainsEquals(
+ array(
+ 'taxonomy' => 'product_cat',
+ 'field' => 'slug',
+ 'terms' => array( 'accessories' ),
+ 'operator' => 'IN',
+ ),
+ $filter_clauses,
+ 'Should contain correct product_cat tax query.'
+ );
+
+ // Verify Tag.
+ $this->assertContainsEquals(
+ array(
+ 'taxonomy' => 'product_tag',
+ 'field' => 'slug',
+ 'terms' => array( 'tag-new' ),
+ 'operator' => 'IN',
+ ),
+ $filter_clauses,
+ 'Should contain correct product_tag tax query.'
+ );
+
+ // Verify Brand.
+ $this->assertContainsEquals(
+ array(
+ 'taxonomy' => 'product_brand',
+ 'field' => 'slug',
+ 'terms' => array( 'nike' ),
+ 'operator' => 'IN',
+ ),
+ $filter_clauses,
+ 'Should contain correct product_brand tax query.'
+ );
+
+ // Clean up global state.
+ set_query_var( 'categories', '' );
+ set_query_var( 'tags', '' );
+ set_query_var( 'brands', '' );
+ }
+
+ /**
+ * Test that the strictly string-based filter logic works and SAFELY ignores arrays.
+ * Matches logic: if ( ! is_string($param_value) ) continue;
+ */
+ public function test_filter_strict_string_handling() {
+ // Scenario: Array Input (Should be IGNORED).
+ // ?categories[]=hats.
+ set_query_var( 'categories', array( 'hats' ) );
+
+ // Execute.
+ $merged_query = Utils::initialize_merged_query( $this->block_instance );
+ $tax_queries = $merged_query['tax_query'] ?? array();
+ $filter_clauses = $this->extract_filter_clauses( $tax_queries );
+
+ // Assertion: The array input should have been ignored.
+ $this->assertNotContainsEquals(
+ array(
+ 'taxonomy' => 'product_cat',
+ 'field' => 'slug',
+ 'terms' => array( 'hats' ),
+ 'operator' => 'IN',
+ ),
+ $filter_clauses,
+ 'Should not contain product_cat tax query because array input should be ignored.'
+ );
+
+ // Clean up.
+ set_query_var( 'categories', '' );
+ }
+
+ /**
+ * Helper to extract filter clauses from the tax_query array.
+ *
+ * @param array $tax_queries The tax_query array from the merged query.
+ * @return array The extracted filter clauses.
+ */
+ private function extract_filter_clauses( array $tax_queries ) {
+
+ $and_query = array();
+
+ // Find the 'AND' relation group where filters are stored.
+ foreach ( $tax_queries as $tax_query ) {
+ if ( isset( $tax_query['relation'] ) && 'AND' === $tax_query['relation'] ) {
+ $and_query = $tax_query;
+ break;
+ }
+ }
+
+ $clauses = array();
+ if ( ! empty( $and_query ) ) {
+ foreach ( $and_query as $item ) {
+ if ( is_array( $item ) ) {
+ $clauses[] = $item;
+ }
+ }
+ }
+
+ return $clauses;
+ }
}