whoami7 - Manager
:
/
home
/
rattkxnv
/
fitwithiit.com
/
wp-content
/
plugins
/
web-stories
/
includes
/
Shopping
/
Upload File:
files >> //home/rattkxnv/fitwithiit.com/wp-content/plugins/web-stories/includes/Shopping/Shopify_Query.php
<?php /** * Class Shopify_Query * * @link https://github.com/googleforcreators/web-stories-wp * * @copyright 2022 Google LLC * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 */ /** * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ declare(strict_types = 1); namespace Google\Web_Stories\Shopping; use Google\Web_Stories\Interfaces\Product_Query; use Google\Web_Stories\Settings; use WP_Error; use WP_Http; /** * Class Shopify_Query * * @phpstan-type ShopifyGraphQLError array{ * message: string, * extensions: array{code: string, requestId: string} * }[] * @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 * }[], * pageInfo: array{ * hasNextPage: bool, * endCursor: string * } * } * } * } */ class Shopify_Query implements Product_Query { protected const API_VERSION = '2022-04'; /** * Settings instance. * * @var Settings Settings instance. */ private Settings $settings; /** * Constructor. * * @param Settings $settings Settings instance. */ public function __construct( Settings $settings ) { $this->settings = $settings; } /** * Get products by search term. * * @since 1.21.0 * * @param string $search_term Search term. * @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{products: array<Product>, has_next_page: bool}|WP_Error */ 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; } $products = []; $has_next_page = $result['data']['products']['pageInfo']['hasNextPage']; foreach ( $result['data']['products']['edges'] as $edge ) { $product = $edge['node']; $images = []; foreach ( $product['images']['edges'] as $image_edge ) { $image = $image_edge['node']; $images[] = [ 'url' => $image['url'], 'alt' => $image['altText'] ?? '', ]; } // URL is null if the resource is currently not published to the Online Store sales channel, // or if the shop is password-protected. // 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'] ); $products[] = new Product( [ 'id' => $product['id'], 'title' => $product['title'], 'brand' => $product['vendor'], // TODO: Maybe eventually provide full price range. // See https://github.com/ampproject/amphtml/issues/37957. 'price' => (float) $product['priceRange']['minVariantPrice']['amount'], 'price_currency' => $product['priceRange']['minVariantPrice']['currencyCode'], 'images' => $images, 'details' => $product['description'], // URL is null if the resource is currently not published to the Online Store sales channel, // or if the shop is password-protected. 'url' => $product_url, ] ); } return compact( 'products', 'has_next_page' ); } /** * Returns the Shopify host name. * * @since 1.21.0 * * @return string Shopify host. */ protected function get_host(): string { /** * Host name. * * @var string $host */ $host = $this->settings->get_setting( Settings::SETTING_NAME_SHOPIFY_HOST ); return $host; } /** * Returns the Shopify access token. * * @since 1.21.0 * * @return string Shopify access token. */ protected function get_access_token(): string { /** * Access token. * * @var string $access_token */ $access_token = $this->settings->get_setting( Settings::SETTING_NAME_SHOPIFY_ACCESS_TOKEN ); return $access_token; } /** * Remotely executes a GraphQL query. * * @since 1.21.0 * * @param string $query GraphQL query to execute. * @return string|WP_Error Query result or error object on failure. */ protected function execute_query( string $query ) { $host = $this->get_host(); $access_token = $this->get_access_token(); if ( empty( $host ) || empty( $access_token ) ) { return new WP_Error( 'rest_missing_credentials', __( 'Missing API credentials.', 'web-stories' ), [ 'status' => 400 ] ); } if ( ! preg_match( '/^[\w-]+\.myshopify\.com$/i', $host ) ) { return new WP_Error( 'rest_invalid_hostname', __( 'Invalid Shopify hostname.', 'web-stories' ), [ 'status' => 400 ] ); } $url = esc_url_raw( sprintf( 'https://%1$s/api/%2$s/graphql.json', $host, self::API_VERSION ) ); $response = wp_remote_post( $url, [ 'headers' => [ 'Content-Type' => 'application/graphql', 'X-Shopify-Storefront-Access-Token' => $access_token, ], 'body' => $query, ] ); $status_code = wp_remote_retrieve_response_code( $response ); if ( WP_Http::UNAUTHORIZED === $status_code || WP_Http::NOT_FOUND === $status_code ) { return new WP_Error( 'rest_invalid_credentials', __( 'Invalid API credentials.', 'web-stories' ), [ 'status' => $status_code ] ); } if ( WP_Http::OK !== $status_code ) { return new WP_Error( 'rest_unknown', __( 'Error fetching products', 'web-stories' ), [ 'status' => $status_code ] ); } return wp_remote_retrieve_body( $response ); } /** * Returns the GraphQL query for getting all products from the store. * * @since 1.21.0 * * @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 $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: $per_page, after: $after, sortKey: $sortkey, reverse: $reverse, query: "title:$search_string") { edges { node { id handle title vendor description priceRange { minVariantPrice { amount currencyCode } } onlineStoreUrl images(first: 10) { edges { node { url(transform:{maxWidth:1000,maxHeight:1000}) altText } } } } } pageInfo { hasNextPage endCursor } } } QUERY; } /** * Remotely fetches all products from the store. * * Retrieves cached data if available. * * @since 1.21.0 * * @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. * * @phpstan-return ShopifyGraphQLResponse|WP_Error */ 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 = $this->get_cache_key( $search_term, $after, $per_page, $orderby, $order ); $data = get_transient( $cache_key ); if ( \is_string( $data ) && ! empty( $data ) ) { /** * Cached response. * * @phpstan-var ShopifyGraphQLResponse $cached_result */ $cached_result = (array) json_decode( $data, true ); return $cached_result; } $query = $this->get_products_query( $search_term, $after, $per_page, $orderby, $order ); $body = $this->execute_query( $query ); if ( is_wp_error( $body ) ) { return $body; } /** * Shopify GraphQL API response. * * @var array $result * @phpstan-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. switch ( $error_code ) { case 'THROTTLED': $wp_error->add( 'rest_throttled', __( 'Shopify API rate limit exceeded. Try again later.', 'web-stories' ), [ 'status' => 429 ] ); break; case 'ACCESS_DENIED': $wp_error->add( 'rest_invalid_credentials', __( 'Invalid Shopify API credentials provided.', 'web-stories' ), [ 'status' => 401 ] ); break; case 'SHOP_INACTIVE': $wp_error->add( 'rest_inactive_shop', __( 'Inactive Shopify shop.', 'web-stories' ), [ 'status' => 403 ] ); break; case 'INTERNAL_SERVER_ERROR': $wp_error->add( 'rest_internal_error', __( 'Shopify experienced an internal server error.', 'web-stories' ), [ 'status' => 500 ] ); break; default: $wp_error->add( 'rest_unknown', __( 'Error fetching products from Shopify.', 'web-stories' ), [ 'status' => 500 ] ); } } return $wp_error; } // TODO: Maybe cache errors too? set_transient( $cache_key, $body, $cache_ttl ); return $result; } /** * Get cache key for properties. * * @since 1.22.0 * * @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). */ protected function get_cache_key( string $search_term, string $after, int $per_page, string $orderby, string $order ): string { $cache_args = (string) wp_json_encode( compact( 'search_term', 'after', 'per_page', 'orderby', 'order' ) ); return 'web_stories_shopify_data_' . md5( $cache_args ); } /** * Remotely fetches all products from the store. * * @since 1.22.0 * * @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. * * @phpstan-return ShopifyGraphQLResponse|WP_Error */ 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++ ) { $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']; } } return $this->get_remote_products( $search_term, $after, $per_page, $orderby, $order ); } }
Copyright ©2021 || Defacer Indonesia