Skip to content
This repository was archived by the owner on Feb 23, 2024. It is now read-only.

Commit 34f9a1f

Browse files
authored
REST API: Update products endpoint to get products across different attributes (#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
1 parent c6a7dc2 commit 34f9a1f

File tree

2 files changed

+265
-3
lines changed

2 files changed

+265
-3
lines changed

includes/class-wgpb-products-controller.php

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -230,9 +230,11 @@ public function prepare_item_for_response( $post, $request ) {
230230
protected function prepare_objects_query( $request ) {
231231
$args = parent::prepare_objects_query( $request );
232232

233-
$orderby = $request->get_param( 'orderby' );
234-
$order = $request->get_param( 'order' );
235-
$cat_operator = $request->get_param( 'cat_operator' );
233+
$orderby = $request->get_param( 'orderby' );
234+
$order = $request->get_param( 'order' );
235+
$cat_operator = $request->get_param( 'cat_operator' );
236+
$attributes = $request->get_param( 'attributes' );
237+
$attr_operator = $request->get_param( 'attr_operator' );
236238

237239
$ordering_args = WC()->query->get_catalog_ordering_args( $orderby, $order );
238240
$args['orderby'] = $ordering_args['orderby'];
@@ -250,6 +252,30 @@ protected function prepare_objects_query( $request ) {
250252
}
251253
}
252254

255+
$tax_query = array();
256+
if ( $attributes ) {
257+
foreach ( $attributes as $attribute => $attribute_terms ) {
258+
if ( in_array( $attribute, wc_get_attribute_taxonomy_names(), true ) ) {
259+
$tax_query[] = array(
260+
'taxonomy' => $attribute,
261+
'field' => 'term_id',
262+
'terms' => $attribute_terms,
263+
'operator' => ! $attr_operator ? 'IN' : $attr_operator,
264+
);
265+
}
266+
}
267+
}
268+
269+
// Merge attribute `$tax_query`s into the request's WP_Query args.
270+
if ( ! empty( $tax_query ) ) {
271+
if ( ! empty( $args['tax_query'] ) ) {
272+
$args['tax_query'] = array_merge( $tax_query, $args['tax_query'] ); // WPCS: slow query ok.
273+
} else {
274+
$args['tax_query'] = $tax_query; // WPCS: slow query ok.
275+
}
276+
$args['tax_query']['relation'] = 'AND' === $attr_operator ? 'AND' : 'OR';
277+
}
278+
253279
return $args;
254280
}
255281

@@ -292,6 +318,31 @@ public function get_collection_params() {
292318
'sanitize_callback' => 'sanitize_text_field',
293319
'validate_callback' => 'rest_validate_request_arg',
294320
);
321+
$params['attr_operator'] = array(
322+
'description' => __( 'Operator to compare product attribute terms.', 'woo-gutenberg-products-block' ),
323+
'type' => 'string',
324+
'enum' => array( 'IN', 'AND' ),
325+
'sanitize_callback' => 'sanitize_text_field',
326+
'validate_callback' => 'rest_validate_request_arg',
327+
);
328+
329+
$attr_properties = array();
330+
foreach ( wc_get_attribute_taxonomy_names() as $name ) {
331+
$attr_properties[ $name ] = array(
332+
'type' => 'array',
333+
'items' => array( 'type' => 'string' ),
334+
);
335+
}
336+
$params['attributes'] = array(
337+
'description' => __( 'Map of attributes to selected terms.', 'woo-gutenberg-products-block' ),
338+
'type' => 'object',
339+
'validate_callback' => 'rest_validate_request_arg',
340+
);
341+
if ( ! empty( $attr_properties ) ) {
342+
$params['attributes']['properties'] = $attr_properties;
343+
$params['attributes']['additionalProperties'] = false;
344+
}
345+
295346
return $params;
296347
}
297348

tests/api/products-attributes.php

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
<?php
2+
/**
3+
* @package WooCommerce\Tests\API
4+
*/
5+
6+
/**
7+
* Product Controller "products by attributes" REST API Test
8+
*
9+
* @since 1.2.0
10+
*/
11+
class WC_Tests_API_Products_By_Attributes_Controller extends WC_REST_Unit_Test_Case {
12+
13+
/**
14+
* Endpoints.
15+
*
16+
* @var string
17+
*/
18+
protected $endpoint = '/wc-pb/v3';
19+
20+
/**
21+
* Setup test products data. Called before every test.
22+
*
23+
* @since 1.2.0
24+
*/
25+
public function setUp() {
26+
parent::setUp();
27+
28+
$this->user = $this->factory->user->create(
29+
array(
30+
'role' => 'administrator',
31+
)
32+
);
33+
34+
// Create 2 product attributes with terms.
35+
$attr_color = WC_Helper_Product::create_attribute( 'color', array( 'red', 'yellow', 'blue' ) );
36+
$attr_size = WC_Helper_Product::create_attribute( 'size', array( 'small', 'medium', 'large' ) );
37+
38+
$red_term = get_term_by( 'slug', 'red', $attr_color['attribute_taxonomy'] );
39+
$blue_term = get_term_by( 'slug', 'blue', $attr_color['attribute_taxonomy'] );
40+
$yellow_term = get_term_by( 'slug', 'yellow', $attr_color['attribute_taxonomy'] );
41+
$small_term = get_term_by( 'slug', 'small', $attr_size['attribute_taxonomy'] );
42+
$medium_term = get_term_by( 'slug', 'medium', $attr_size['attribute_taxonomy'] );
43+
$large_term = get_term_by( 'slug', 'large', $attr_size['attribute_taxonomy'] );
44+
45+
$this->attr_term_ids = array(
46+
'red' => $red_term->term_id,
47+
'blue' => $blue_term->term_id,
48+
'yellow' => $yellow_term->term_id,
49+
'small' => $small_term->term_id,
50+
'medium' => $medium_term->term_id,
51+
'large' => $large_term->term_id,
52+
);
53+
54+
$color = new WC_Product_Attribute();
55+
$color->set_id( $attr_color['attribute_id'] );
56+
$color->set_name( $attr_color['attribute_taxonomy'] );
57+
$color->set_visible( true );
58+
59+
$size = new WC_Product_Attribute();
60+
$size->set_id( $attr_size['attribute_id'] );
61+
$size->set_name( $attr_size['attribute_taxonomy'] );
62+
$size->set_visible( true );
63+
64+
// Create some products with a mix of attributes:
65+
// - Product 1 – color: red, blue; size: medium.
66+
// - Product 2 – color: blue; size: large, medium.
67+
// - Product 3 – color: yellow.
68+
$this->products = array();
69+
$color->set_options( [ $this->attr_term_ids['red'], $this->attr_term_ids['blue'] ] );
70+
$size->set_options( [ $this->attr_term_ids['medium'] ] );
71+
$this->products[0] = WC_Helper_Product::create_simple_product( false );
72+
$this->products[0]->set_attributes( [ $color, $size ] );
73+
$this->products[0]->save();
74+
75+
$color->set_options( [ $this->attr_term_ids['blue'] ] );
76+
$size->set_options( [ $this->attr_term_ids['medium'], $this->attr_term_ids['large'] ] );
77+
$this->products[1] = WC_Helper_Product::create_simple_product( false );
78+
$this->products[1]->set_attributes( [ $color, $size ] );
79+
$this->products[1]->save();
80+
81+
$color->set_options( [ $this->attr_term_ids['yellow'] ] );
82+
$this->products[2] = WC_Helper_Product::create_simple_product( false );
83+
$this->products[2]->set_attributes( [ $color ] );
84+
$this->products[2]->save();
85+
}
86+
87+
/**
88+
* Test getting products by a single attribute term.
89+
*
90+
* @since 1.2.0
91+
*/
92+
public function test_get_products() {
93+
wp_set_current_user( $this->user );
94+
$request = new WP_REST_Request( 'GET', $this->endpoint . '/products' );
95+
$request->set_param( 'attribute', 'pa_color' );
96+
$request->set_param( 'attribute_term', (string) $this->attr_term_ids['red'] );
97+
98+
$response = $this->server->dispatch( $request );
99+
$response_products = $response->get_data();
100+
$this->assertEquals( 200, $response->get_status() );
101+
$this->assertEquals( 1, count( $response_products ) );
102+
}
103+
104+
/**
105+
* Test getting products by multiple terms in one attribute.
106+
*
107+
* @since 1.2.0
108+
*/
109+
public function test_get_products_by_multiple_terms() {
110+
wp_set_current_user( $this->user );
111+
$request = new WP_REST_Request( 'GET', $this->endpoint . '/products' );
112+
$request->set_param( 'attribute', 'pa_color' );
113+
$request->set_param(
114+
'attribute_term',
115+
// Terms list needs to be a string.
116+
$this->attr_term_ids['red'] . ',' . $this->attr_term_ids['yellow']
117+
);
118+
119+
$response = $this->server->dispatch( $request );
120+
$response_products = $response->get_data();
121+
$this->assertEquals( 200, $response->get_status() );
122+
$this->assertEquals( 2, count( $response_products ) );
123+
}
124+
125+
/**
126+
* Test getting products by multiple terms in one attribute, matching all.
127+
*
128+
* @since 1.2.0
129+
*/
130+
public function test_get_products_by_multiple_terms_all() {
131+
wp_set_current_user( $this->user );
132+
$request = new WP_REST_Request( 'GET', $this->endpoint . '/products' );
133+
$request->set_param(
134+
'attributes',
135+
array(
136+
'pa_color' => array(
137+
$this->attr_term_ids['red'],
138+
$this->attr_term_ids['blue'],
139+
),
140+
)
141+
);
142+
$request->set_param( 'attr_operator', 'AND' );
143+
144+
$response = $this->server->dispatch( $request );
145+
$response_products = $response->get_data();
146+
$this->assertEquals( 200, $response->get_status() );
147+
$this->assertEquals( 1, count( $response_products ) );
148+
}
149+
150+
/**
151+
* Test getting products by multiple terms in multiple attributes, matching any.
152+
*
153+
* @since 1.2.0
154+
*/
155+
public function test_get_products_by_multiple_terms_multiple_attrs_any() {
156+
wp_set_current_user( $this->user );
157+
$request = new WP_REST_Request( 'GET', $this->endpoint . '/products' );
158+
$request->set_param(
159+
'attributes',
160+
array(
161+
'pa_color' => array( $this->attr_term_ids['red'] ),
162+
'pa_size' => array( $this->attr_term_ids['large'] ),
163+
)
164+
);
165+
$request->set_param( 'attr_operator', 'IN' );
166+
167+
$response = $this->server->dispatch( $request );
168+
$response_products = $response->get_data();
169+
$this->assertEquals( 200, $response->get_status() );
170+
$this->assertEquals( 2, count( $response_products ) );
171+
}
172+
173+
/**
174+
* Test getting products by multiple terms in multiple attributes, matching all.
175+
*
176+
* @since 1.2.0
177+
*/
178+
public function test_get_products_by_multiple_terms_multiple_attrs_all() {
179+
wp_set_current_user( $this->user );
180+
$request = new WP_REST_Request( 'GET', $this->endpoint . '/products' );
181+
$request->set_param(
182+
'attributes',
183+
array(
184+
'pa_color' => array( $this->attr_term_ids['blue'] ),
185+
'pa_size' => array( $this->attr_term_ids['medium'] ),
186+
)
187+
);
188+
$request->set_param( 'attr_operator', 'AND' );
189+
190+
$response = $this->server->dispatch( $request );
191+
$response_products = $response->get_data();
192+
$this->assertEquals( 200, $response->get_status() );
193+
$this->assertEquals( 2, count( $response_products ) );
194+
}
195+
196+
/**
197+
* Test getting products by attributes that don't exist.
198+
*
199+
* Note: This test is currently skipped because the API isn't registering the attribute
200+
* properties correctly, and therefor not validating attribute names against "real" attributes.
201+
*
202+
* @since 1.2.0
203+
*/
204+
public function xtest_get_products_by_fake_attrs() {
205+
wp_set_current_user( $this->user );
206+
$request = new WP_REST_Request( 'GET', $this->endpoint . '/products' );
207+
$request->set_param( 'attributes', array( 'pa_fake' => array( 21 ) ) );
208+
$response = $this->server->dispatch( $request );
209+
$this->assertEquals( 400, $response->get_status() );
210+
}
211+
}

0 commit comments

Comments
 (0)