Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Shopping: Support pagination in API #11575

Merged
merged 50 commits into from
Jun 8, 2022
Merged
Show file tree
Hide file tree
Changes from 45 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
f0fedb8
sidebar product sorting
timarney May 20, 2022
a2bbf24
update dropdown
timarney May 24, 2022
e1bac30
use array for switch
timarney May 24, 2022
11f7423
add orderby + order params
timarney May 24, 2022
d2badd2
file reser
timarney May 24, 2022
277d50a
fix tests
timarney May 24, 2022
7eb8917
remove empty quotes
timarney May 24, 2022
319536c
lint
timarney May 24, 2022
9a2e4ee
Update packages/story-editor/src/components/library/panes/shopping/pr…
timarney May 25, 2022
6c67d4a
Update includes/Shopping/Shopify_Query.php
timarney May 25, 2022
8b1e4b5
Update includes/Shopping/Shopify_Query.php
timarney May 25, 2022
5ba725c
update var names
timarney May 25, 2022
b59acac
test data provider
timarney May 25, 2022
7e9d650
Update includes/REST_API/Products_Controller.php
timarney May 25, 2022
7cf285a
Update tests/phpunit/integration/includes/Shopping/Mock_Vendor_Error.php
timarney May 25, 2022
10faab3
Update includes/REST_API/Products_Controller.php
timarney May 25, 2022
90a4d1b
Update includes/Shopping/Shopify_Query.php
timarney May 25, 2022
6fa54c1
Merge branch 'main' into fix/11253
timarney May 25, 2022
9588665
Update includes/Shopping/Shopify_Query.php
timarney May 25, 2022
899419f
lint
timarney May 25, 2022
39fa5ff
lint
timarney May 25, 2022
8008da6
add karma test
timarney May 26, 2022
5fa2786
Respect per page param.
spacedmonkey May 26, 2022
d8fba81
update wc price query
timarney May 26, 2022
6359139
Merge branch 'fix/11253' into fix/pagination-11253
spacedmonkey May 26, 2022
ec83131
Update includes/Shopping/Shopify_Query.php
timarney May 27, 2022
a068bba
Merge branch 'main' into fix/11253
spacedmonkey May 30, 2022
0da480f
Add pagination support.
spacedmonkey May 30, 2022
ab8590f
Merge branch 'fix/11253' into fix/pagination-11253
spacedmonkey May 30, 2022
9ae4a03
Refactor with caching.
spacedmonkey May 30, 2022
578f7bc
Fix tests.
spacedmonkey May 30, 2022
e5520b6
Improve cache key generation.
spacedmonkey May 30, 2022
cf57cb8
Fix lint
spacedmonkey May 30, 2022
8e99979
Add header.
spacedmonkey May 30, 2022
3784e53
Feedback.
spacedmonkey May 31, 2022
efac296
Add comment.
spacedmonkey May 31, 2022
1f5bfe9
Clearer logic for after.
spacedmonkey Jun 1, 2022
c540e08
Improve comments and error message.
spacedmonkey Jun 1, 2022
22a2911
Merge branch 'main' into fix/pagination-11253
spacedmonkey Jun 1, 2022
a86f76e
Improve comments again.
spacedmonkey Jun 1, 2022
b76f8d1
Fix lint.
spacedmonkey Jun 1, 2022
c3cec2a
Merge branch 'main' into fix/pagination-11253
spacedmonkey Jun 1, 2022
a908fae
Merge branch 'main' into fix/pagination-11253
spacedmonkey Jun 8, 2022
7d1571e
Fix lint.
spacedmonkey Jun 8, 2022
670575d
Fix lint.
spacedmonkey Jun 8, 2022
e43f1ad
Apply suggestions from code review
spacedmonkey Jun 8, 2022
d96cdf4
Fix test.
spacedmonkey Jun 8, 2022
baa7839
Merge branch 'main' into fix/pagination-11253
spacedmonkey Jun 8, 2022
4ad79ac
Remove repeated logic.
spacedmonkey Jun 8, 2022
79bd1a2
Apply suggestions from code review
spacedmonkey Jun 8, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions includes/Interfaces/Product_Query.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@

namespace Google\Web_Stories\Interfaces;

use Google\Web_Stories\Shopping\Product;
use WP_Error;

/**
Expand All @@ -39,9 +38,11 @@ interface Product_Query {
* @since 1.21.0
*
* @param string $search_term Search term.
* @param string $orderby Sort collection by product attribute.
* @param string $order Order sort attribute ascending or descending.
* @return Product[]|WP_Error
* @param int $page Number of page for paginated requests.
* @param int $per_page Number of products to be fetched.
* @param string $orderby Sort collection by product attribute.
* @param string $order Order sort attribute ascending or descending.
* @return array|WP_Error
*/
public function get_search( string $search_term, string $orderby, string $order);
public function get_search( string $search_term, int $page = 1, int $per_page = 100, string $orderby = 'date', string $order = 'desc' );
}
38 changes: 32 additions & 6 deletions includes/REST_API/Products_Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ public function get_items_permissions_check( $request ) {
/**
* Retrieves all products.
*
* @SuppressWarnings(PHPMD.NPathComplexity)
*
* @since 1.20.0
*
* @param WP_REST_Request $request Full details about the request.
Expand Down Expand Up @@ -168,34 +170,56 @@ public function get_items( $request ) {
* @var string $search_term
*/
$search_term = ! empty( $request['search'] ) ? $request['search'] : '';

/**
* Request context.
*
* @var string $orderby
*/
$orderby = ! empty( $request['orderby'] ) ? $request['orderby'] : 'date';

/**
* Request context.
*
* @var int $page
*/
$page = ! empty( $request['page'] ) ? $request['page'] : 1;

/**
* Request context.
*
* @var int $per_page
*/
$per_page = ! empty( $request['per_page'] ) ? $request['per_page'] : 100;

/**
* Request context.
*
* @var string $order
*/
$order = ! empty( $request['order'] ) ? $request['order'] : 'desc';


$query_result = $query->get_search( $search_term, $orderby, $order );

$query_result = $query->get_search( $search_term, $page, $per_page, $orderby, $order );
if ( is_wp_error( $query_result ) ) {
return $query_result;
}

$products = [];
foreach ( $query_result as $product ) {
foreach ( $query_result['products'] as $product ) {
$data = $this->prepare_item_for_response( $product, $request );
$products[] = $this->prepare_response_for_collection( $data );
}

return rest_ensure_response( $products );
/**
* Response object.
*
* @var WP_REST_Response $response
*/
$response = rest_ensure_response( $products );

$response->header( 'X-WP-HasNextPage', (string) $query_result['has_next_page'] );

return $response;
}

/**
Expand Down Expand Up @@ -420,6 +444,8 @@ public function get_item_schema(): array {
public function get_collection_params(): array {
$query_params = parent::get_collection_params();

$query_params['per_page']['default'] = 100;

$query_params['orderby'] = [
'description' => __( 'Sort collection by product attribute.', 'web-stories' ),
'type' => 'string',
Expand Down
114 changes: 79 additions & 35 deletions includes/Shopping/Shopify_Query.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@
* @phpstan-type ShopifyGraphQLPriceRange array{minVariantPrice: array{amount: int, currencyCode: string}}
* @phpstan-type ShopifyGraphQLProductImage array{url: string, altText: string}
* @phpstan-type ShopifyGraphQLProduct array{id: string, handle: string, title: string, vendor: string, description: string, onlineStoreUrl?: string, images: array{edges: array{node: ShopifyGraphQLProductImage}[]}, priceRange: ShopifyGraphQLPriceRange}
* @phpstan-type ShopifyGraphQLResponse array{errors?: ShopifyGraphQLError, data: array{products: array{edges: array{node: ShopifyGraphQLProduct}[]}}}
* @phpstan-type ShopifyGraphQLResponse array{errors?: ShopifyGraphQLError, data: array{products: array{edges: array{node: ShopifyGraphQLProduct}[], pageInfo: array{hasNextPage: bool, endCursor: string}}}}
*/
class Shopify_Query implements Product_Query {
protected const API_VERSION = '2022-01';
protected const API_VERSION = '2022-04';

/**
* Settings instance.
Expand Down Expand Up @@ -150,18 +150,22 @@ protected function execute_query( string $query ) {
*
* @since 1.21.0
*
* @param string $search_term Search term to filter products by.
* @param string $orderby Sort collection by product attribute.
* @param string $order Order sort attribute ascending or descending.
* @return string The assembled GraphQL query.
* @param string $search_term Search term to filter products by.
* @param string $after The cursor to retrieve nodes after in the connection.
* @param int $per_page Number of products to be fetched.
* @param string $orderby Sort collection by product attribute.
* @param string $order Order sort attribute ascending or descending.
* @return string The assembled GraphQL query.
*/
protected function get_products_query( string $search_term, string $orderby, string $order ): string {
protected function get_products_query( string $search_term, string $after, int $per_page, string $orderby, string $order ): string {
$search_string = empty( $search_term ) ? '*' : '*' . $search_term . '*';
$sortkey = 'date' === $orderby ? 'CREATED_AT' : strtoupper( $orderby );
$reverse = 'asc' === $order ? 'false' : 'true';
$after = empty( $after ) ? 'null' : sprintf( '"%s"', $after );

return <<<QUERY
{
products(first: 100, sortKey: $sortkey, reverse: $reverse, query: "title:$search_string") {
products(first: $per_page, after: $after, sortKey: $sortkey, reverse: $reverse, query: "title:$search_string") {
edges {
node {
id
Expand All @@ -186,6 +190,10 @@ protected function get_products_query( string $search_term, string $orderby, str
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
QUERY;
Expand All @@ -198,34 +206,34 @@ protected function get_products_query( string $search_term, string $orderby, str
*
* @since 1.21.0
*
* @param string $search_term Search term to filter products by.
* @param string $orderby Sort retrieved products by parameter.
* @param string $order Whether to order products in ascending or descending order.
* Accepts 'asc' (ascending) or 'desc' (descending).
* @return array|WP_Error Response data or error object on failure.
* @param string $search_term Search term to filter products by.
* @param string $after The cursor to retrieve nodes after in the connection.
* @param int $per_page Number of products to be fetched.
* @param string $orderby Sort retrieved products by parameter.
* @param string $order Whether to order products in ascending or descending order.
* Accepts 'asc' (ascending) or 'desc' (descending).
* @return array|WP_Error Response data or error object on failure.
*/
protected function fetch_remote_products( string $search_term, string $orderby, string $order ) {

protected function get_remote_products( string $search_term, string $after, int $per_page, string $orderby, string $order ) {
/**
* Filters the Shopify products data TTL value.
*
* @since 1.21.0
*
* @param int $time Time to live (in seconds). Default is 5 minutes.
*/
$cache_ttl = apply_filters( 'web_stories_shopify_data_cache_ttl', 5 * MINUTE_IN_SECONDS );
$cache_key = 'web_stories_shopify_data_' . md5( $search_term . '-' . $orderby . '-' . $order );
$cache_ttl = apply_filters( 'web_stories_shopify_data_cache_ttl', 5 * MINUTE_IN_SECONDS );
$cache_args = (string) wp_json_encode( compact( 'search_term', 'after', 'per_page', 'orderby', 'order' ) );
$cache_key = 'web_stories_shopify_data_' . md5( $cache_args );

$data = get_transient( $cache_key );

if ( \is_string( $data ) && ! empty( $data ) ) {
return (array) json_decode( $data, true );
}

$query = $this->get_products_query( $search_term, $orderby, $order );

$body = $this->execute_query( $query );

$query = $this->get_products_query( $search_term, $after, $per_page, $orderby, $order );
$body = $this->execute_query( $query );
if ( is_wp_error( $body ) ) {
return $body;
}
Expand All @@ -236,10 +244,8 @@ protected function fetch_remote_products( string $search_term, string $orderby,
* @var ShopifyGraphQLResponse $result
*/
$result = json_decode( $body, true );

if ( isset( $result['errors'] ) ) {
$wp_error = new WP_Error();

foreach ( $result['errors'] as $error ) {
$error_code = $error['extensions']['code'];
// https://shopify.dev/api/storefront#status_and_error_codes.
Expand All @@ -260,7 +266,7 @@ protected function fetch_remote_products( string $search_term, string $orderby,
$wp_error->add( 'rest_unknown', __( 'Error fetching products from Shopify.', 'web-stories' ), [ 'status' => 500 ] );
}
}

return $wp_error;
}

Expand All @@ -270,26 +276,64 @@ protected function fetch_remote_products( string $search_term, string $orderby,
return $result;
}


/**
* Remotely fetches all products from the store.
*
* @since 1.21.0
spacedmonkey marked this conversation as resolved.
Show resolved Hide resolved
*
* @param string $search_term Search term to filter products by.
* @param int $page Number of page for paginated requests.
* @param int $per_page Number of products to be fetched.
* @param string $orderby Sort retrieved products by parameter.
* @param string $order Whether to order products in ascending or descending order.
* Accepts 'asc' (ascending) or 'desc' (descending).
* @return array|WP_Error Response data or error object on failure.
*/
protected function fetch_remote_products( string $search_term, int $page, int $per_page, string $orderby, string $order ) {
$after = '';
if ( $page > 1 ) {
// Loop around all the pages, getting the endCursor of each page, until you get the last one.
for ( $i = 1; $i < $page; $i ++ ) {
spacedmonkey marked this conversation as resolved.
Show resolved Hide resolved
$result = $this->get_remote_products( $search_term, $after, $per_page, $orderby, $order );
if ( is_wp_error( $result ) ) {
return $result;
}

$has_next_page = $result['data']['products']['pageInfo']['hasNextPage'];
if ( ! $has_next_page ) {
return new WP_Error( 'rest_no_page', __( 'Error fetching products from Shopify.', 'web-stories' ), [ 'status' => 404 ] );
}
$after = (string) $result['data']['products']['pageInfo']['endCursor'];
}
}

$result = $this->get_remote_products( $search_term, $after, $per_page, $orderby, $order );

return $result;
spacedmonkey marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Get products by search term.
*
* @since 1.21.0
*
* @param string $search_term Search term.
* @param string $orderby Sort retrieved products by parameter. Default 'date'.
* @param string $order Whether to order products in ascending or descending order.
* Accepts 'asc' (ascending) or 'desc' (descending). Default 'desc'.
* @return Product[]|WP_Error
* @param int $page Number of page for paginated requests.
* @param int $per_page Number of products to be fetched.
* @param string $orderby Sort retrieved products by parameter. Default 'date'.
* @param string $order Whether to order products in ascending or descending order.
* Accepts 'asc' (ascending) or 'desc' (descending). Default 'desc'.
* @return array|WP_Error
*/
public function get_search( string $search_term, string $orderby = 'date', string $order = 'desc' ) {
$result = $this->fetch_remote_products( $search_term, $orderby, $order );

public function get_search( string $search_term, int $page = 1, int $per_page = 100, string $orderby = 'date', string $order = 'desc' ) {
$result = $this->fetch_remote_products( $search_term, $page, $per_page, $orderby, $order );
if ( is_wp_error( $result ) ) {
return $result;
}

$results = [];
$products = [];

$has_next_page = $result['data']['products']['pageInfo']['hasNextPage'] ?? false;

foreach ( $result['data']['products']['edges'] as $edge ) {
$product = $edge['node'];
Expand All @@ -309,7 +353,7 @@ public function get_search( string $search_term, string $orderby = 'date', strin
// In this case, we can fall back to a manually constructed product URL.
$product_url = $product['onlineStoreUrl'] ?? sprintf( 'https://%1$s/products/%2$s/', $this->get_host(), $product['handle'] );

$results[] = new Product(
$products[] = new Product(
[
'id' => $product['id'],
'title' => $product['title'],
Expand All @@ -327,6 +371,6 @@ public function get_search( string $search_term, string $orderby = 'date', strin
);
}

return $results;
return compact( 'products', 'has_next_page' );
}
}
Loading