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;
+	}
 }