diff --git a/includes/abstracts/abstract-wc-rest-controller.php b/includes/abstracts/abstract-wc-rest-controller.php index 8bd1dd86cc8..6d4616868e1 100644 --- a/includes/abstracts/abstract-wc-rest-controller.php +++ b/includes/abstracts/abstract-wc-rest-controller.php @@ -319,6 +319,24 @@ abstract class WC_REST_Controller extends WP_REST_Controller { ); } + /** + * Add meta query. + * + * @since 2.7.0 + * @param array $args Query args. + * @param array $meta_query Meta query. + * @return array + */ + protected function add_meta_query( $args, $meta_query ) { + if ( ! empty( $args['meta_query'] ) ) { + $args['meta_query'] = array(); + } + + $args['meta_query'][] = $meta_query; + + return $args['meta_query']; + } + /** * Get the batch schema, conforming to JSON Schema. * diff --git a/includes/api/class-wc-rest-products-controller.php b/includes/api/class-wc-rest-products-controller.php index 3a23e960ee9..120f8d2f1fa 100644 --- a/includes/api/class-wc-rest-products-controller.php +++ b/includes/api/class-wc-rest-products-controller.php @@ -186,15 +186,39 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { // Filter by sku. if ( ! empty( $request['sku'] ) ) { - if ( ! empty( $args['meta_query'] ) ) { - $args['meta_query'] = array(); - } + $args['meta_query'] = $this->add_meta_query( $args, array( + 'key' => '_sku', + 'value' => $request['sku'], + ) ); + } - $args['meta_query'][] = array( - 'key' => '_sku', - 'value' => $request['sku'], - 'compare' => '=', - ); + // Filter featured. + if ( is_bool( $request['featured'] ) ) { + $args['meta_query'] = $this->add_meta_query( $args, array( + 'key' => '_featured', + 'value' => true === $request['featured'] ? 'yes' : 'no', + ) ); + } + + // Filter by tax class. + if ( ! empty( $request['tax_class'] ) ) { + $args['meta_query'] = $this->add_meta_query( $args, array( + 'key' => '_tax_class', + 'value' => 'standard' !== $request['tax_class'] ? $request['tax_class'] : '', + ) ); + } + + // Price filter. + if ( ! empty( $request['min_price'] ) || ! empty( $request['max_price'] ) ) { + $args['meta_query'] = $this->add_meta_query( $args, wc_get_min_max_price_meta_query( $request ) ); + } + + // Filter product in stock or out of stock. + if ( is_bool( $request['in_stock'] ) ) { + $args['meta_query'] = $this->add_meta_query( $args, array( + 'key' => '_stock_status', + 'value' => true === $request['in_stock'] ? 'instock' : 'outofstock', + ) ); } // Apply all WP_Query filters again. @@ -2689,20 +2713,32 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { 'sanitize_callback' => 'sanitize_key', 'validate_callback' => 'rest_validate_request_arg', ); + $params['sku'] = array( + 'description' => __( 'Limit result set to products with a specific SKU.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['featured'] = array( + 'description' => __( 'Limit result set to featured products.', 'woocommerce' ), + 'type' => 'boolean', + 'sanitize_callback' => 'wc_string_to_bool', + 'validate_callback' => 'rest_validate_request_arg', + ); $params['category'] = array( - 'description' => __( 'Limit result set to products assigned a specific category.', 'woocommerce' ), + 'description' => __( 'Limit result set to products assigned a specific category ID.', 'woocommerce' ), 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => 'rest_validate_request_arg', ); $params['tag'] = array( - 'description' => __( 'Limit result set to products assigned a specific tag.', 'woocommerce' ), + 'description' => __( 'Limit result set to products assigned a specific tag ID.', 'woocommerce' ), 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => 'rest_validate_request_arg', ); $params['shipping_class'] = array( - 'description' => __( 'Limit result set to products assigned a specific shipping class.', 'woocommerce' ), + 'description' => __( 'Limit result set to products assigned a specific shipping class ID.', 'woocommerce' ), 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => 'rest_validate_request_arg', @@ -2714,13 +2750,36 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { 'validate_callback' => 'rest_validate_request_arg', ); $params['attribute_term'] = array( - 'description' => __( 'Limit result set to products with a specific attribute term (required an assigned attribute).', 'woocommerce' ), + 'description' => __( 'Limit result set to products with a specific attribute term ID (required an assigned attribute).', 'woocommerce' ), 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => 'rest_validate_request_arg', ); - $params['sku'] = array( - 'description' => __( 'Limit result set to products with a specific SKU.', 'woocommerce' ), + + if ( wc_tax_enabled() ) { + $params['tax_class'] = array( + 'description' => __( 'Limit result set to products with a specific tax class.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array_map( 'sanitize_title', array_merge( array( 'standard' ), WC_Tax::get_tax_classes() ) ), + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + } + + $params['in_stock'] = array( + 'description' => __( 'Limit result set to products in stock or out of stock.', 'woocommerce' ), + 'type' => 'boolean', + 'sanitize_callback' => 'wc_string_to_bool', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['min_price'] = array( + 'description' => __( 'Limit result set to products based on a minimum price.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['max_price'] = array( + 'description' => __( 'Limit result set to products based on a maximum price.', 'woocommerce' ), 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => 'rest_validate_request_arg', diff --git a/includes/class-wc-query.php b/includes/class-wc-query.php index 56ed8f9da33..cc1de8f9ddd 100644 --- a/includes/class-wc-query.php +++ b/includes/class-wc-query.php @@ -545,35 +545,12 @@ class WC_Query { */ private function price_filter_meta_query() { if ( isset( $_GET['max_price'] ) || isset( $_GET['min_price'] ) ) { - $min = isset( $_GET['min_price'] ) ? floatval( $_GET['min_price'] ) : 0; - $max = isset( $_GET['max_price'] ) ? floatval( $_GET['max_price'] ) : 9999999999; + $meta_query = wc_get_min_max_price_meta_query( $_GET ); + $meta_query['price_filter'] = true; - /** - * Adjust if the store taxes are not displayed how they are stored. - * Max is left alone because the filter was already increased. - * Kicks in when prices excluding tax are displayed including tax. - */ - if ( wc_tax_enabled() && 'incl' === get_option( 'woocommerce_tax_display_shop' ) && ! wc_prices_include_tax() ) { - $tax_classes = array_merge( array( '' ), WC_Tax::get_tax_classes() ); - $class_min = $min; - - foreach ( $tax_classes as $tax_class ) { - if ( $tax_rates = WC_Tax::get_rates( $tax_class ) ) { - $class_min = $min - WC_Tax::get_tax_total( WC_Tax::calc_exclusive_tax( $min, $tax_rates ) ); - } - } - - $min = $class_min; - } - - return array( - 'key' => '_price', - 'value' => array( $min, $max ), - 'compare' => 'BETWEEN', - 'type' => 'DECIMAL', - 'price_filter' => true, - ); + return $meta_query; } + return array(); } diff --git a/includes/vendor/wp-rest-functions.php b/includes/vendor/wp-rest-functions.php index 83d2dee259a..aec4b332ed3 100644 --- a/includes/vendor/wp-rest-functions.php +++ b/includes/vendor/wp-rest-functions.php @@ -1,6 +1,6 @@ __( 'Hidden', 'woocommerce' ), ) ); } + +/** + * Get min/max price meta query args. + * + * @since 2.7.0 + * @param array $args Min price and max price arguments. + * @return array + */ +function wc_get_min_max_price_meta_query( $args ) { + $min = isset( $args['min_price'] ) ? floatval( $args['min_price'] ) : 0; + $max = isset( $args['max_price'] ) ? floatval( $args['max_price'] ) : 9999999999; + + /** + * Adjust if the store taxes are not displayed how they are stored. + * Max is left alone because the filter was already increased. + * Kicks in when prices excluding tax are displayed including tax. + */ + if ( wc_tax_enabled() && 'incl' === get_option( 'woocommerce_tax_display_shop' ) && ! wc_prices_include_tax() ) { + $tax_classes = array_merge( array( '' ), WC_Tax::get_tax_classes() ); + $class_min = $min; + + foreach ( $tax_classes as $tax_class ) { + if ( $tax_rates = WC_Tax::get_rates( $tax_class ) ) { + $class_min = $min - WC_Tax::get_tax_total( WC_Tax::calc_exclusive_tax( $min, $tax_rates ) ); + } + } + + $min = $class_min; + } + + return array( + 'key' => '_price', + 'value' => array( $min, $max ), + 'compare' => 'BETWEEN', + 'type' => 'DECIMAL', + ); +} diff --git a/tests/unit-tests/product/functions.php b/tests/unit-tests/product/functions.php index 0ce7de86654..038215aa530 100644 --- a/tests/unit-tests/product/functions.php +++ b/tests/unit-tests/product/functions.php @@ -183,4 +183,20 @@ class WC_Tests_Product_Functions extends WC_Unit_Test_Case { // Delete Product WC_Helper_Product::delete_product( $product->id ); } + + /** + * Test wc_get_min_max_price_meta_query() + * + * @since 2.7.0 + */ + public function test_wc_get_min_max_price_meta_query() { + $meta_query = wc_get_min_max_price_meta_query( array( 'min_price' => 10, 'max_price' => 100 ) ); + + $this->assertEquals( array( + 'key' => '_price', + 'value' => array( 10, 100 ), + 'compare' => 'BETWEEN', + 'type' => 'DECIMAL', + ), $meta_query ); + } }