Merge pull request #32046 from woocommerce/api/28508-search-including-sku

Enable searching for products by SKU
This commit is contained in:
Josh Betz 2022-04-28 13:50:42 -05:00 committed by GitHub
commit 870ea59738
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 263 additions and 15 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Adds a `search_sku` parameter to the v3 products endpoint. Allows for partial match search of the product SKU field.

View File

@ -25,6 +25,15 @@ class WC_REST_Products_Controller extends WC_REST_Products_V2_Controller {
*/ */
protected $namespace = 'wc/v3'; protected $namespace = 'wc/v3';
/**
* A string to inject into a query to do a partial match SKU search.
*
* See prepare_objects_query()
*
* @var string
*/
private $search_sku_in_product_lookup_table = '';
/** /**
* Get the images for a product or product variation. * Get the images for a product or product variation.
* *
@ -144,6 +153,15 @@ class WC_REST_Products_Controller extends WC_REST_Products_V2_Controller {
); );
} }
if ( wc_product_sku_enabled() ) {
// Do a partial match for a sku. Supercedes sku parameter that does exact matching.
if ( ! empty( $request['search_sku'] ) ) {
// Store this for use in the query clause filters.
$this->search_sku_in_product_lookup_table = $request['search_sku'];
unset( $request['sku'] );
}
// Filter by sku. // Filter by sku.
if ( ! empty( $request['sku'] ) ) { if ( ! empty( $request['sku'] ) ) {
$skus = explode( ',', $request['sku'] ); $skus = explode( ',', $request['sku'] );
@ -161,6 +179,7 @@ class WC_REST_Products_Controller extends WC_REST_Products_V2_Controller {
) )
); );
} }
}
// Filter by tax class. // Filter by tax class.
if ( ! empty( $request['tax_class'] ) ) { if ( ! empty( $request['tax_class'] ) ) {
@ -201,7 +220,7 @@ class WC_REST_Products_Controller extends WC_REST_Products_V2_Controller {
} }
// Force the post_type argument, since it's not a user input variable. // Force the post_type argument, since it's not a user input variable.
if ( ! empty( $request['sku'] ) ) { if ( ! empty( $request['sku'] ) || ! empty( $request['search_sku'] ) ) {
$args['post_type'] = array( 'product', 'product_variation' ); $args['post_type'] = array( 'product', 'product_variation' );
} else { } else {
$args['post_type'] = $this->post_type; $args['post_type'] = $this->post_type;
@ -217,6 +236,61 @@ class WC_REST_Products_Controller extends WC_REST_Products_V2_Controller {
return $args; return $args;
} }
/**
* Get objects.
*
* @param array $query_args Query args.
* @return array
*/
public function get_objects( $query_args ) {
// Add filters for search criteria in product postmeta via the lookup table.
if ( ! empty( $this->search_sku_in_product_lookup_table ) ) {
add_filter( 'posts_join', array( $this, 'add_search_criteria_to_wp_query_join' ) );
add_filter( 'posts_where', array( $this, 'add_search_criteria_to_wp_query_where' ) );
}
$result = parent::get_objects( $query_args );
// Remove filters for search criteria in product postmeta via the lookup table.
if ( ! empty( $this->search_sku_in_product_lookup_table ) ) {
remove_filter( 'posts_join', array( $this, 'add_search_criteria_to_wp_query_join' ) );
remove_filter( 'posts_where', array( $this, 'add_search_criteria_to_wp_query_where' ) );
$this->search_sku_in_product_lookup_table = '';
}
return $result;
}
/**
* Join `wc_product_meta_lookup` table when SKU search query is present.
*
* @param string $join Join clause used to search posts.
* @return string
*/
public function add_search_criteria_to_wp_query_join( $join ) {
global $wpdb;
if ( ! empty( $this->search_sku_in_product_lookup_table ) && ! strstr( $join, 'wc_product_meta_lookup' ) ) {
$join .= " LEFT JOIN $wpdb->wc_product_meta_lookup wc_product_meta_lookup
ON $wpdb->posts.ID = wc_product_meta_lookup.product_id ";
}
return $join;
}
/**
* Add a where clause for matching the SKU field.
*
* @param string $where Where clause used to search posts.
* @return string
*/
public function add_search_criteria_to_wp_query_where( $where ) {
global $wpdb;
if ( ! empty( $this->search_sku_in_product_lookup_table ) ) {
$like_search = '%' . $wpdb->esc_like( $this->search_sku_in_product_lookup_table ) . '%';
$where .= ' AND ' . $wpdb->prepare( '(wc_product_meta_lookup.sku LIKE %s)', $like_search );
}
return $where;
}
/** /**
* Set product images. * Set product images.
* *
@ -1368,6 +1442,13 @@ class WC_REST_Products_Controller extends WC_REST_Products_V2_Controller {
'validate_callback' => 'rest_validate_request_arg', 'validate_callback' => 'rest_validate_request_arg',
); );
$params['search_sku'] = array(
'description' => __( 'Limit results to those with a SKU that partial matches a string.', 'woocommerce' ),
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => 'rest_validate_request_arg',
);
return $params; return $params;
} }

View File

@ -5,6 +5,57 @@
* Product Controller tests for V3 REST API. * Product Controller tests for V3 REST API.
*/ */
class WC_REST_Products_Controller_Tests extends WC_REST_Unit_Test_Case { class WC_REST_Products_Controller_Tests extends WC_REST_Unit_Test_Case {
/**
* @var WC_Product_Simple[]
*/
protected static $products = array();
/**
* Create products for tests.
*
* @return void
*/
public static function wpSetUpBeforeClass() {
self::$products[] = WC_Helper_Product::create_simple_product(
true,
array(
'name' => 'Pancake',
'sku' => 'pancake-1',
)
);
self::$products[] = WC_Helper_Product::create_simple_product(
true,
array(
'name' => 'Waffle 1',
'sku' => 'pancake-2',
)
);
self::$products[] = WC_Helper_Product::create_simple_product(
true,
array(
'name' => 'French Toast',
'sku' => 'waffle-2',
)
);
self::$products[] = WC_Helper_Product::create_simple_product(
true,
array(
'name' => 'Waffle 3',
'sku' => 'waffle-3',
)
);
}
/**
* Clean up products after tests.
*
* @return void
*/
public static function wpTearDownAfterClass() {
foreach ( self::$products as $product ) {
WC_Helper_Product::delete_product( $product->get_id() );
}
}
/** /**
* Setup our test server, endpoints, and user info. * Setup our test server, endpoints, and user info.
@ -147,4 +198,116 @@ class WC_REST_Products_Controller_Tests extends WC_REST_Unit_Test_Case {
$this->assertContains( $field, $response_fields, "Field $field was expected but not present in product API response." ); $this->assertContains( $field, $response_fields, "Field $field was expected but not present in product API response." );
} }
} }
/**
* Test that the `search` parameter does partial matching in the product name, but not the SKU.
*
* @return void
*/
public function test_products_search_with_search_param_only() {
wp_set_current_user( $this->user );
$request = new WP_REST_Request( 'GET', '/wc/v3/products' );
$request->set_query_params(
array(
'search' => 'waffle',
'order' => 'asc',
'orderby' => 'id',
)
);
$response = $this->server->dispatch( $request );
$this->assertEquals( 200, $response->get_status() );
$response_products = $response->get_data();
$this->assertEquals( 2, count( $response_products ) );
$this->assertEquals( $response_products[0]['name'], 'Waffle 1' );
$this->assertEquals( $response_products[0]['sku'], 'pancake-2' );
$this->assertEquals( $response_products[1]['name'], 'Waffle 3' );
$this->assertEquals( $response_products[1]['sku'], 'waffle-3' );
}
/**
* Test that the `search_sku` parameter does partial matching in the product SKU, but not the name.
*
* @return void
*/
public function test_products_search_with_search_sku_param_only() {
wp_set_current_user( $this->user );
$request = new WP_REST_Request( 'GET', '/wc/v3/products' );
$request->set_query_params(
array(
'search_sku' => 'waffle',
'order' => 'asc',
'orderby' => 'id',
)
);
$response = $this->server->dispatch( $request );
$this->assertEquals( 200, $response->get_status() );
$response_products = $response->get_data();
$this->assertEquals( 2, count( $response_products ) );
$this->assertEquals( $response_products[0]['name'], 'French Toast' );
$this->assertEquals( $response_products[0]['sku'], 'waffle-2' );
$this->assertEquals( $response_products[1]['name'], 'Waffle 3' );
$this->assertEquals( $response_products[1]['sku'], 'waffle-3' );
}
/**
* Test that using the `search` and `search_sku` parameters together only matches when both match.
*
* @return void
*/
public function test_products_search_with_search_and_search_sku_param() {
wp_set_current_user( $this->user );
$request = new WP_REST_Request( 'GET', '/wc/v3/products' );
$request->set_query_params(
array(
'search' => 'waffle',
'search_sku' => 'waffle',
'order' => 'asc',
'orderby' => 'id',
)
);
$response = $this->server->dispatch( $request );
$this->assertEquals( 200, $response->get_status() );
$response_products = $response->get_data();
$this->assertEquals( 1, count( $response_products ) );
$this->assertEquals( $response_products[0]['name'], 'Waffle 3' );
$this->assertEquals( $response_products[0]['sku'], 'waffle-3' );
}
/**
* Test that the `search_sku` parameter does nothing when product SKUs are disabled.
*
* @return void
*/
public function test_products_search_with_search_sku_when_skus_disabled() {
wp_set_current_user( $this->user );
add_filter( 'wc_product_sku_enabled', '__return_false' );
$request = new WP_REST_Request( 'GET', '/wc/v3/products' );
$request->set_query_params(
array(
'search' => 'waffle',
'search_sku' => 'waffle',
'order' => 'asc',
'orderby' => 'id',
)
);
$response = $this->server->dispatch( $request );
$this->assertEquals( 200, $response->get_status() );
$response_products = $response->get_data();
$this->assertEquals( 2, count( $response_products ) );
$this->assertEquals( $response_products[0]['name'], 'Waffle 1' );
$this->assertEquals( $response_products[0]['sku'], 'pancake-2' );
$this->assertEquals( $response_products[1]['name'], 'Waffle 3' );
$this->assertEquals( $response_products[1]['sku'], 'waffle-3' );
remove_filter( 'wc_product_sku_enabled', '__return_false' );
}
} }