From e385e4f363fafc5beb783220f1254ca3f20af27e Mon Sep 17 00:00:00 2001 From: Sukhendu Sekhar Guria Date: Tue, 7 Apr 2026 14:39:31 +0530 Subject: [PATCH 1/2] Multisite: Add tax_query support to WP_Site_Query --- src/wp-includes/class-wp-site-query.php | 54 ++- tests/phpunit/tests/multisite/wpSiteQuery.php | 327 ++++++++++++++++++ 2 files changed, 380 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/class-wp-site-query.php b/src/wp-includes/class-wp-site-query.php index 52ae228d90af0..65f30c1b7e4ee 100644 --- a/src/wp-includes/class-wp-site-query.php +++ b/src/wp-includes/class-wp-site-query.php @@ -64,6 +64,22 @@ class WP_Site_Query { */ public $date_query = false; + /** + * Taxonomy query container. + * + * @since x.x.x + * @var WP_Tax_Query A taxonomy query instance. + */ + public $tax_query = false; + + /** + * Taxonomy query clauses. + * + * @since x.x.x + * @var array + */ + protected $tax_query_clauses; + /** * Query vars set by the user. * @@ -112,6 +128,7 @@ class WP_Site_Query { * @since 5.1.0 Introduced the 'update_site_meta_cache', 'meta_query', 'meta_key', * 'meta_compare_key', 'meta_value', 'meta_type', and 'meta_compare' parameters. * @since 5.3.0 Introduced the 'meta_type_key' parameter. + * @since x.x.x Introduced the 'tax_query' parameter. * * @param string|array $query { * Optional. Array or query string of site query parameters. Default empty. @@ -183,6 +200,8 @@ class WP_Site_Query { * See WP_Meta_Query::__construct() for accepted values and default value. * @type array $meta_query An associative array of WP_Meta_Query arguments. * See WP_Meta_Query::__construct() for accepted values. + * @type array $tax_query An associative array of WP_Tax_Query arguments. + * See WP_Tax_Query::__construct() for accepted values. * } */ public function __construct( $query = '' ) { @@ -224,6 +243,7 @@ public function __construct( $query = '' ) { 'meta_value' => '', 'meta_type' => '', 'meta_compare' => '', + 'tax_query' => '', ); if ( ! empty( $query ) ) { @@ -306,6 +326,21 @@ public function get_sites() { $this->meta_query_clauses = $this->meta_query->get_sql( 'blog', $wpdb->blogs, 'blog_id', $this ); } + // Parse taxonomy query. + if ( ! empty( $this->query_vars['tax_query'] ) && is_array( $this->query_vars['tax_query'] ) ) { + $this->tax_query = new WP_Tax_Query( $this->query_vars['tax_query'] ); + $this->tax_query_clauses = $this->tax_query->get_sql( $wpdb->blogs, 'blog_id' ); + } + + /** + * Fires after the site taxonomy query has been parsed. + * + * @since x.x.x + * + * @param WP_Site_Query $query The WP_Site_Query instance (passed by reference). + */ + do_action_ref_array( 'parse_site_tax_query', array( &$this ) ); + $site_data = null; /** @@ -383,7 +418,10 @@ public function get_sites() { // If querying for a count only, there's nothing more to do. if ( $this->query_vars['count'] ) { // $site_ids is actually a count in this case. - return (int) $site_ids; + $count = (int) $site_ids; + // Set found_sites for consistency with non-count queries. + $this->found_sites = $count; + return $count; } $site_ids = array_map( 'intval', $site_ids ); @@ -497,6 +535,9 @@ protected function get_site_ids() { if ( $this->query_vars['count'] ) { $fields = 'COUNT(*)'; + if ( ! empty( $this->tax_query_clauses ) ) { + $fields = "COUNT(DISTINCT {$wpdb->blogs}.blog_id)"; + } } else { $fields = "{$wpdb->blogs}.blog_id"; } @@ -651,6 +692,17 @@ protected function get_site_ids() { } } + if ( ! empty( $this->tax_query_clauses ) ) { + $join .= $this->tax_query_clauses['join']; + + // Strip leading 'AND'. + $this->sql_clauses['where']['tax_query'] = preg_replace( '/^\s*AND\s*/', '', $this->tax_query_clauses['where'] ); + + if ( ! $this->query_vars['count'] ) { + $groupby = "{$wpdb->blogs}.blog_id"; + } + } + $where = implode( ' AND ', $this->sql_clauses['where'] ); $pieces = array( 'fields', 'join', 'where', 'orderby', 'limits', 'groupby' ); diff --git a/tests/phpunit/tests/multisite/wpSiteQuery.php b/tests/phpunit/tests/multisite/wpSiteQuery.php index 0bca5818fe863..9769bd243419c 100644 --- a/tests/phpunit/tests/multisite/wpSiteQuery.php +++ b/tests/phpunit/tests/multisite/wpSiteQuery.php @@ -923,6 +923,333 @@ public function test_wp_site_query_cache_with_same_fields_different_cache_fields $this->assertSame( $number_of_queries, get_num_queries() ); } + /** + * @ticket 46184 + * @dataProvider data_wp_site_query_tax_query + */ + public function test_wp_site_query_tax_query( $query, $expected, $strict ) { + register_taxonomy( 'wptests_site_tax', 'blog' ); + + // Create test terms. + $term_1 = self::factory()->term->create( + array( + 'taxonomy' => 'wptests_site_tax', + 'slug' => 'test-term-1', + 'name' => 'Test Term 1', + ) + ); + $term_2 = self::factory()->term->create( + array( + 'taxonomy' => 'wptests_site_tax', + 'slug' => 'test-term-2', + 'name' => 'Test Term 2', + ) + ); + $term_3 = self::factory()->term->create( + array( + 'taxonomy' => 'wptests_site_tax', + 'slug' => 'test-term-3', + 'name' => 'Test Term 3', + ) + ); + + // Assign terms to sites. + wp_set_object_terms( self::$site_ids['wordpress.org/'], $term_1, 'wptests_site_tax' ); + wp_set_object_terms( self::$site_ids['wordpress.org/foo/'], $term_2, 'wptests_site_tax' ); + wp_set_object_terms( self::$site_ids['wordpress.org/foo/bar/'], array( $term_1, $term_2 ), 'wptests_site_tax' ); + wp_set_object_terms( self::$site_ids['make.wordpress.org/'], $term_3, 'wptests_site_tax' ); + + $query['fields'] = 'ids'; + + $q = new WP_Site_Query(); + $found = $q->query( $query ); + + foreach ( $expected as $index => $domain_path ) { + $expected[ $index ] = self::$site_ids[ $domain_path ]; + } + + if ( $strict ) { + $this->assertSame( $expected, $found ); + } else { + $this->assertSameSets( $expected, $found ); + } + } + + public function data_wp_site_query_tax_query() { + return array( + // Single term by slug. + array( + array( + 'tax_query' => array( + array( + 'taxonomy' => 'wptests_site_tax', + 'field' => 'slug', + 'terms' => 'test-term-1', + ), + ), + ), + array( + 'wordpress.org/', + 'wordpress.org/foo/bar/', + ), + false, + ), + // Single term by name. + array( + array( + 'tax_query' => array( + array( + 'taxonomy' => 'wptests_site_tax', + 'field' => 'name', + 'terms' => 'Test Term 1', + ), + ), + ), + array( + 'wordpress.org/', + 'wordpress.org/foo/bar/', + ), + false, + ), + // Multiple terms with IN operator. + array( + array( + 'tax_query' => array( + array( + 'taxonomy' => 'wptests_site_tax', + 'field' => 'slug', + 'terms' => array( 'test-term-1', 'test-term-2' ), + ), + ), + ), + array( + 'wordpress.org/', + 'wordpress.org/foo/', + 'wordpress.org/foo/bar/', + ), + false, + ), + // NOT IN operator - excludes sites with the term. + array( + array( + 'tax_query' => array( + array( + 'taxonomy' => 'wptests_site_tax', + 'field' => 'slug', + 'terms' => array( 'test-term-1' ), + 'operator' => 'NOT IN', + ), + ), + 'site__not_in' => array( 1 ), // Exclude main site. + ), + array( + 'wordpress.org/foo/', + 'make.wordpress.org/', + 'make.wordpress.org/foo/', + 'www.w.org/', + 'www.w.org/foo/', + 'www.w.org/foo/bar/', + 'www.w.org/make/', + ), + false, + ), + // AND operator (must have all terms). + array( + array( + 'tax_query' => array( + array( + 'taxonomy' => 'wptests_site_tax', + 'field' => 'slug', + 'terms' => array( 'test-term-1', 'test-term-2' ), + 'operator' => 'AND', + ), + ), + ), + array( + 'wordpress.org/foo/bar/', + ), + false, + ), + // Nested tax query with relation OR. + array( + array( + 'tax_query' => array( + 'relation' => 'OR', + array( + 'taxonomy' => 'wptests_site_tax', + 'field' => 'slug', + 'terms' => 'test-term-1', + ), + array( + 'taxonomy' => 'wptests_site_tax', + 'field' => 'slug', + 'terms' => 'test-term-3', + ), + ), + ), + array( + 'wordpress.org/', + 'wordpress.org/foo/bar/', + 'make.wordpress.org/', + ), + false, + ), + // Nested tax query with relation AND. + array( + array( + 'tax_query' => array( + 'relation' => 'AND', + array( + 'taxonomy' => 'wptests_site_tax', + 'field' => 'slug', + 'terms' => 'test-term-1', + ), + array( + 'taxonomy' => 'wptests_site_tax', + 'field' => 'slug', + 'terms' => 'test-term-2', + ), + ), + ), + array( + 'wordpress.org/foo/bar/', + ), + false, + ), + // EXISTS operator - sites that have any term in this taxonomy. + array( + array( + 'tax_query' => array( + array( + 'taxonomy' => 'wptests_site_tax', + 'operator' => 'EXISTS', + ), + ), + ), + array( + 'wordpress.org/', + 'wordpress.org/foo/', + 'wordpress.org/foo/bar/', + 'make.wordpress.org/', + ), + false, + ), + // NOT EXISTS operator - sites that have no terms in this taxonomy (excluding main site from expected). + array( + array( + 'tax_query' => array( + array( + 'taxonomy' => 'wptests_site_tax', + 'operator' => 'NOT EXISTS', + ), + ), + 'site__not_in' => array( 1 ), // Exclude main site. + ), + array( + 'make.wordpress.org/foo/', + 'www.w.org/', + 'www.w.org/foo/', + 'www.w.org/foo/bar/', + 'www.w.org/make/', + ), + false, + ), + ); + } + + /** + * @ticket 46184 + */ + public function test_wp_site_query_tax_query_with_term_taxonomy_id() { + register_taxonomy( 'wptests_site_tax', 'blog' ); + + // Create test term. + $term_1 = self::factory()->term->create( + array( + 'taxonomy' => 'wptests_site_tax', + 'slug' => 'test-term-1', + 'name' => 'Test Term 1', + ) + ); + + wp_set_object_terms( self::$site_ids['wordpress.org/'], $term_1, 'wptests_site_tax' ); + + $term = get_term( $term_1, 'wptests_site_tax' ); + + $q = new WP_Site_Query( + array( + 'fields' => 'ids', + 'tax_query' => array( + array( + 'taxonomy' => 'wptests_site_tax', + 'field' => 'term_taxonomy_id', + 'terms' => array( $term->term_taxonomy_id ), + ), + ), + ) + ); + + $this->assertSameSets( array( self::$site_ids['wordpress.org/'] ), $q->sites ); + } + + /** + * @ticket 46184 + */ + public function test_wp_site_query_tax_query_with_count() { + // Create a test taxonomy for sites. + register_taxonomy( 'wptests_site_tax_count', 'blog' ); + + // Create test terms with unique slugs. + $term_1 = self::factory()->term->create( + array( + 'taxonomy' => 'wptests_site_tax_count', + 'slug' => 'count-term-1', + ) + ); + $term_2 = self::factory()->term->create( + array( + 'taxonomy' => 'wptests_site_tax_count', + 'slug' => 'count-term-2', + ) + ); + + // Assign terms to sites. + wp_set_object_terms( self::$site_ids['wordpress.org/'], $term_1, 'wptests_site_tax_count' ); + wp_set_object_terms( self::$site_ids['wordpress.org/foo/'], $term_2, 'wptests_site_tax_count' ); + + $q1 = new WP_Site_Query( + array( + 'fields' => 'ids', + 'tax_query' => array( + array( + 'taxonomy' => 'wptests_site_tax_count', + 'field' => 'slug', + 'terms' => array( 'count-term-1', 'count-term-2' ), + ), + ), + ) + ); + + $this->assertCount( 2, $q1->sites, 'Without count, should find 2 sites' ); + + $q2 = new WP_Site_Query(); + $count2 = $q2->query( + array( + 'count' => true, + 'tax_query' => array( + array( + 'taxonomy' => 'wptests_site_tax_count', + 'field' => 'slug', + 'terms' => array( 'count-term-1', 'count-term-2' ), + ), + ), + ) + ); + + $this->assertSame( 2, $count2, 'With count=true, query() should return 2' ); + $this->assertSame( 2, $q2->found_sites, 'found_sites should be 2' ); + } + /** * @ticket 40229 * @dataProvider data_wp_site_query_meta_query From 9cc16cc00384760661e82e7dbf499222d83858c8 Mon Sep 17 00:00:00 2001 From: Sukhendu Sekhar Guria Date: Tue, 7 Apr 2026 15:09:56 +0530 Subject: [PATCH 2/2] fix: remove PHPCS errors --- tests/phpunit/tests/multisite/wpSiteQuery.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/phpunit/tests/multisite/wpSiteQuery.php b/tests/phpunit/tests/multisite/wpSiteQuery.php index 9769bd243419c..8b8cd1fd17574 100644 --- a/tests/phpunit/tests/multisite/wpSiteQuery.php +++ b/tests/phpunit/tests/multisite/wpSiteQuery.php @@ -1032,7 +1032,7 @@ public function data_wp_site_query_tax_query() { // NOT IN operator - excludes sites with the term. array( array( - 'tax_query' => array( + 'tax_query' => array( array( 'taxonomy' => 'wptests_site_tax', 'field' => 'slug', @@ -1137,7 +1137,7 @@ public function data_wp_site_query_tax_query() { // NOT EXISTS operator - sites that have no terms in this taxonomy (excluding main site from expected). array( array( - 'tax_query' => array( + 'tax_query' => array( array( 'taxonomy' => 'wptests_site_tax', 'operator' => 'NOT EXISTS',