From 1977e1586ba63ca9f4cc579c9c7e989c4b08d7d1 Mon Sep 17 00:00:00 2001 From: Kelly Dwan Date: Wed, 9 Jan 2019 10:21:23 -0500 Subject: [PATCH] REST API: Update products endpoint to get products across different attributes (https://github.com/woocommerce/woocommerce-blocks/pull/299) * Add products by attributes tests * Add new properties to Products endpoint args Allows for requesting a combination of attribute terms across different attributes. * Unskip working tests --- .../class-wgpb-products-controller.php | 57 ++++- .../tests/api/products-attributes.php | 211 ++++++++++++++++++ 2 files changed, 265 insertions(+), 3 deletions(-) create mode 100644 plugins/woocommerce-blocks/tests/api/products-attributes.php diff --git a/plugins/woocommerce-blocks/includes/class-wgpb-products-controller.php b/plugins/woocommerce-blocks/includes/class-wgpb-products-controller.php index c3e3bc3fd36..e9c06676990 100644 --- a/plugins/woocommerce-blocks/includes/class-wgpb-products-controller.php +++ b/plugins/woocommerce-blocks/includes/class-wgpb-products-controller.php @@ -230,9 +230,11 @@ class WGPB_Products_Controller extends WC_REST_Products_Controller { protected function prepare_objects_query( $request ) { $args = parent::prepare_objects_query( $request ); - $orderby = $request->get_param( 'orderby' ); - $order = $request->get_param( 'order' ); - $cat_operator = $request->get_param( 'cat_operator' ); + $orderby = $request->get_param( 'orderby' ); + $order = $request->get_param( 'order' ); + $cat_operator = $request->get_param( 'cat_operator' ); + $attributes = $request->get_param( 'attributes' ); + $attr_operator = $request->get_param( 'attr_operator' ); $ordering_args = WC()->query->get_catalog_ordering_args( $orderby, $order ); $args['orderby'] = $ordering_args['orderby']; @@ -250,6 +252,30 @@ class WGPB_Products_Controller extends WC_REST_Products_Controller { } } + $tax_query = array(); + if ( $attributes ) { + foreach ( $attributes as $attribute => $attribute_terms ) { + if ( in_array( $attribute, wc_get_attribute_taxonomy_names(), true ) ) { + $tax_query[] = array( + 'taxonomy' => $attribute, + 'field' => 'term_id', + 'terms' => $attribute_terms, + 'operator' => ! $attr_operator ? 'IN' : $attr_operator, + ); + } + } + } + + // Merge attribute `$tax_query`s into the request's WP_Query args. + if ( ! empty( $tax_query ) ) { + if ( ! empty( $args['tax_query'] ) ) { + $args['tax_query'] = array_merge( $tax_query, $args['tax_query'] ); // WPCS: slow query ok. + } else { + $args['tax_query'] = $tax_query; // WPCS: slow query ok. + } + $args['tax_query']['relation'] = 'AND' === $attr_operator ? 'AND' : 'OR'; + } + return $args; } @@ -292,6 +318,31 @@ class WGPB_Products_Controller extends WC_REST_Products_Controller { 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => 'rest_validate_request_arg', ); + $params['attr_operator'] = array( + 'description' => __( 'Operator to compare product attribute terms.', 'woo-gutenberg-products-block' ), + 'type' => 'string', + 'enum' => array( 'IN', 'AND' ), + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + + $attr_properties = array(); + foreach ( wc_get_attribute_taxonomy_names() as $name ) { + $attr_properties[ $name ] = array( + 'type' => 'array', + 'items' => array( 'type' => 'string' ), + ); + } + $params['attributes'] = array( + 'description' => __( 'Map of attributes to selected terms.', 'woo-gutenberg-products-block' ), + 'type' => 'object', + 'validate_callback' => 'rest_validate_request_arg', + ); + if ( ! empty( $attr_properties ) ) { + $params['attributes']['properties'] = $attr_properties; + $params['attributes']['additionalProperties'] = false; + } + return $params; } diff --git a/plugins/woocommerce-blocks/tests/api/products-attributes.php b/plugins/woocommerce-blocks/tests/api/products-attributes.php new file mode 100644 index 00000000000..480f176b3d8 --- /dev/null +++ b/plugins/woocommerce-blocks/tests/api/products-attributes.php @@ -0,0 +1,211 @@ +user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + + // Create 2 product attributes with terms. + $attr_color = WC_Helper_Product::create_attribute( 'color', array( 'red', 'yellow', 'blue' ) ); + $attr_size = WC_Helper_Product::create_attribute( 'size', array( 'small', 'medium', 'large' ) ); + + $red_term = get_term_by( 'slug', 'red', $attr_color['attribute_taxonomy'] ); + $blue_term = get_term_by( 'slug', 'blue', $attr_color['attribute_taxonomy'] ); + $yellow_term = get_term_by( 'slug', 'yellow', $attr_color['attribute_taxonomy'] ); + $small_term = get_term_by( 'slug', 'small', $attr_size['attribute_taxonomy'] ); + $medium_term = get_term_by( 'slug', 'medium', $attr_size['attribute_taxonomy'] ); + $large_term = get_term_by( 'slug', 'large', $attr_size['attribute_taxonomy'] ); + + $this->attr_term_ids = array( + 'red' => $red_term->term_id, + 'blue' => $blue_term->term_id, + 'yellow' => $yellow_term->term_id, + 'small' => $small_term->term_id, + 'medium' => $medium_term->term_id, + 'large' => $large_term->term_id, + ); + + $color = new WC_Product_Attribute(); + $color->set_id( $attr_color['attribute_id'] ); + $color->set_name( $attr_color['attribute_taxonomy'] ); + $color->set_visible( true ); + + $size = new WC_Product_Attribute(); + $size->set_id( $attr_size['attribute_id'] ); + $size->set_name( $attr_size['attribute_taxonomy'] ); + $size->set_visible( true ); + + // Create some products with a mix of attributes: + // - Product 1 – color: red, blue; size: medium. + // - Product 2 – color: blue; size: large, medium. + // - Product 3 – color: yellow. + $this->products = array(); + $color->set_options( [ $this->attr_term_ids['red'], $this->attr_term_ids['blue'] ] ); + $size->set_options( [ $this->attr_term_ids['medium'] ] ); + $this->products[0] = WC_Helper_Product::create_simple_product( false ); + $this->products[0]->set_attributes( [ $color, $size ] ); + $this->products[0]->save(); + + $color->set_options( [ $this->attr_term_ids['blue'] ] ); + $size->set_options( [ $this->attr_term_ids['medium'], $this->attr_term_ids['large'] ] ); + $this->products[1] = WC_Helper_Product::create_simple_product( false ); + $this->products[1]->set_attributes( [ $color, $size ] ); + $this->products[1]->save(); + + $color->set_options( [ $this->attr_term_ids['yellow'] ] ); + $this->products[2] = WC_Helper_Product::create_simple_product( false ); + $this->products[2]->set_attributes( [ $color ] ); + $this->products[2]->save(); + } + + /** + * Test getting products by a single attribute term. + * + * @since 1.2.0 + */ + public function test_get_products() { + wp_set_current_user( $this->user ); + $request = new WP_REST_Request( 'GET', $this->endpoint . '/products' ); + $request->set_param( 'attribute', 'pa_color' ); + $request->set_param( 'attribute_term', (string) $this->attr_term_ids['red'] ); + + $response = $this->server->dispatch( $request ); + $response_products = $response->get_data(); + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 1, count( $response_products ) ); + } + + /** + * Test getting products by multiple terms in one attribute. + * + * @since 1.2.0 + */ + public function test_get_products_by_multiple_terms() { + wp_set_current_user( $this->user ); + $request = new WP_REST_Request( 'GET', $this->endpoint . '/products' ); + $request->set_param( 'attribute', 'pa_color' ); + $request->set_param( + 'attribute_term', + // Terms list needs to be a string. + $this->attr_term_ids['red'] . ',' . $this->attr_term_ids['yellow'] + ); + + $response = $this->server->dispatch( $request ); + $response_products = $response->get_data(); + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 2, count( $response_products ) ); + } + + /** + * Test getting products by multiple terms in one attribute, matching all. + * + * @since 1.2.0 + */ + public function test_get_products_by_multiple_terms_all() { + wp_set_current_user( $this->user ); + $request = new WP_REST_Request( 'GET', $this->endpoint . '/products' ); + $request->set_param( + 'attributes', + array( + 'pa_color' => array( + $this->attr_term_ids['red'], + $this->attr_term_ids['blue'], + ), + ) + ); + $request->set_param( 'attr_operator', 'AND' ); + + $response = $this->server->dispatch( $request ); + $response_products = $response->get_data(); + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 1, count( $response_products ) ); + } + + /** + * Test getting products by multiple terms in multiple attributes, matching any. + * + * @since 1.2.0 + */ + public function test_get_products_by_multiple_terms_multiple_attrs_any() { + wp_set_current_user( $this->user ); + $request = new WP_REST_Request( 'GET', $this->endpoint . '/products' ); + $request->set_param( + 'attributes', + array( + 'pa_color' => array( $this->attr_term_ids['red'] ), + 'pa_size' => array( $this->attr_term_ids['large'] ), + ) + ); + $request->set_param( 'attr_operator', 'IN' ); + + $response = $this->server->dispatch( $request ); + $response_products = $response->get_data(); + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 2, count( $response_products ) ); + } + + /** + * Test getting products by multiple terms in multiple attributes, matching all. + * + * @since 1.2.0 + */ + public function test_get_products_by_multiple_terms_multiple_attrs_all() { + wp_set_current_user( $this->user ); + $request = new WP_REST_Request( 'GET', $this->endpoint . '/products' ); + $request->set_param( + 'attributes', + array( + 'pa_color' => array( $this->attr_term_ids['blue'] ), + 'pa_size' => array( $this->attr_term_ids['medium'] ), + ) + ); + $request->set_param( 'attr_operator', 'AND' ); + + $response = $this->server->dispatch( $request ); + $response_products = $response->get_data(); + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 2, count( $response_products ) ); + } + + /** + * Test getting products by attributes that don't exist. + * + * Note: This test is currently skipped because the API isn't registering the attribute + * properties correctly, and therefor not validating attribute names against "real" attributes. + * + * @since 1.2.0 + */ + public function xtest_get_products_by_fake_attrs() { + wp_set_current_user( $this->user ); + $request = new WP_REST_Request( 'GET', $this->endpoint . '/products' ); + $request->set_param( 'attributes', array( 'pa_fake' => array( 21 ) ) ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 400, $response->get_status() ); + } +}