REST API - Store API - Product filtering data, products endpoint, and cart refinements (https://github.com/woocommerce/woocommerce-blocks/pull/1055)

* Cart add endpoint and schema

* Empty card DELETE method

* Merge into single controller

* Revise verb usage

* PUT/update requests

* Move under rest api namespace

* Basic test coverage

* Invalid tests with status check

* Variation handling

* Update src/RestApi/StoreApi/Schemas/CartItemSchema.php

Co-Authored-By: Albert Juhé Lluveras <contact@albertjuhe.com>

* Remove key arg for delete endpoint

* code comment for creation

* rename param to product_id

* Renaming methods from _item to _cart_item

* Prepare storeAPI products endpoint for public use

* Price filter headers

* Attribute counts

* Add Rating filter

* Rating counts

* Fix counts

* Moved utilities

* API docs

* Use correct response for cart item

* Attributes filtering

* Stats

* Products/Stats unit tests

* Rename stats to collection data

* Remove `embed` from schema

* Add since $VID:$ tags

* Improve operator logic and add isset checks

* Force cart schema to be readonly
This commit is contained in:
Mike Jolley 2019-10-25 10:43:52 +01:00 committed by GitHub
parent a6c150a11c
commit 9d79403db2
18 changed files with 2321 additions and 43 deletions

View File

@ -71,6 +71,8 @@ class RestApi {
'product-reviews' => __NAMESPACE__ . '\RestApi\Controllers\ProductReviews',
'store-cart' => __NAMESPACE__ . '\RestApi\StoreApi\Controllers\Cart',
'store-cart-items' => __NAMESPACE__ . '\RestApi\StoreApi\Controllers\CartItems',
'store-products' => __NAMESPACE__ . '\RestApi\StoreApi\Controllers\Products',
'store-product-collection-data' => __NAMESPACE__ . '\RestApi\StoreApi\Controllers\ProductCollectionData',
];
}
}

View File

@ -18,6 +18,8 @@ use Automattic\WooCommerce\Blocks\RestApi\StoreApi\Utilities\CartController;
/**
* Cart API.
*
* @since $VID:$
*/
class Cart extends RestContoller {
/**
@ -37,15 +39,15 @@ class Cart extends RestContoller {
/**
* Schema class instance.
*
* @var array
* @var object
*/
protected $cart_schema;
protected $schema;
/**
* Setup API class.
*/
public function __construct() {
$this->cart_schema = new CartSchema();
$this->schema = new CartSchema();
}
/**
@ -94,7 +96,7 @@ class Cart extends RestContoller {
* @return array
*/
public function get_item_schema() {
return $this->cart_schema->get_item_schema();
return $this->schema->get_item_schema();
}
/**
@ -105,7 +107,7 @@ class Cart extends RestContoller {
* @return \WP_REST_Response Response object.
*/
public function prepare_item_for_response( $cart, $request ) {
$data = $this->cart_schema->get_item_response( $cart );
$data = $this->schema->get_item_response( $cart );
return rest_ensure_response( $data );
}

View File

@ -19,6 +19,8 @@ use Automattic\WooCommerce\Blocks\RestApi\StoreApi\Utilities\CartController;
/**
* Cart API.
*
* @since $VID:$
*/
class CartItems extends RestContoller {
/**
@ -36,17 +38,17 @@ class CartItems extends RestContoller {
protected $rest_base = 'cart/items';
/**
* Schema class instances.
* Schema class instance.
*
* @var array
* @var object
*/
protected $cart_item_schema;
protected $schema;
/**
* Setup API class.
*/
public function __construct() {
$this->cart_item_schema = new CartItemSchema();
$this->schema = new CartItemSchema();
}
/**
@ -173,7 +175,9 @@ class CartItems extends RestContoller {
return $result;
}
return rest_ensure_response( $this->prepare_item_for_response( $controller->get_cart_item( $result ), $request ) );
$response = rest_ensure_response( $this->prepare_item_for_response( $controller->get_cart_item( $result ), $request ) );
$response->set_status( 201 );
return $response;
}
/**
@ -237,7 +241,7 @@ class CartItems extends RestContoller {
* @return array
*/
public function get_item_schema() {
return $this->cart_item_schema->get_item_schema();
return $this->schema->get_item_schema();
}
/**
@ -248,7 +252,7 @@ class CartItems extends RestContoller {
* @return \WP_REST_Response Response object.
*/
public function prepare_item_for_response( $cart_item, $request ) {
$data = $this->cart_item_schema->get_item_response( $cart_item );
$data = $this->schema->get_item_response( $cart_item );
return rest_ensure_response( $data );
}

View File

@ -0,0 +1,210 @@
<?php
/**
* Products collection data controller. Get's aggregate data from a collection of products.
*
* Supports the same parameters as /products, but returns a different response.
*
* @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions.
* @package WooCommerce/Blocks
*/
namespace Automattic\WooCommerce\Blocks\RestApi\StoreApi\Controllers;
defined( 'ABSPATH' ) || exit;
use \WP_REST_Controller as RestContoller;
use \WP_REST_Server as RestServer;
use Automattic\WooCommerce\Blocks\RestApi\StoreApi\Utilities\ProductQueryFilters;
/**
* ProductCollectionData API.
*
* @since $VID:$
*/
class ProductCollectionData extends RestContoller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/store';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'products/collection-data';
/**
* Register the routes for products.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => RestServer::READABLE,
'callback' => array( $this, 'get_items' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Return the schema.
*
* @return array
*/
public function get_item_schema() {
return [
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'product_collection_data',
'type' => 'object',
'properties' => [
'min_price' => array(
'description' => __( 'Min price found in collection of products.', 'woo-gutenberg-products-block' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'max_price' => array(
'description' => __( 'Max price found in collection of products.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'attribute_counts' => array(
'description' => __( 'Returns number of products within attribute terms, indexed by term ID.', 'woo-gutenberg-products-block' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'items' => array(
'type' => 'object',
'properties' => array(
'term' => array(
'description' => __( 'Term ID', 'woo-gutenberg-products-block' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'count' => array(
'description' => __( 'Number of products.', 'woo-gutenberg-products-block' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
),
),
'rating_counts' => array(
'description' => __( 'Returns number of products with each average rating.', 'woo-gutenberg-products-block' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'items' => array(
'type' => 'object',
'properties' => array(
'rating' => array(
'description' => __( 'Average rating', 'woo-gutenberg-products-block' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'count' => array(
'description' => __( 'Number of products.', 'woo-gutenberg-products-block' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
),
),
],
];
}
/**
* Get a collection of posts and add the post title filter option to \WP_Query.
*
* @param \WP_REST_Request $request Full details about the request.
* @return RestError|\WP_REST_Response
*/
public function get_items( $request ) {
$return = [
'min_price' => null,
'max_price' => null,
'attribute_counts' => null,
'rating_counts' => null,
];
$filters = new ProductQueryFilters();
if ( ! empty( $request['calculate_price_range'] ) ) {
$price_results = $filters->get_filtered_price( $request );
$return['min_price'] = $price_results->min_price;
$return['max_price'] = $price_results->max_price;
}
if ( ! empty( $request['calculate_attribute_counts'] ) ) {
$return['attribute_counts'] = [];
$counts = $filters->get_attribute_counts( $request, $request['calculate_attribute_counts'] );
foreach ( $counts as $key => $value ) {
$return['attribute_counts'][] = [
'term' => $key,
'count' => $value,
];
}
}
if ( ! empty( $request['calculate_rating_counts'] ) ) {
$return['rating_counts'] = [];
$counts = $filters->get_rating_counts( $request );
foreach ( $counts as $key => $value ) {
$return['rating_counts'][] = [
'rating' => $key,
'count' => $value,
];
}
}
return rest_ensure_response( $return );
}
/**
* Get the query params for collections of products.
*
* @return array
*/
public function get_collection_params() {
$params = ( new Products() )->get_collection_params();
$params['calculate_price_range'] = array(
'description' => __( 'If true, calculates the minimum and maximum product prices for the collection.', 'woo-gutenberg-products-block' ),
'type' => 'boolean',
'default' => false,
);
$params['calculate_attribute_counts'] = array(
'description' => __( 'If requested, calculates attribute term counts for products in the collection.', 'woo-gutenberg-products-block' ),
'type' => 'array',
'items' => array(
'type' => 'string',
),
'default' => array(),
);
$params['calculate_rating_counts'] = array(
'description' => __( 'If true, calculates rating counts for products in the collection.', 'woo-gutenberg-products-block' ),
'type' => 'boolean',
'default' => false,
);
return $params;
}
}

View File

@ -0,0 +1,475 @@
<?php
/**
* Products controller.
*
* @internal This API is used internally by Blocks--it is still in flux and may be subject to revisions.
* @package WooCommerce/Blocks
*/
namespace Automattic\WooCommerce\Blocks\RestApi\StoreApi\Controllers;
defined( 'ABSPATH' ) || exit;
use \WP_Error as RestError;
use \WP_REST_Server as RestServer;
use \WP_REST_Controller as RestContoller;
use \WC_REST_Exception as RestException;
use Automattic\WooCommerce\Blocks\RestApi\StoreApi\Schemas\ProductSchema;
use Automattic\WooCommerce\Blocks\RestApi\StoreApi\Utilities\Pagination;
use Automattic\WooCommerce\Blocks\RestApi\StoreApi\Utilities\ProductQuery;
/**
* Products API.
*
* @since $VID:$
*/
class Products extends RestContoller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/store';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'products';
/**
* Schema class instance.
*
* @var ProductSchema
*/
protected $schema;
/**
* Query class instance.
*
* @var ProductQuery
*/
protected $product_query;
/**
* Setup API class.
*/
public function __construct() {
$this->schema = new ProductSchema();
$this->product_query = new ProductQuery();
}
/**
* Register the routes for products.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => RestServer::READABLE,
'callback' => [ $this, 'get_items' ],
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<id>[\d]+)',
array(
'args' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woo-gutenberg-products-block' ),
'type' => 'integer',
),
),
array(
'methods' => RestServer::READABLE,
'callback' => array( $this, 'get_item' ),
'args' => array(
'context' => $this->get_context_param(
array(
'default' => 'view',
)
),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Product item schema.
*
* @return array
*/
public function get_item_schema() {
return $this->schema->get_item_schema();
}
/**
* Prepare a single item for response.
*
* @param \WC_Product $item Product object.
* @param \WP_REST_Request $request Request object.
* @return \WP_REST_Response $response Response data.
*/
public function prepare_item_for_response( $item, $request ) {
return rest_ensure_response( $this->schema->get_item_response( $item ) );
}
/**
* Get a single item.
*
* @param \WP_REST_Request $request Full details about the request.
* @return RestError|\WP_REST_Response
*/
public function get_item( $request ) {
$object = wc_get_product( (int) $request['id'] );
if ( ! $object || 0 === $object->get_id() ) {
return new RestError( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woo-gutenberg-products-block' ), array( 'status' => 404 ) );
}
$data = $this->prepare_item_for_response( $object, $request );
$response = rest_ensure_response( $data );
return $response;
}
/**
* Get a collection of posts and add the post title filter option to \WP_Query.
*
* @param \WP_REST_Request $request Full details about the request.
* @return RestError|\WP_REST_Response
*/
public function get_items( $request ) {
$query_results = $this->product_query->get_objects( $request );
$objects = array();
foreach ( $query_results['objects'] as $object ) {
$data = $this->prepare_item_for_response( $object, $request );
$objects[] = $this->prepare_response_for_collection( $data );
}
$total = $query_results['total'];
$max_pages = $query_results['pages'];
$response = rest_ensure_response( $objects );
$response = ( new Pagination() )->add_headers( $response, $request, $total, $max_pages );
return $response;
}
/**
* Prepare links for the request.
*
* @param \WC_Product $item Product object.
* @param \WP_REST_Request $request Request object.
* @return array
*/
protected function prepare_links( $item, $request ) {
$links = array(
'self' => array(
'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $item->get_id() ) ), // @codingStandardsIgnoreLine.
),
'collection' => array(
'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), // @codingStandardsIgnoreLine.
),
);
if ( $item->get_parent_id() ) {
$links['up'] = array(
'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $item->get_parent_id() ) ), // @codingStandardsIgnoreLine.
);
}
return $links;
}
/**
* Get the query params for collections of products.
*
* @return array
*/
public function get_collection_params() {
$params = array();
$params['context'] = $this->get_context_param();
$params['context']['default'] = 'view';
$params['page'] = array(
'description' => __( 'Current page of the collection.', 'woo-gutenberg-products-block' ),
'type' => 'integer',
'default' => 1,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
'minimum' => 1,
);
$params['per_page'] = array(
'description' => __( 'Maximum number of items to be returned in result set.', 'woo-gutenberg-products-block' ),
'type' => 'integer',
'default' => 10,
'minimum' => 1,
'maximum' => 100,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['search'] = array(
'description' => __( 'Limit results to those matching a string.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => 'rest_validate_request_arg',
);
$params['after'] = array(
'description' => __( 'Limit response to resources created after a given ISO8601 compliant date.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['before'] = array(
'description' => __( 'Limit response to resources created before a given ISO8601 compliant date.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['date_column'] = array(
'description' => __( 'When limiting response using after/before, which date column to compare against.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'default' => 'date',
'enum' => array(
'date',
'date_gmt',
'modified',
'modified_gmt',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['exclude'] = array(
'description' => __( 'Ensure result set excludes specific IDs.', 'woo-gutenberg-products-block' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
'sanitize_callback' => 'wp_parse_id_list',
);
$params['include'] = array(
'description' => __( 'Limit result set to specific ids.', 'woo-gutenberg-products-block' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
'sanitize_callback' => 'wp_parse_id_list',
);
$params['offset'] = array(
'description' => __( 'Offset the result set by a specific number of items.', 'woo-gutenberg-products-block' ),
'type' => 'integer',
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['order'] = array(
'description' => __( 'Order sort attribute ascending or descending.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'default' => 'desc',
'enum' => array( 'asc', 'desc' ),
'validate_callback' => 'rest_validate_request_arg',
);
$params['orderby'] = array(
'description' => __( 'Sort collection by object attribute.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'default' => 'date',
'enum' => array(
'date',
'modified',
'id',
'include',
'title',
'slug',
'price',
'popularity',
'rating',
'menu_order',
'comment_count',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['parent'] = array(
'description' => __( 'Limit result set to those of particular parent IDs.', 'woo-gutenberg-products-block' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'sanitize_callback' => 'wp_parse_id_list',
'default' => array(),
);
$params['parent_exclude'] = array(
'description' => __( 'Limit result set to all items except those of a particular parent ID.', 'woo-gutenberg-products-block' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'sanitize_callback' => 'wp_parse_id_list',
'default' => array(),
);
$params['type'] = array(
'description' => __( 'Limit result set to products assigned a specific type.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'enum' => array_keys( wc_get_product_types() ),
'sanitize_callback' => 'sanitize_key',
'validate_callback' => 'rest_validate_request_arg',
);
$params['sku'] = array(
'description' => __( 'Limit result set to products with specific SKU(s). Use commas to separate.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => 'rest_validate_request_arg',
);
$params['featured'] = array(
'description' => __( 'Limit result set to featured products.', 'woo-gutenberg-products-block' ),
'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 ID.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
);
$params['tag'] = array(
'description' => __( 'Limit result set to products assigned a specific tag ID.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
);
$params['on_sale'] = array(
'description' => __( 'Limit result set to products on sale.', 'woo-gutenberg-products-block' ),
'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.', 'woo-gutenberg-products-block' ),
'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.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => 'rest_validate_request_arg',
);
$params['stock_status'] = array(
'description' => __( 'Limit result set to products with specified stock status.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'enum' => array_keys( wc_get_product_stock_status_options() ),
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => 'rest_validate_request_arg',
);
$params['category_operator'] = array(
'description' => __( 'Operator to compare product category terms.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'enum' => array( 'in', 'not_in', 'and' ),
'default' => 'in',
'sanitize_callback' => 'sanitize_key',
'validate_callback' => 'rest_validate_request_arg',
);
$params['tag_operator'] = array(
'description' => __( 'Operator to compare product tags.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'enum' => array( 'in', 'not_in', 'and' ),
'default' => 'in',
'sanitize_callback' => 'sanitize_key',
'validate_callback' => 'rest_validate_request_arg',
);
$params['attribute_operator'] = array(
'description' => __( 'Operator to compare product attribute terms.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'enum' => array( 'in', 'not_in', 'and' ),
'default' => 'in',
'sanitize_callback' => 'sanitize_key',
'validate_callback' => 'rest_validate_request_arg',
);
$params['attributes'] = array(
'description' => __( 'Limit result set to products with selected global attributes.', 'woo-gutenberg-products-block' ),
'type' => 'array',
'items' => array(
'type' => 'object',
'properties' => array(
'attribute' => array(
'description' => __( 'Attribute taxonomy name.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'sanitize_callback' => 'wc_sanitize_taxonomy_name',
),
'term_id' => array(
'description' => __( 'Attribute term ID.', 'woo-gutenberg-products-block' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list',
),
'slug' => array(
'description' => __( 'Comma separatede list of attribute slug(s). If a term ID is provided, this will be ignored.', 'woo-gutenberg-products-block' ),
'type' => 'string',
),
'operator' => array(
'description' => __( 'Operator to compare product attribute terms.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'enum' => [ 'in', 'not in', 'and' ],
),
),
),
'default' => array(),
);
$params['catalog_visibility'] = array(
'description' => __( 'Determines if hidden or visible catalog products are shown.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'enum' => array( 'any', 'visible', 'catalog', 'search', 'hidden' ),
'sanitize_callback' => 'sanitize_key',
'validate_callback' => 'rest_validate_request_arg',
);
$params['rating'] = array(
'description' => __( 'Limit result set to products with a certain average rating.', 'woo-gutenberg-products-block' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
'enum' => range( 1, 5 ),
),
'default' => array(),
'sanitize_callback' => 'wp_parse_id_list',
);
return $params;
}
}

View File

@ -1,15 +1,533 @@
# WooCommerce Store API
The WooCommerce Store API is a public-facing REST API that makes it possible to add items to cart, update the cart, and retrieve cart data in JSON format.
The WooCommerce Store API is a public-facing REST API; unlike the main WooCommerce REST API, this API does not require authentication. It is intended to be used by client side code to provide functionality to customers.
Unlike the main WooCommerce REST API, this API does not require authentication. It is intended to be used via AJAX and other client side code, such as the add-to-cart functionality in blocks, to provide functionality to customers.
Documentation in this readme file assumes knowledge of REST concepts.
## Endpoints
## Current status
- GET `/wc/store/cart` - Get a representation of the cart, including totals.
- GET `/wc/store/cart/items` - Get cart items.
- GET `/wc/store/cart/items/<key>` - Get a single cart item.
- PUT `/wc/store/cart/items/<key>` - Update a single cart item (quantity only).
- POST `/wc/store/cart/items` - Create a new cart item.
- DELETE `/wc/store/cart/items` - Delete all cart items (clear cart).
- DELETE `/wc/store/cart/items/<key>` - Delete a single cart item.
This API is used internally by Blocks--it is still in flux and may be subject to revisions. There is currently no versioning system and this should be used at your own risk. Eventually, it will be moved to the main WooCommerce REST API at which point it will be versioned and safe to use in other projects.
## Basic usage
Example of a valid API request using cURL:
```http
curl "https://example-store.com/wp-json/wc/store/products"
```
The API uses JSON to serialize data. You dont need to specify `.json` at the end of an API URL.
## Namespace
Resources in the Store API are all found within the `wc/store/` namespace, and since this API extends the WordPress API, accessing it requires the `/wp-json/` base. Examples:
```http
GET /wp-json/wc/store/products
GET /wp-json/wc/store/cart
```
## Authentication
Requests to the store API do not require authentication. Only public data is returned, and most endpoints are read-only, with the exception of the cart API which only lets you manipulate data for the current user.
## Status codes
The following table gives an overview of how the API functions generally behave.
| Request type | Description |
| :----------- | :---------------------------------------------------------------------------------------------------------- |
| `GET` | Access one or more resources and return `200 OK` and the result as JSON. |
| `POST` | Return `201 Created` if the resource is successfully created and return the newly created resource as JSON. |
| `PUT` | Return `200 OK` if the resource is modified successfully. The modified result is returned as JSON. |
| `DELETE` | Returns `204 No Content` if the resource was deleted successfully. |
The following table shows the possible return codes for API requests.
| Response code | Description |
| :----------------------- | :------------------------------------------------------------------------------------------------------------------------------ |
| `200 OK` | The request was successful, the resource(s) itself is returned as JSON. |
| `204 No Content` | The server has successfully fulfilled the request and that there is no additional content to send in the response payload body. |
| `201 Created` | The POST request was successful and the resource is returned as JSON. |
| `400 Bad Request` | A required attribute of the API request is missing. | |
| `403 Forbidden` | The request is not allowed. |
| `404 Not Found` | A resource could not be accessed, for example it doesn't exist. |
| `405 Method Not Allowed` | The request is not supported. |
| `500 Server Error` | While handling the request something went wrong server-side. |
## Pagination
If collections contain many results, they may be paginated. When listing resources you can pass the following parameters:
| Parameter | Description |
| :--------- | :------------------------------------------------------------------------------------- |
| `page` | Current page of the collection. Defaults to `1`. |
| `per_page` | Maximum number of items to be returned in result set. Defaults to `10`. Maximum `100`. |
In the example below, we list 20 products per page and return page 2.
```http
curl "https://example-store.com/wp-json/wc/store/products?page=2&per_page=20"
```
### Pagination headers
Additional pagination headers are also sent back.
| Header | Description |
| :---------------- | :------------------------------------------------------------------------ |
| `X-WP-Total` | The total number of items in the collection. |
| `X-WP-TotalPages` | The total number of pages in the collection. |
| `Link` | Contains links to other pages; `next`, `prev`, and `up` where applicable. |
## API resources
Available resources in the Store API include:
| Resource | Available endpoints |
| :--------------------------------------------------------- | :----------------------------------- |
| [`Product Collection Data`](#products-collection-data-api) | `/wc/store/products/collection-data` |
| [`Products`](#products-api) | `/wc/store/products` |
| [`Cart`](#cart-api) | `/wc/store/cart` |
| [`Cart Items`](#cart-items-api) | `/wc/store/cart/items` |
## Product Collection Data API
This endpoint allows you to get aggregate data from a collection of products, for example, the min and max price in a collection of products (ignoring pagination). This is used by blocks for product filtering widgets, since counts are based on the product catalog being viewed.
```http
GET /products/collection-data
GET /products/collection-data?calculate_price_range=true
GET /products/collection-data?calculate_attribute_counts=pa_size,pa_color
GET /products/collection-data?calculate_rating_counts=true
```
| Attribute | Type | Required | Description |
| :--------------------------- | :----- | :------: | :--------------------------------------------------------------------------------------------------------------------------------------- |
| `calculate_price_range` | bool | No | Returns the min and max price for the product collection. If false, only `null` will be returned. |
| `calculate_attribute_counts` | string | No | Returns attribute counts for a list of attribute (taxonomy) names you pass in via the parameter. If empty, only `null` will be returned. |
| `calculate_rating_counts` | bool | No | Returns the counts of products with a certain average rating, 1-5. If false, only `null` will be returned. |
**In addition to the above attributes**, all product list attributes are supported. This allows you to get data for a certain subset of products. See [the products API list products section](#list-products) for the full list.
```http
curl "https://example-store.com/wp-json/wc/store/products/collection-data?calculate_price_range=true&calculate_attribute_counts=pa_size,pa_color&calculate_rating_counts=true"
```
Example response:
```json
{
"min_price": "0.00",
"max_price": "90.00",
"attribute_counts": [
{
"term": 22,
"count": 4
},
{
"term": 23,
"count": 3
},
{
"term": 24,
"count": 4
}
],
"rating_counts": [
{
"rating": 3,
"count": 1
},
{
"rating": 4,
"count": 1
}
]
}
```
## Products API
### List products
```http
GET /products
GET /products?search=product%20name
GET /products?after=2017-03-22&date_column=date
GET /products?before=2017-03-22&date_column=date
GET /products?exclude=10,44,33
GET /products?include=10,44,33
GET /products?offset=10
GET /products?order=asc&orderby=price
GET /products?parent=10
GET /products?parent_exclude=10
GET /products?type=simple
GET /products?sku=sku-1,sku-2
GET /products?featured=true
GET /products?category=t-shirts
GET /products?tag=special-items
GET /products?attributes[0][attribute]=pa_color&attributes[0][slug]=red
GET /products?on_sale=true
GET /products?min_price=50
GET /products?max_price=100
GET /products?stock_status=outofstock
GET /products?catalog_visibility=search
GET /products?rating=4,5
GET /products?return_price_range=true
GET /products?return_attribute_counts=pa_size,pa_color
GET /products?return_rating_counts=true
```
| Attribute | Type | Required | Description |
| :------------------- | :------ | :------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `search` | integer | no | Limit results to those matching a string. |
| `after` | string | no | Limit response to resources created after a given ISO8601 compliant date. |
| `before` | string | no | Limit response to resources created before a given ISO8601 compliant date. |
| `date_column` | string | no | When limiting response using after/before, which date column to compare against. Allowed values: `date`, `date_gmt`, `modified`, `modified_gmt` |
| `exclude` | array | no | Ensure result set excludes specific IDs. |
| `include` | array | no | Limit result set to specific ids. |
| `offset` | integer | no | Offset the result set by a specific number of items. |
| `order` | string | no | Order sort attribute ascending or descending. Allowed values: `asc`, `desc` |
| `orderby` | string | no | Sort collection by object attribute. Allowed values: `date`, `modified`, `id`, `include`, `title`, `slug`, `price`, `popularity`, `rating`, `menu_order`, `comment_count` |
| `parent` | array | no | Limit result set to those of particular parent IDs. |
| `parent_exclude` | array | no | Limit result set to all items except those of a particular parent ID. |
| `type` | string | no | Limit result set to products assigned a specific type. |
| `sku` | string | no | Limit result set to products with specific SKU(s). Use commas to separate. |
| `featured` | boolean | no | Limit result set to featured products. |
| `category` | string | no | Limit result set to products assigned a specific category ID. |
| `category_operator` | string | no | Operator to compare product category terms. Allowed values: `in`, `not_in`, `and` |
| `tag` | string | no | Limit result set to products assigned a specific tag ID. |
| `tag_operator` | string | no | Operator to compare product tags. Allowed values: `in`, `not_in`, `and` |
| `attributes` | array | no | Limit result set to specific attribute terms. Expects an array of objects containing `attribute` (taxonomy), `term_id` or `slug`, and optional `operator` for comparison. |
| `on_sale` | boolean | no | Limit result set to products on sale. |
| `min_price` | string | no | Limit result set to products based on a minimum price. |
| `max_price` | string | no | Limit result set to products based on a maximum price. |
| `stock_status` | string | no | Limit result set to products with specified stock status. |
| `catalog_visibility` | string | no | Determines if hidden or visible catalog products are shown. Allowed values: `any`, `visible`, `catalog`, `search`, `hidden` |
| `rating` | boolean | no | Limit result set to products with a certain average rating. |
```http
curl "https://example-store.com/wp-json/wc/store/products"
```
Example response:
```json
[
{
"id": 95,
"name": "WordPress Pennant",
"variation": "",
"permalink": "http://local.wordpress.test/product/wordpress-pennant/",
"sku": "wp-pennant",
"description": "<p>This is an external product.</p>\n",
"price": "0",
"price_html": "<span class=\"woocommerce-Price-amount amount\"><span class=\"woocommerce-Price-currencySymbol\">&pound;</span>0.00</span>",
"average_rating": "3.60",
"review_count": 5,
"images": [
{
"id": 60,
"src": "http://local.wordpress.test/wp-content/uploads/2019/07/pennant-1.jpg",
"name": "pennant-1.jpg",
"alt": ""
}
]
}
]
```
### Single product
Get a single product.
```http
GET /products/:id
```
| Attribute | Type | Required | Description |
| :-------- | :------ | :------: | :--------------------------------- |
| `id` | integer | Yes | The ID of the product to retrieve. |
```http
curl "https://example-store.com/wp-json/wc/store/products/95"
```
Example response:
```json
{
"id": 95,
"name": "WordPress Pennant",
"variation": "",
"permalink": "http://local.wordpress.test/product/wordpress-pennant/",
"sku": "wp-pennant",
"description": "<p>This is an external product.</p>\n",
"price": "0",
"price_html": "<span class=\"woocommerce-Price-amount amount\"><span class=\"woocommerce-Price-currencySymbol\">&pound;</span>0.00</span>",
"average_rating": "3.60",
"review_count": 5,
"images": [
{
"id": 60,
"src": "http://local.wordpress.test/wp-content/uploads/2019/07/pennant-1.jpg",
"name": "pennant-1.jpg",
"alt": ""
}
]
}
```
## Cart API
```http
GET /cart
```
There are no parameters required for this endpoint.
```http
curl "https://example-store.com/wp-json/wc/store/cart"
```
Example response:
```json
{
"currency": "GBP",
"item_count": 2,
"items": [
{
"key": "11a7ec12ea1071fdecd917d6da5c87ae",
"id": 88,
"quantity": 2,
"name": "V-Neck T-Shirt",
"sku": "woo-vneck-tee-blue",
"permalink": "http://local.wordpress.test/product/v-neck-t-shirt/?attribute_pa_color=blue",
"images": [
{
"id": 41,
"src": "http://local.wordpress.test/wp-content/uploads/2019/07/vnech-tee-blue-1.jpg",
"name": "vnech-tee-blue-1.jpg",
"alt": ""
}
],
"price": "15.00",
"line_price": "30.00",
"variation": {
"Color": "Blue",
"Size": "Small"
}
}
],
"needs_shipping": true,
"total_price": "30.00",
"total_weight": 0
}
```
## Cart items API
### List cart items
```http
GET /cart/items
```
There are no parameters required for this endpoint.
```http
curl "https://example-store.com/wp-json/wc/store/cart/items"
```
Example response:
```json
[
{
"key": "11a7ec12ea1071fdecd917d6da5c87ae",
"id": 88,
"quantity": 2,
"name": "V-Neck T-Shirt",
"sku": "woo-vneck-tee-blue",
"permalink": "http://local.wordpress.test/product/v-neck-t-shirt/?attribute_pa_color=blue",
"images": [
{
"id": 41,
"src": "http://local.wordpress.test/wp-content/uploads/2019/07/vnech-tee-blue-1.jpg",
"name": "vnech-tee-blue-1.jpg",
"alt": ""
}
],
"price": "15.00",
"line_price": "30.00",
"variation": {
"Color": "Blue",
"Size": "Small"
}
}
]
```
### Single cart item
Get a single cart item.
```http
GET /cart/items/:key
```
| Attribute | Type | Required | Description |
| :-------- | :----- | :------: | :------------------------------------ |
| `key` | string | Yes | The key of the cart item to retrieve. |
```http
curl "https://example-store.com/wp-json/wc/store/cart/items/11a7ec12ea1071fdecd917d6da5c87ae"
```
Example response:
```json
{
"key": "11a7ec12ea1071fdecd917d6da5c87ae",
"id": 88,
"quantity": 2,
"name": "V-Neck T-Shirt",
"sku": "woo-vneck-tee-blue",
"permalink": "http://local.wordpress.test/product/v-neck-t-shirt/?attribute_pa_color=blue",
"images": [
{
"id": 41,
"src": "http://local.wordpress.test/wp-content/uploads/2019/07/vnech-tee-blue-1.jpg",
"name": "vnech-tee-blue-1.jpg",
"alt": ""
}
],
"price": "15.00",
"line_price": "30.00",
"variation": {
"Color": "Blue",
"Size": "Small"
}
}
```
### New cart item
Add an item to the cart.
```http
POST /cart/items/
```
| Attribute | Type | Required | Description |
| :---------- | :------ | :------: | :--------------------------------------------------------------------------------------------------- |
| `id` | integer | Yes | The cart item product or variation ID. |
| `quantity` | integer | Yes | Quantity of this item in the cart. |
| `variation` | array | Yes | Chosen attributes (for variations) containing an array of objects with keys `attribute` and `value`. |
```http
curl --request POST https://example-store.com/wp-json/wc/store/cart/items?id=100&quantity=1
```
Example response:
```json
{
"key": "3ef815416f775098fe977004015c6193",
"id": 100,
"quantity": 1,
"name": "Single",
"sku": "woo-single",
"permalink": "http://local.wordpress.test/product/single/",
"images": [
{
"id": 56,
"src": "http://local.wordpress.test/wp-content/uploads/2019/07/single-1.jpg",
"name": "single-1.jpg",
"alt": ""
}
],
"price": "2.00",
"line_price": "2.00",
"variation": []
}
```
### Edit single cart item
Edit an item in the cart.
```http
PUT /cart/items/:key
```
| Attribute | Type | Required | Description |
| :--------- | :------ | :------: | :--------------------------------- |
| `key` | string | Yes | The key of the cart item to edit. |
| `quantity` | integer | Yes | Quantity of this item in the cart. |
```http
curl --request PUT https://example-store.com/wp-json/wc/store/cart/items/3ef815416f775098fe977004015c6193&quantity=10
```
Example response:
```json
{
"key": "3ef815416f775098fe977004015c6193",
"id": 100,
"quantity": 10,
"name": "Single",
"sku": "woo-single",
"permalink": "http://local.wordpress.test/product/single/",
"images": [
{
"id": 56,
"src": "http://local.wordpress.test/wp-content/uploads/2019/07/single-1.jpg",
"name": "single-1.jpg",
"alt": ""
}
],
"price": "2.00",
"line_price": "20.00",
"variation": []
}
```
### Delete single cart item
Delete/remove an item from the cart.
```http
DELETE /cart/items/:key
```
| Attribute | Type | Required | Description |
| :-------- | :----- | :------: | :-------------------------------- |
| `key` | string | Yes | The key of the cart item to edit. |
```http
curl --request DELETE https://example-store.com/wp-json/wc/store/cart/items/3ef815416f775098fe977004015c6193
```
### Delete all cart items
Delete/remove all items from the cart.
```http
DELETE /cart/items/
```
There are no parameters required for this endpoint.
```http
curl --request DELETE https://example-store.com/wp-json/wc/store/cart/items
```
Example response:
```json
[]
```

View File

@ -13,6 +13,8 @@ defined( 'ABSPATH' ) || exit;
/**
* AbstractBlock class.
*
* @since $VID:$
*/
abstract class AbstractSchema {
/**
@ -42,4 +44,23 @@ abstract class AbstractSchema {
* @return array
*/
abstract protected function get_properties();
/**
* Force all schema properties to be readonly.
*
* @param array $properties Schema.
* @return array Updated schema.
*/
protected function force_schema_readonly( $properties ) {
return array_map(
function( $property ) {
$property['readonly'] = true;
if ( isset( $property['items']['properties'] ) ) {
$property['items']['properties'] = $this->force_schema_readonly( $property['items']['properties'] );
}
return $property;
},
$properties
);
}
}

View File

@ -15,6 +15,8 @@ use Automattic\WooCommerce\Blocks\RestApi\Utilities\ProductImages;
/**
* AbstractBlock class.
*
* @since $VID:$
*/
class CartItemSchema extends AbstractSchema {
/**

View File

@ -11,6 +11,8 @@ defined( 'ABSPATH' ) || exit;
/**
* CartSchema class.
*
* @since $VID:$
*/
class CartSchema extends AbstractSchema {
/**
@ -48,7 +50,7 @@ class CartSchema extends AbstractSchema {
'readonly' => true,
'items' => array(
'type' => 'object',
'properties' => ( new CartItemSchema() )->get_properties(),
'properties' => $this->force_schema_readonly( ( new CartItemSchema() )->get_properties() ),
),
),
'needs_shipping' => array(

View File

@ -0,0 +1,146 @@
<?php
/**
* Product Schema.
*
* @package WooCommerce/Blocks
*/
namespace Automattic\WooCommerce\Blocks\RestApi\StoreApi\Schemas;
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Blocks\RestApi\Utilities\ProductImages;
/**
* ProductSchema class.
*
* @since $VID:$
*/
class ProductSchema extends AbstractSchema {
/**
* The schema item name.
*
* @var string
*/
protected $title = 'product';
/**
* Cart schema properties.
*
* @return array
*/
protected function get_properties() {
return [
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woo-gutenberg-products-block' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'Product name.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'variation' => array(
'description' => __( 'Product variation attributes, if applicable.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'permalink' => array(
'description' => __( 'Product URL.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'description' => array(
'description' => __( 'Short description or excerpt from description.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'sku' => array(
'description' => __( 'Unique identifier.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'price' => array(
'description' => __( 'Current product price.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'price_html' => array(
'description' => __( 'Price formatted in HTML.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'average_rating' => array(
'description' => __( 'Reviews average rating.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'review_count' => array(
'description' => __( 'Amount of reviews that the product has.', 'woo-gutenberg-products-block' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'images' => array(
'description' => __( 'List of images.', 'woo-gutenberg-products-block' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Image ID.', 'woo-gutenberg-products-block' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'src' => array(
'description' => __( 'Image URL.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view', 'edit' ),
),
'name' => array(
'description' => __( 'Image name.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'alt' => array(
'description' => __( 'Image alternative text.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
),
),
),
];
}
/**
* Convert a WooCommerce product into an object suitable for the response.
*
* @param array $product Product object.
* @return array
*/
public function get_item_response( $product ) {
return [
'id' => $product->get_id(),
'name' => $product->get_title(),
'variation' => $product->is_type( 'variation' ) ? wc_get_formatted_variation( $product, true, true, false ) : '',
'permalink' => $product->get_permalink(),
'sku' => $product->get_sku(),
'description' => apply_filters( 'woocommerce_short_description', $product->get_short_description() ? $product->get_short_description() : wc_trim_string( $product->get_description(), 400 ) ),
'price' => $product->get_price(),
'price_html' => $product->get_price_html(),
'average_rating' => $product->get_average_rating(),
'review_count' => $product->get_review_count(),
'images' => ( new ProductImages() )->images_to_array( $product ),
];
}
}

View File

@ -16,6 +16,8 @@ use \WC_REST_Exception as RestException;
/**
* Woo Cart Controller class.
*
* @since $VID:$
*/
class CartController {

View File

@ -0,0 +1,81 @@
<?php
/**
* Pagination helper.
*
* @package Automattic/WooCommerce/RestApi
*/
namespace Automattic\WooCommerce\Blocks\RestApi\StoreApi\Utilities;
defined( 'ABSPATH' ) || exit;
/**
* Pagination class.
*
* @since $VID:$
*/
class Pagination {
/**
* Add pagination headers to a response object.
*
* @param \WP_REST_Response $response Reference to the response object.
* @param \WP_REST_Request $request The request object.
* @param int $total_items Total items found.
* @param int $total_pages Total pages found.
* @return \WP_REST_Response
*/
public function add_headers( $response, $request, $total_items, $total_pages ) {
$response->header( 'X-WP-Total', $total_items );
$response->header( 'X-WP-TotalPages', $total_pages );
$current_page = $this->get_current_page( $request );
$link_base = $this->get_link_base( $request );
if ( $current_page > 1 ) {
$previous_page = $current_page - 1;
if ( $previous_page > $total_pages ) {
$previous_page = $total_pages;
}
$this->add_page_link( $response, 'prev', $previous_page, $link_base );
}
if ( $total_pages > $current_page ) {
$this->add_page_link( $response, 'next', ( $current_page + 1 ), $link_base );
}
return $response;
}
/**
* Get current page.
*
* @param \WP_REST_Request $request The request object.
* @return int Get the page from the request object.
*/
protected function get_current_page( $request ) {
return (int) $request->get_param( 'page' );
}
/**
* Get base for links from the request object.
*
* @param \WP_REST_Request $request The request object.
* @return string
*/
protected function get_link_base( $request ) {
return add_query_arg( $request->get_query_params(), rest_url( $request->get_route() ) );
}
/**
* Add a page link.
*
* @param \WP_REST_Response $response Reference to the response object.
* @param string $name Page link name. e.g. prev.
* @param int $page Page number.
* @param string $link_base Base URL.
*/
protected function add_page_link( &$response, $name, $page, $link_base ) {
$response->link_header( $name, add_query_arg( 'page', $page, $link_base ) );
}
}

View File

@ -0,0 +1,142 @@
<?php
/**
* Product Query fllters helper.
*
* @package Automattic/WooCommerce/RestApi
*/
namespace Automattic\WooCommerce\Blocks\RestApi\StoreApi\Utilities;
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Blocks\RestApi\StoreApi\Utilities\ProductQuery;
/**
* Product Query fllters class.
*
* @since $VID:$
*/
class ProductQueryFilters {
/**
* Get filtered min price for current products.
*
* @param \WP_REST_Request $request The request object.
* @return array
*/
public function get_filtered_price( $request ) {
global $wpdb;
// Regenerate the products query without min/max price request params.
unset( $request['min_price'], $request['max_price'] );
// Grab the request from the WP Query object, and remove SQL_CALC_FOUND_ROWS and Limits so we get a list of all products.
$product_query = new ProductQuery();
add_filter( 'posts_clauses', array( $product_query, 'add_query_clauses' ), 10, 2 );
add_filter( 'posts_pre_query', '__return_empty_array' );
$query_args = $product_query->prepare_objects_query( $request );
$query_args['no_found_rows'] = true;
$query_args['posts_per_page'] = -1;
$query = new \WP_Query();
$result = $query->query( $query_args );
$product_query_sql = $query->request;
remove_filter( 'posts_clauses', array( $product_query, 'add_query_clauses' ), 10 );
remove_filter( 'posts_pre_query', '__return_empty_array' );
$price_filter_sql = "
SELECT min( min_price ) as min_price, MAX( max_price ) as max_price
FROM {$wpdb->wc_product_meta_lookup}
WHERE product_id IN ( {$product_query_sql} )
";
return $wpdb->get_row( $price_filter_sql ); // phpcs:ignore
}
/**
* Get attribute counts for the current products.
*
* @param \WP_REST_Request $request The request object.
* @param array $attribute_names Attributes to count.
* @return array termId=>count pairs.
*/
public function get_attribute_counts( $request, $attribute_names = [] ) {
global $wpdb;
// Grab the request from the WP Query object, and remove SQL_CALC_FOUND_ROWS and Limits so we get a list of all products.
$product_query = new ProductQuery();
add_filter( 'posts_clauses', array( $product_query, 'add_query_clauses' ), 10, 2 );
add_filter( 'posts_pre_query', '__return_empty_array' );
$query_args = $product_query->prepare_objects_query( $request );
$query_args['no_found_rows'] = true;
$query_args['posts_per_page'] = -1;
$query = new \WP_Query();
$result = $query->query( $query_args );
$product_query_sql = $query->request;
remove_filter( 'posts_clauses', array( $product_query, 'add_query_clauses' ), 10 );
remove_filter( 'posts_pre_query', '__return_empty_array' );
$attributes_to_count = array_map( 'wc_sanitize_taxonomy_name', $attribute_names );
$attributes_to_count_sql = 'AND term_taxonomy.taxonomy IN ("' . implode( '","', $attributes_to_count ) . '")';
$attribute_count_sql = "
SELECT COUNT( DISTINCT posts.ID ) as term_count, terms.term_id as term_count_id
FROM {$wpdb->posts} AS posts
INNER JOIN {$wpdb->term_relationships} AS term_relationships ON posts.ID = term_relationships.object_id
INNER JOIN {$wpdb->term_taxonomy} AS term_taxonomy USING( term_taxonomy_id )
INNER JOIN {$wpdb->terms} AS terms USING( term_id )
WHERE posts.ID IN ( {$product_query_sql} )
{$attributes_to_count_sql}
GROUP BY terms.term_id
";
$results = $wpdb->get_results( $attribute_count_sql ); // phpcs:ignore
return array_map( 'absint', wp_list_pluck( $results, 'term_count', 'term_count_id' ) );
}
/**
* Get rating counts for the current products.
*
* @param \WP_REST_Request $request The request object.
* @return array rating=>count pairs.
*/
public function get_rating_counts( $request ) {
global $wpdb;
// Regenerate the products query without rating request params.
unset( $request['rating'] );
// Grab the request from the WP Query object, and remove SQL_CALC_FOUND_ROWS and Limits so we get a list of all products.
$product_query = new ProductQuery();
add_filter( 'posts_clauses', array( $product_query, 'add_query_clauses' ), 10, 2 );
add_filter( 'posts_pre_query', '__return_empty_array' );
$query_args = $product_query->prepare_objects_query( $request );
$query_args['no_found_rows'] = true;
$query_args['posts_per_page'] = -1;
$query = new \WP_Query();
$result = $query->query( $query_args );
$product_query_sql = $query->request;
remove_filter( 'posts_clauses', array( $product_query, 'add_query_clauses' ), 10 );
remove_filter( 'posts_pre_query', '__return_empty_array' );
$rating_count_sql = "
SELECT COUNT( DISTINCT product_id ) as product_count, ROUND( average_rating, 0 ) as rounded_average_rating
FROM {$wpdb->wc_product_meta_lookup}
WHERE product_id IN ( {$product_query_sql} )
AND average_rating > 0
GROUP BY rounded_average_rating
ORDER BY rounded_average_rating ASC
";
$results = $wpdb->get_results( $rating_count_sql ); // phpcs:ignore
return array_map( 'absint', wp_list_pluck( $results, 'product_count', 'rounded_average_rating' ) );
}
}

View File

@ -0,0 +1,349 @@
<?php
/**
* Helper class to handle product queries for the API.
*
* @package WooCommerce/Blocks
*/
namespace Automattic\WooCommerce\Blocks\RestApi\StoreApi\Utilities;
defined( 'ABSPATH' ) || exit;
/**
* Product Query class.
*
* @since $VID:$
*/
class ProductQuery {
/**
* Prepare query args to pass to WP_Query for a REST API request.
*
* @param \WP_REST_Request $request Request data.
* @return array
*/
public function prepare_objects_query( $request ) {
$args = array();
$args['offset'] = $request['offset'];
$args['order'] = $request['order'];
$args['orderby'] = $request['orderby'];
$args['paged'] = $request['page'];
$args['post__in'] = $request['include'];
$args['post__not_in'] = $request['exclude'];
$args['posts_per_page'] = $request['per_page'];
$args['post_parent__in'] = $request['parent'];
$args['post_parent__not_in'] = $request['parent_exclude'];
$args['s'] = $request['search'];
$args['fields'] = 'ids';
$args['ignore_sticky_posts'] = true;
$args['post_status'] = 'publish';
if ( 'date' === $args['orderby'] ) {
$args['orderby'] = 'date ID';
}
$args['date_query'] = array();
// Set before into date query. Date query must be specified as an array of an array.
if ( isset( $request['before'] ) ) {
$args['date_query'][0]['before'] = $request['before'];
}
// Set after into date query. Date query must be specified as an array of an array.
if ( isset( $request['after'] ) ) {
$args['date_query'][0]['after'] = $request['after'];
}
// Set date query colummn. Defaults to post_date.
if ( isset( $request['date_column'] ) && ! empty( $args['date_query'][0] ) ) {
$args['date_query'][0]['column'] = 'post_' . $request['date_column'];
}
// Set custom args to handle later during clauses.
$custom_keys = array(
'sku',
'min_price',
'max_price',
'stock_status',
);
foreach ( $custom_keys as $key ) {
if ( ! empty( $request[ $key ] ) ) {
$args[ $key ] = $request[ $key ];
}
}
// Taxonomy query to filter products by type, category,
// tag, shipping class, and attribute.
$tax_query = array();
// Map between taxonomy name and arg's key.
$taxonomies = array(
'product_cat' => 'category',
'product_tag' => 'tag',
);
// Set tax_query for each passed arg.
foreach ( $taxonomies as $taxonomy => $key ) {
if ( ! empty( $request[ $key ] ) ) {
$tax_query[] = array(
'taxonomy' => $taxonomy,
'field' => 'term_id',
'terms' => $request[ $key ],
);
}
}
// Filter product type by slug.
if ( ! empty( $request['type'] ) ) {
$tax_query[] = array(
'taxonomy' => 'product_type',
'field' => 'slug',
'terms' => $request['type'],
);
}
// Filter by attributes.
if ( ! empty( $request['attributes'] ) ) {
foreach ( $request['attributes'] as $attribute ) {
if ( empty( $attribute['term_id'] ) && empty( $attribute['slug'] ) ) {
continue;
}
if ( in_array( $attribute['attribute'], wc_get_attribute_taxonomy_names(), true ) ) {
$tax_query[] = array(
'taxonomy' => $attribute['attribute'],
'field' => ! empty( $attribute['term_id'] ) ? 'term_id' : 'slug',
'terms' => ! empty( $attribute['term_id'] ) ? $attribute['term_id'] : $attribute['slug'],
'operator' => isset( $attribute['operator'] ) ? $attribute['operator'] : 'IN',
);
}
}
}
// Build tax_query if taxonomies are set.
if ( ! empty( $tax_query ) ) {
if ( ! empty( $args['tax_query'] ) ) {
$args['tax_query'] = array_merge( $tax_query, $args['tax_query'] ); // phpcs:ignore
} else {
$args['tax_query'] = $tax_query; // phpcs:ignore
}
}
// Filter featured.
if ( is_bool( $request['featured'] ) ) {
$args['tax_query'][] = array(
'taxonomy' => 'product_visibility',
'field' => 'name',
'terms' => 'featured',
'operator' => true === $request['featured'] ? 'IN' : 'NOT IN',
);
}
// Filter by on sale products.
if ( is_bool( $request['on_sale'] ) ) {
$on_sale_key = $request['on_sale'] ? 'post__in' : 'post__not_in';
$on_sale_ids = wc_get_product_ids_on_sale();
// Use 0 when there's no on sale products to avoid return all products.
$on_sale_ids = empty( $on_sale_ids ) ? array( 0 ) : $on_sale_ids;
$args[ $on_sale_key ] += $on_sale_ids;
}
$operator_mapping = array(
'in' => 'IN',
'not_in' => 'NOT IN',
'and' => 'AND',
);
if ( isset( $args['tax_query'] ) ) {
$category_operator = $request->get_param( 'category_operator' );
$tag_operator = $request->get_param( 'tag_operator' );
$attribute_operator = $request->get_param( 'attribute_operator' );
foreach ( $args['tax_query'] as $i => $tax_query ) {
if ( $category_operator && 'product_cat' === $tax_query['taxonomy'] ) {
$operator = isset( $operator_mapping[ $category_operator ] ) ? $operator_mapping[ $category_operator ] : 'IN';
$args['tax_query'][ $i ]['operator'] = $operator;
$args['tax_query'][ $i ]['include_children'] = 'AND' === $operator ? false : true;
}
if ( 'product_tag' === $tax_query['taxonomy'] ) {
$operator = isset( $operator_mapping[ $tag_operator ] ) ? $operator_mapping[ $tag_operator ] : 'IN';
$args['tax_query'][ $i ]['operator'] = $operator;
}
if ( in_array( $tax_query['taxonomy'], wc_get_attribute_taxonomy_names(), true ) ) {
$operator = isset( $operator_mapping[ $attribute_operator ] ) ? $operator_mapping[ $attribute_operator ] : 'IN';
$args['tax_query'][ $i ]['operator'] = $operator;
}
}
}
$catalog_visibility = $request->get_param( 'catalog_visibility' );
$rating = $request->get_param( 'rating' );
$visibility_options = wc_get_product_visibility_options();
if ( in_array( $catalog_visibility, array_keys( $visibility_options ), true ) ) {
$exclude_from_catalog = 'search' === $catalog_visibility ? '' : 'exclude-from-catalog';
$exclude_from_search = 'catalog' === $catalog_visibility ? '' : 'exclude-from-search';
$args['tax_query'][] = array(
'taxonomy' => 'product_visibility',
'field' => 'name',
'terms' => array( $exclude_from_catalog, $exclude_from_search ),
'operator' => 'hidden' === $catalog_visibility ? 'AND' : 'NOT IN',
'rating_filter' => true,
);
}
if ( $rating ) {
$rating_terms = [];
foreach ( $rating as $value ) {
$rating_terms[] = 'rated-' . $value;
}
$args['tax_query'][] = array(
'taxonomy' => 'product_visibility',
'field' => 'name',
'terms' => $rating_terms,
);
}
// Force the post_type argument, since it's not a user input variable.
if ( ! empty( $request['sku'] ) ) {
$args['post_type'] = array( 'product', 'product_variation' );
} else {
$args['post_type'] = 'product';
}
$orderby = $request->get_param( 'orderby' );
$order = $request->get_param( 'order' );
$ordering_args = WC()->query->get_catalog_ordering_args( $orderby, $order );
$args['orderby'] = $ordering_args['orderby'];
$args['order'] = $ordering_args['order'];
if ( 'include' === $orderby ) {
$args['orderby'] = 'post__in';
} elseif ( 'id' === $orderby ) {
$args['orderby'] = 'ID'; // ID must be capitalized.
} elseif ( 'slug' === $orderby ) {
$args['orderby'] = 'name';
}
if ( $ordering_args['meta_key'] ) {
$args['meta_key'] = $ordering_args['meta_key']; // phpcs:ignore
}
return $args;
}
/**
* Get objects.
*
* @param \WP_REST_Request $request Request data.
* @return array
*/
public function get_objects( $request ) {
$query_args = $this->prepare_objects_query( $request );
add_filter( 'posts_clauses', array( $this, 'add_query_clauses' ), 10, 2 );
$query = new \WP_Query();
$result = $query->query( $query_args );
$total_posts = $query->found_posts;
// Out-of-bounds, run the query again without LIMIT for total count.
if ( $total_posts < 1 ) {
unset( $query_args['paged'] );
$count_query = new \WP_Query();
$count_query->query( $query_args );
$total_posts = $count_query->found_posts;
}
remove_filter( 'posts_clauses', array( $this, 'add_query_clauses' ), 10 );
return array(
'objects' => array_map( 'wc_get_product', $result ),
'total' => (int) $total_posts,
'pages' => (int) ceil( $total_posts / (int) $query->query_vars['posts_per_page'] ),
);
}
/**
* Add in conditional search filters for products.
*
* @param array $args Query args.
* @param \WC_Query $wp_query WC_Query object.
* @return array
*/
public function add_query_clauses( $args, $wp_query ) {
global $wpdb;
if ( $wp_query->get( 'search' ) ) {
$search = "'%" . $wpdb->esc_like( $wp_query->get( 'search' ) ) . "%'";
$args['join'] = $this->append_product_sorting_table_join( $args['join'] );
$args['where'] .= " AND ({$wpdb->posts}.post_title LIKE {$search}";
$args['where'] .= wc_product_sku_enabled() ? ' OR wc_product_meta_lookup.sku LIKE ' . $search . ')' : ')';
}
if ( $wp_query->get( 'sku' ) ) {
$skus = explode( ',', $wp_query->get( 'sku' ) );
// Include the current string as a SKU too.
if ( 1 < count( $skus ) ) {
$skus[] = $wp_query->get( 'sku' );
}
$args['join'] = $this->append_product_sorting_table_join( $args['join'] );
$args['where'] .= ' AND wc_product_meta_lookup.sku IN ("' . implode( '","', array_map( 'esc_sql', $skus ) ) . '")';
}
if ( $wp_query->get( 'min_price' ) ) {
$args['join'] = $this->append_product_sorting_table_join( $args['join'] );
$args['where'] .= $wpdb->prepare( ' AND wc_product_meta_lookup.min_price >= %f ', floatval( $wp_query->get( 'min_price' ) ) );
}
if ( $wp_query->get( 'max_price' ) ) {
$args['join'] = $this->append_product_sorting_table_join( $args['join'] );
$args['where'] .= $wpdb->prepare( ' AND wc_product_meta_lookup.max_price <= %f ', floatval( $wp_query->get( 'max_price' ) ) );
}
if ( $wp_query->get( 'stock_status' ) ) {
$args['join'] = $this->append_product_sorting_table_join( $args['join'] );
$args['where'] .= $wpdb->prepare( ' AND wc_product_meta_lookup.stock_status = %s ', $wp_query->get( 'stock_status' ) );
}
return $args;
}
/**
* Add meta query.
*
* @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'] = []; // phpcs:ignore
}
$args['meta_query'][] = $meta_query;
return $args['meta_query'];
}
/**
* Join wc_product_meta_lookup to posts if not already joined.
*
* @param string $sql SQL join.
* @return string
*/
protected function append_product_sorting_table_join( $sql ) {
global $wpdb;
if ( ! strstr( $sql, 'wc_product_meta_lookup' ) ) {
$sql .= " LEFT JOIN {$wpdb->wc_product_meta_lookup} wc_product_meta_lookup ON $wpdb->posts.ID = wc_product_meta_lookup.product_id ";
}
return $sql;
}
}

View File

@ -17,8 +17,6 @@ use \WC_Helper_Product as ProductHelper;
class Cart extends TestCase {
/**
* Setup test products data. Called before every test.
*
* @since 1.2.0
*/
public function setUp() {
parent::setUp();
@ -49,8 +47,6 @@ class Cart extends TestCase {
/**
* Test route registration.
*
* @since 3.6.0
*/
public function test_register_routes() {
$routes = $this->server->get_routes();

View File

@ -17,8 +17,6 @@ use \WC_Helper_Product as ProductHelper;
class CartItems extends TestCase {
/**
* Setup test products data. Called before every test.
*
* @since 1.2.0
*/
public function setUp() {
parent::setUp();
@ -49,8 +47,6 @@ class CartItems extends TestCase {
/**
* Test route registration.
*
* @since 3.6.0
*/
public function test_register_routes() {
$routes = $this->server->get_routes();
@ -109,14 +105,14 @@ class CartItems extends TestCase {
$response = $this->server->dispatch( $request );
$data = $response->get_data();
$this->assertEquals( 200, $response->get_status() );
$this->assertEquals( 201, $response->get_status() );
$this->assertEquals( $this->products[0]->get_id(), $data['id'] );
$this->assertEquals( 10, $data['quantity'] );
$response = $this->server->dispatch( $request );
$data = $response->get_data();
$this->assertEquals( 200, $response->get_status() );
$this->assertEquals( 201, $response->get_status() );
$this->assertEquals( $this->products[0]->get_id(), $data['id'] );
$this->assertEquals( 20, $data['quantity'] );
}

View File

@ -0,0 +1,172 @@
<?php
/**
* Controller Tests.
*
* @package WooCommerce\Blocks\Tests
*/
namespace Automattic\WooCommerce\Blocks\Tests\RestApi\StoreApi\Controllers;
use \WP_REST_Request;
use \WC_REST_Unit_Test_Case as TestCase;
use \WC_Helper_Product as ProductHelper;
/**
* Controller Tests.
*/
class ProductCollectionData extends TestCase {
/**
* Setup test products data. Called before every test.
*/
public function setUp() {
parent::setUp();
wp_set_current_user( 0 );
$this->products = [];
$this->products[0] = ProductHelper::create_simple_product( false );
$this->products[0]->set_regular_price( 10 );
$this->products[0]->save();
$this->products[1] = ProductHelper::create_simple_product( false );
$this->products[1]->set_regular_price( 100 );
$this->products[1]->save();
wp_insert_comment( [
'comment_post_ID' => $this->products[0]->get_id(),
'comment_author' => 'admin',
'comment_author_email' => 'woo@woo.local',
'comment_author_url' => '',
'comment_content' => 'Good product.',
'comment_approved' => 1,
'comment_type' => 'review',
'comment_meta' => [
'rating' => 5,
]
] );
wp_insert_comment( [
'comment_post_ID' => $this->products[1]->get_id(),
'comment_author' => 'admin',
'comment_author_email' => 'woo@woo.local',
'comment_author_url' => '',
'comment_content' => 'Another very good product.',
'comment_approved' => 1,
'comment_type' => 'review',
'comment_meta' => [
'rating' => 4,
]
] );
\WC_Comments::clear_transients( $this->products[0]->get_id() );
\WC_Comments::clear_transients( $this->products[1]->get_id() );
}
/**
* Test route registration.
*/
public function test_register_routes() {
$routes = $this->server->get_routes();
$this->assertArrayHasKey( '/wc/store/products/collection-data', $routes );
}
/**
* Test getting items.
*/
public function test_get_items() {
$response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/store/products/collection-data' ) );
$data = $response->get_data();
$this->assertEquals( 200, $response->get_status() );
$this->assertEquals( null, $data['min_price'] );
$this->assertEquals( null, $data['max_price'] );
$this->assertEquals( null, $data['attribute_counts'] );
$this->assertEquals( null, $data['rating_counts'] );
}
/**
* Test calculation method.
*/
public function test_calculate_price_range() {
$request = new WP_REST_Request( 'GET', '/wc/store/products/collection-data' );
$request->set_param( 'calculate_price_range', true );
$response = $this->server->dispatch( $request );
$data = $response->get_data();
$this->assertEquals( 200, $response->get_status() );
$this->assertEquals( '10.00', $data['min_price'] );
$this->assertEquals( '100.00', $data['max_price'] );
$this->assertEquals( null, $data['attribute_counts'] );
$this->assertEquals( null, $data['rating_counts'] );
}
/**
* Test calculation method.
*/
public function test_calculate_attribute_counts() {
ProductHelper::create_variation_product();
$request = new WP_REST_Request( 'GET', '/wc/store/products/collection-data' );
$request->set_param( 'calculate_attribute_counts', 'pa_size' );
$response = $this->server->dispatch( $request );
$data = $response->get_data();
$this->assertEquals( 200, $response->get_status() );
$this->assertEquals( null, $data['min_price'] );
$this->assertEquals( null, $data['max_price'] );
$this->assertEquals( null, $data['rating_counts'] );
$this->assertArrayHasKey( 'term', $data['attribute_counts'][0] );
$this->assertArrayHasKey( 'count', $data['attribute_counts'][0] );
}
/**
* Test calculation method.
*/
public function test_calculate_rating_counts() {
$request = new WP_REST_Request( 'GET', '/wc/store/products/collection-data' );
$request->set_param( 'calculate_rating_counts', true );
$response = $this->server->dispatch( $request );
$data = $response->get_data();
$this->assertEquals( 200, $response->get_status() );
$this->assertEquals( null, $data['min_price'] );
$this->assertEquals( null, $data['max_price'] );
$this->assertEquals( null, $data['attribute_counts'] );
$this->assertEquals( [
[
'rating' => 4,
'count' => 1,
],
[
'rating' => 5,
'count' => 1,
]
], $data['rating_counts'] );
}
/**
* Test schema retrieval.
*/
public function test_get_item_schema() {
$controller = new \Automattic\WooCommerce\Blocks\RestApi\StoreApi\Controllers\ProductCollectionData();
$schema = $controller->get_item_schema();
$this->assertArrayHasKey( 'min_price', $schema['properties'] );
$this->assertArrayHasKey( 'max_price', $schema['properties'] );
$this->assertArrayHasKey( 'attribute_counts', $schema['properties'] );
$this->assertArrayHasKey( 'rating_counts', $schema['properties'] );
}
/**
* Test collection params getter.
*/
public function test_get_collection_params() {
$controller = new \Automattic\WooCommerce\Blocks\RestApi\StoreApi\Controllers\ProductCollectionData();
$params = $controller->get_collection_params();
$this->assertArrayHasKey( 'calculate_price_range', $params );
$this->assertArrayHasKey( 'calculate_attribute_counts', $params );
$this->assertArrayHasKey( 'calculate_rating_counts', $params );
}
}

View File

@ -0,0 +1,158 @@
<?php
/**
* Controller Tests.
*
* @package WooCommerce\Blocks\Tests
*/
namespace Automattic\WooCommerce\Blocks\Tests\RestApi\StoreApi\Controllers;
use \WP_REST_Request;
use \WC_REST_Unit_Test_Case as TestCase;
use \WC_Helper_Product as ProductHelper;
/**
* Products Controller Tests.
*/
class Products extends TestCase {
/**
* Setup test products data. Called before every test.
*/
public function setUp() {
parent::setUp();
wp_set_current_user( 0 );
$this->products = [];
$this->products[0] = ProductHelper::create_simple_product( false );
$this->products[0]->save();
$this->products[1] = ProductHelper::create_simple_product( false );
$this->products[1]->save();
}
/**
* Test route registration.
*/
public function test_register_routes() {
$routes = $this->server->get_routes();
$this->assertArrayHasKey( '/wc/store/products', $routes );
$this->assertArrayHasKey( '/wc/store/products/(?P<id>[\d]+)', $routes );
}
/**
* Test getting item.
*/
public function test_get_item() {
$response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/store/products/' . $this->products[0]->get_id() ) );
$data = $response->get_data();
$this->assertEquals( 200, $response->get_status() );
$this->assertEquals( $this->products[0]->get_id(), $data['id'] );
$this->assertEquals( $this->products[0]->get_title(), $data['name'] );
$this->assertEquals( $this->products[0]->get_permalink(), $data['permalink'] );
$this->assertEquals( $this->products[0]->get_sku(), $data['sku'] );
$this->assertEquals( $this->products[0]->get_price(), $data['price'] );
$this->assertEquals( $this->products[0]->get_price_html(), $data['price_html'] );
$this->assertEquals( $this->products[0]->get_average_rating(), $data['average_rating'] );
$this->assertEquals( $this->products[0]->get_review_count(), $data['review_count'] );
}
/**
* Test getting items.
*/
public function test_get_items() {
$response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/store/products' ) );
$data = $response->get_data();
$this->assertEquals( 200, $response->get_status() );
$this->assertEquals( 2, count( $data ) );
$this->assertArrayHasKey( 'id', $data[0] );
$this->assertArrayHasKey( 'name', $data[0] );
$this->assertArrayHasKey( 'variation', $data[0] );
$this->assertArrayHasKey( 'permalink', $data[0] );
$this->assertArrayHasKey( 'description', $data[0] );
$this->assertArrayHasKey( 'sku', $data[0] );
$this->assertArrayHasKey( 'price', $data[0] );
$this->assertArrayHasKey( 'price_html', $data[0] );
$this->assertArrayHasKey( 'average_rating', $data[0] );
$this->assertArrayHasKey( 'review_count', $data[0] );
$this->assertArrayHasKey( 'images', $data[0] );
}
/**
* Test schema retrieval.
*/
public function test_get_item_schema() {
$controller = new \Automattic\WooCommerce\Blocks\RestApi\StoreApi\Controllers\Products();
$schema = $controller->get_item_schema();
$this->assertArrayHasKey( 'id', $schema['properties'] );
$this->assertArrayHasKey( 'name', $schema['properties'] );
$this->assertArrayHasKey( 'variation', $schema['properties'] );
$this->assertArrayHasKey( 'permalink', $schema['properties'] );
$this->assertArrayHasKey( 'description', $schema['properties'] );
$this->assertArrayHasKey( 'sku', $schema['properties'] );
$this->assertArrayHasKey( 'price', $schema['properties'] );
$this->assertArrayHasKey( 'price_html', $schema['properties'] );
$this->assertArrayHasKey( 'average_rating', $schema['properties'] );
$this->assertArrayHasKey( 'review_count', $schema['properties'] );
$this->assertArrayHasKey( 'images', $schema['properties'] );
}
/**
* Test conversion of prdouct to rest response.
*/
public function test_prepare_item_for_response() {
$controller = new \Automattic\WooCommerce\Blocks\RestApi\StoreApi\Controllers\Products();
$response = $controller->prepare_item_for_response( $this->products[0], [] );
$this->assertArrayHasKey( 'id', $response->get_data() );
$this->assertArrayHasKey( 'name', $response->get_data() );
$this->assertArrayHasKey( 'variation', $response->get_data() );
$this->assertArrayHasKey( 'permalink', $response->get_data() );
$this->assertArrayHasKey( 'description', $response->get_data() );
$this->assertArrayHasKey( 'sku', $response->get_data() );
$this->assertArrayHasKey( 'price', $response->get_data() );
$this->assertArrayHasKey( 'price_html', $response->get_data() );
$this->assertArrayHasKey( 'average_rating', $response->get_data() );
$this->assertArrayHasKey( 'review_count', $response->get_data() );
$this->assertArrayHasKey( 'images', $response->get_data() );
}
/**
* Test collection params getter.
*/
public function test_get_collection_params() {
$controller = new \Automattic\WooCommerce\Blocks\RestApi\StoreApi\Controllers\Products();
$params = $controller->get_collection_params();
$this->assertArrayHasKey( 'page', $params );
$this->assertArrayHasKey( 'per_page', $params );
$this->assertArrayHasKey( 'search', $params );
$this->assertArrayHasKey( 'after', $params );
$this->assertArrayHasKey( 'before', $params );
$this->assertArrayHasKey( 'date_column', $params );
$this->assertArrayHasKey( 'exclude', $params );
$this->assertArrayHasKey( 'include', $params );
$this->assertArrayHasKey( 'offset', $params );
$this->assertArrayHasKey( 'order', $params );
$this->assertArrayHasKey( 'orderby', $params );
$this->assertArrayHasKey( 'parent', $params );
$this->assertArrayHasKey( 'parent_exclude', $params );
$this->assertArrayHasKey( 'type', $params );
$this->assertArrayHasKey( 'sku', $params );
$this->assertArrayHasKey( 'featured', $params );
$this->assertArrayHasKey( 'category', $params );
$this->assertArrayHasKey( 'tag', $params );
$this->assertArrayHasKey( 'on_sale', $params );
$this->assertArrayHasKey( 'min_price', $params );
$this->assertArrayHasKey( 'max_price', $params );
$this->assertArrayHasKey( 'stock_status', $params );
$this->assertArrayHasKey( 'category_operator', $params );
$this->assertArrayHasKey( 'tag_operator', $params );
$this->assertArrayHasKey( 'attribute_operator', $params );
$this->assertArrayHasKey( 'attributes', $params );
$this->assertArrayHasKey( 'catalog_visibility', $params );
$this->assertArrayHasKey( 'rating', $params );
}
}