diff --git a/plugins/woocommerce-blocks/src/BlockTypes/ProductQuery.php b/plugins/woocommerce-blocks/src/BlockTypes/ProductQuery.php index 27ff5d8b67b..6b63c16d489 100644 --- a/plugins/woocommerce-blocks/src/BlockTypes/ProductQuery.php +++ b/plugins/woocommerce-blocks/src/BlockTypes/ProductQuery.php @@ -127,8 +127,9 @@ class ProductQuery extends AbstractBlock { $orderby_query = isset( $orderby ) ? $this->get_custom_orderby_query( $orderby ) : array(); $attributes_query = is_array( $woo_attributes ) ? $this->get_product_attributes_query( $woo_attributes ) : array(); $stock_query = is_array( $woo_stock_status ) ? $this->get_stock_status_query( $woo_stock_status ) : array(); + $visibility_query = $this->get_product_visibility_query( $stock_query ); - return array_merge( $args, $on_sale_query, $orderby_query, $attributes_query, $stock_query ); + return array_merge( $args, $on_sale_query, $orderby_query, $attributes_query, $stock_query, $visibility_query ); } /** @@ -325,6 +326,34 @@ class ProductQuery extends AbstractBlock { * @return array */ private function get_stock_status_query( $stock_statii ) { + if ( ! is_array( $stock_statii ) ) { + return array(); + } + + $stock_status_options = array_keys( wc_get_product_stock_status_options() ); + + /** + * If all available stock status are selected, we don't need to add the + * meta query for stock status. + */ + if ( + count( $stock_statii ) === count( $stock_status_options ) && + array_diff( $stock_statii, $stock_status_options ) === array_diff( $stock_status_options, $stock_statii ) + ) { + return array(); + } + + /** + * If all stock statuses are selected except 'outofstock', we use the + * product visibility query to filter out out of stock products. + * + * @see get_product_visibility_query() + */ + $diff = array_diff( $stock_status_options, $stock_statii ); + if ( count( $diff ) === 1 && in_array( 'outofstock', $diff, true ) ) { + return array(); + } + return array( 'meta_query' => array( array( @@ -336,6 +365,34 @@ class ProductQuery extends AbstractBlock { ); } + /** + * Return a query for product visibility depending on their stock status. + * + * @param array $stock_query Stock status query. + * + * @return array Tax query for product visibility. + */ + private function get_product_visibility_query( $stock_query ) { + $product_visibility_terms = wc_get_product_visibility_term_ids(); + $product_visibility_not_in = array( is_search() ? $product_visibility_terms['exclude-from-search'] : $product_visibility_terms['exclude-from-catalog'] ); + + // Hide out of stock products. + if ( empty( $stock_query ) && 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) { + $product_visibility_not_in[] = $product_visibility_terms['outofstock']; + } + + return array( + 'tax_query' => array( + array( + 'taxonomy' => 'product_visibility', + 'field' => 'term_taxonomy_id', + 'terms' => $product_visibility_not_in, + 'operator' => 'NOT IN', + ), + ), + ); + } + /** * Set the query vars that are used by filter blocks. * @@ -439,11 +496,13 @@ class ProductQuery extends AbstractBlock { $on_sale_enabled = isset( $query['__woocommerceOnSale'] ) && true === $query['__woocommerceOnSale']; $attributes_query = isset( $query['__woocommerceAttributes'] ) ? $this->get_product_attributes_query( $query['__woocommerceAttributes'] ) : array(); $stock_query = isset( $query['__woocommerceStockStatus'] ) ? $this->get_stock_status_query( $query['__woocommerceStockStatus'] ) : array(); + $visibility_query = $this->get_product_visibility_query( $stock_query ); return array( 'on_sale' => ( $on_sale_enabled ? $this->get_on_sale_products_query() : array() ), 'attributes' => $attributes_query, 'stock_status' => $stock_query, + 'visibility' => $visibility_query, ); } diff --git a/plugins/woocommerce-blocks/tests/php/BlockTypes/ProductQuery.php b/plugins/woocommerce-blocks/tests/php/BlockTypes/ProductQuery.php index 38e42e2e03e..f4806646dc4 100644 --- a/plugins/woocommerce-blocks/tests/php/BlockTypes/ProductQuery.php +++ b/plugins/woocommerce-blocks/tests/php/BlockTypes/ProductQuery.php @@ -89,7 +89,10 @@ class ProductQuery extends \WP_UnitTestCase { foreach ( $on_sale_product_ids as $id ) { $this->assertContainsEquals( $id, $merged_query['post__in'] ); } + $this->assertCount( 4, $merged_query['post__in'] ); + + delete_transient( 'wc_products_onsale' ); } /** @@ -112,24 +115,34 @@ class ProductQuery extends \WP_UnitTestCase { ), $merged_query['meta_query'] ); + } + /** + * Test merging default stock queries that should use product visibility + * queries instead of meta query for stock status. + */ + public function test_merging_default_stock_queries() { + $parsed_block = $this->get_base_parsed_block(); + $parsed_block['attrs']['query']['__woocommerceStockStatus'] = array( + 'instock', + 'outofstock', + 'onbackorder', + ); + + $merged_query = $this->initialize_merged_query( $parsed_block ); + + $this->assertEmpty( $merged_query['meta_query'] ); + + // Test with hide out of stock items option enabled. $parsed_block = $this->get_base_parsed_block(); $parsed_block['attrs']['query']['__woocommerceStockStatus'] = array( 'instock', 'onbackorder', ); - $this->block_instance->set_parsed_block( $parsed_block ); - $merged_query = $this->block_instance->build_query( $parsed_block['attrs']['query'] ); + $merged_query = $this->initialize_merged_query( $parsed_block ); - $this->assertContainsEquals( - array( - 'compare' => 'IN', - 'key' => '_stock_status', - 'value' => array( 'instock', 'onbackorder' ), - ), - $merged_query['meta_query'] - ); + $this->assertEmpty( $merged_query['meta_query'] ); } /** @@ -201,6 +214,57 @@ class ProductQuery extends \WP_UnitTestCase { $this->assertEquals( 'total_sales', $merged_query['meta_key'] ); } + /** + * Test product visibility query exist in merged query. + */ + public function test_product_visibility_query_exist_in_merged_query() { + $product_visibility_terms = wc_get_product_visibility_term_ids(); + $product_visibility_not_in = array( is_search() ? $product_visibility_terms['exclude-from-search'] : $product_visibility_terms['exclude-from-catalog'] ); + + $parsed_block = $this->get_base_parsed_block(); + + $merged_query = $this->initialize_merged_query( $parsed_block ); + + $this->assertContainsEquals( + array( + 'taxonomy' => 'product_visibility', + 'field' => 'term_taxonomy_id', + 'terms' => $product_visibility_not_in, + 'operator' => 'NOT IN', + ), + $merged_query['tax_query'] + ); + + $fn = function() { + return 'yes'; + }; + + // Test with hide out of stock items option enabled. + add_filter( + 'pre_option_woocommerce_hide_out_of_stock_items', + $fn + ); + $product_visibility_not_in[] = $product_visibility_terms['outofstock']; + + $parsed_block = $this->get_base_parsed_block(); + + $merged_query = $this->initialize_merged_query( $parsed_block ); + + $this->assertContainsEquals( + array( + 'taxonomy' => 'product_visibility', + 'field' => 'term_taxonomy_id', + 'terms' => $product_visibility_not_in, + 'operator' => 'NOT IN', + ), + $merged_query['tax_query'] + ); + remove_filter( + 'pre_option_woocommerce_hide_out_of_stock_items', + $fn + ); + } + /** * Test merging multiple queries. */ @@ -209,7 +273,7 @@ class ProductQuery extends \WP_UnitTestCase { $parsed_block['attrs']['query']['orderBy'] = 'rating'; $parsed_block['attrs']['query']['__woocommerceStockStatus'] = array( 'instock', - 'onbackorder', + 'outofstock', ); $parsed_block['attrs']['query']['__woocommerceAttributes'] = array( array( @@ -230,7 +294,7 @@ class ProductQuery extends \WP_UnitTestCase { array( 'compare' => 'IN', 'key' => '_stock_status', - 'value' => array( 'instock', 'onbackorder' ), + 'value' => array( 'instock', 'outofstock' ), ), $merged_query['meta_query'] ); @@ -266,6 +330,7 @@ class ProductQuery extends \WP_UnitTestCase { ), $merged_query['meta_query'] ); + set_query_var( 'max_price', '' ); } /** @@ -289,6 +354,7 @@ class ProductQuery extends \WP_UnitTestCase { ), $merged_query['meta_query'] ); + set_query_var( 'min_price', '' ); } /** @@ -318,6 +384,9 @@ class ProductQuery extends \WP_UnitTestCase { ), $merged_query['meta_query'] ); + + set_query_var( 'max_price', '' ); + set_query_var( 'min_price', '' ); } /** @@ -336,6 +405,8 @@ class ProductQuery extends \WP_UnitTestCase { ), $merged_query['meta_query'] ); + + set_query_var( 'filter_stock_status', '' ); } /** @@ -363,8 +434,16 @@ class ProductQuery extends \WP_UnitTestCase { $merged_query = $this->initialize_merged_query(); - $attribute_tax_query = $merged_query['tax_query'][0]; + $attribute_tax_query = array(); + + foreach ( $merged_query['tax_query'] as $tax_query ) { + if ( isset( $tax_query['relation'] ) ) { + $attribute_tax_query = $tax_query; + } + } + $attribute_tax_query_queries = $attribute_tax_query[0]; + $this->assertEquals( 'AND', $attribute_tax_query['relation'] ); $this->assertContainsEquals( @@ -385,6 +464,11 @@ class ProductQuery extends \WP_UnitTestCase { ), $attribute_tax_query_queries ); + + set_query_var( 'filter_color', '' ); + set_query_var( 'query_type_color', '' ); + set_query_var( 'filter_size', '' ); + set_query_var( 'query_type_size', '' ); } /** @@ -424,6 +508,10 @@ class ProductQuery extends \WP_UnitTestCase { ), $merged_query['meta_query'] ); + + set_query_var( 'max_price', '' ); + set_query_var( 'min_price', '' ); + set_query_var( 'filter_stock_status', '' ); } }