BasicShopifyAPI.php (Before) BasicShopifyAPI.php (After)
<?php <?php
   
namespace OhMyBrew; namespace OhMyBrew;
   
use Closure; use Closure;
use Exception; use Exception;
use GuzzleHttp\Client; use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException; use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\ServerException; use GuzzleHttp\Exception\ServerException;
use GuzzleHttp\HandlerStack; use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware; use GuzzleHttp\Middleware;
use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Uri; use GuzzleHttp\Psr7\Uri;
use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel; use Psr\Log\LogLevel;
use stdClass; use stdClass;
.  use Log;
   
/** /**
* Basic Shopify API for REST & GraphQL. * Basic Shopify API for REST & GraphQL.
*/  */ 
class BasicShopifyAPI implements LoggerAwareInterface class BasicShopifyAPI implements LoggerAwareInterface
{ {
   /**    /**
    * API version pattern.     * API version pattern.
    *     *
    * @var string     * @var string
    */      */ 
   const VERSION_PATTERN = '/([0-9]{4}-[0-9]{2})|unstable/';    const VERSION_PATTERN = '/([0-9]{4}-[0-9]{2})|unstable/';
   
   /**    /**
    * The key to use for logging (prefix for filtering).     * The key to use for logging (prefix for filtering).
    *     *
    * @var string     * @var string
    */      */ 
   const LOG_KEY = '[BasicShopifyAPI]';    const LOG_KEY = '[BasicShopifyAPI]';
   
   /**    /**
    * The Guzzle client.     * The Guzzle client.
    *     *
    * @var \GuzzleHttp\Client     * @var \GuzzleHttp\Client
    */      */ 
   protected $client;    protected $client;
   
   /**    /**
    * The version of API.     * The version of API.
    *     *
    * @var string     * @var string
    */      */ 
   protected $version;    protected $version;
   
   /**    /**
    * The Shopify domain.     * The Shopify domain.
    *     *
    * @var string     * @var string
    */      */ 
   protected $shop;    protected $shop;
   
   /**    /**
    * The Shopify access token.     * The Shopify access token.
    *     *
    * @var string     * @var string
    */      */ 
   protected $accessToken;    protected $accessToken;
   
   /**    /**
    * The Shopify API key.     * The Shopify API key.
    *     *
    * @var string     * @var string
    */      */ 
   protected $apiKey;    protected $apiKey;
   
   /**    /**
    * The Shopify API password.     * The Shopify API password.
    *     *
    * @var string     * @var string
    */      */ 
   protected $apiPassword;    protected $apiPassword;
   
   /**    /**
    * The Shopify API secret.     * The Shopify API secret.
    *     *
    * @var string     * @var string
    */      */ 
   protected $apiSecret;    protected $apiSecret;
   
   /**    /**
    * If API calls are from a public or private app.     * If API calls are from a public or private app.
    *     *
    * @var string     * @var string
    */      */ 
   protected $private;    protected $private;
   
   /**    /**
    * If the API was called with per-user grant option, this will be filled.     * If the API was called with per-user grant option, this will be filled.
    *     *
    * @var stdClass     * @var stdClass
    */      */ 
   protected $user;    protected $user;
   
   /**    /**
.      * Used for handling the API rate limit
      */ 
     protected $retryCall;
   
     /**
    * The current API call limits from last request.     * The current API call limits from last request.
    *     *
    * @var array     * @var array
    */      */ 
   protected $apiCallLimits = [    protected $apiCallLimits = [
       'rest'  => [        'rest'  => [
           'left'  => 0,            'left'  => 0,
           'made'  => 0,            'made'  => 0,
           'limit' => 40,            'limit' => 40,
       ],        ],
       'graph' => [        'graph' => [
           'left'          => 0,            'left'          => 0,
           'made'          => 0,            'made'          => 0,
           'limit'         => 1000,            'limit'         => 1000,
           'restoreRate'   => 50,            'restoreRate'   => 50,
           'requestedCost' => 0,            'requestedCost' => 0,
           'actualCost'    => 0,            'actualCost'    => 0,
       ],        ],
   ];    ];
   
   /**    /**
    * If rate limiting is enabled.     * If rate limiting is enabled.
    *     *
    * @var bool     * @var bool
    */      */ 
   protected $rateLimitingEnabled = false;    protected $rateLimitingEnabled = false;
   
   /**    /**
    * The rate limiting cycle (in ms).     * The rate limiting cycle (in ms).
    *     *
    * @var int     * @var int
    */      */ 
   protected $rateLimitCycle = 0.5 * 1000;    protected $rateLimitCycle = 0.5 * 1000;
   
   /**    /**
    * The rate limiting cycle buffer (in ms).     * The rate limiting cycle buffer (in ms).
    *     *
    * @var int     * @var int
    */      */ 
   protected $rateLimitCycleBuffer = 0.1 * 1000;    protected $rateLimitCycleBuffer = 0.1 * 1000;
   
   /**    /**
    * Request timestamp for every new call.     * Request timestamp for every new call.
    * Used for rate limiting.     * Used for rate limiting.
    *     *
    * @var int     * @var int
    */      */ 
   protected $requestTimestamp;    protected $requestTimestamp;
   
   /**    /**
    * The logger.     * The logger.
    *     *
    * @var LoggerInterface     * @var LoggerInterface
    */      */ 
   protected $logger;    protected $logger;
   
   /**    /**
    * Constructor.     * Constructor.
    *     *
    * @param bool $private If this is a private or public app     * @param bool $private If this is a private or public app
    *     *
    * @return self     * @return self
    */      */ 
   public function __construct(bool $private = false)    public function __construct(bool $private = false)
   {    {
       // Set if app is private or public        // Set if app is private or public
       $this->private = $private;        $this->private = $private;
   
.         // Set the retryCall as 1 for handling the API rate limit
         $this->retryCall = 1;
   
       // Create the stack and assign the middleware which attempts to fix redirects        // Create the stack and assign the middleware which attempts to fix redirects
       $stack = HandlerStack::create();        $stack = HandlerStack::create();
       $stack->push(Middleware::mapRequest([$this, 'authRequest']));        $stack->push(Middleware::mapRequest([$this, 'authRequest']));
   
       // Create a default Guzzle client with our stack        // Create a default Guzzle client with our stack
       $this->client = new Client([        $this->client = new Client([
           'handler'  => $stack,            'handler'  => $stack,
           'headers'  => [            'headers'  => [
               'Accept'       => 'application/json',                'Accept'       => 'application/json',
               'Content-Type' => 'application/json',                'Content-Type' => 'application/json',
           ],            ],
       ]);        ]);
   
       return $this;        return $this;
   }    }
   
   /**    /**
    * Determines if the calls are private.     * Determines if the calls are private.
    *     *
    * @return bool     * @return bool
    */      */ 
   public function isPrivate()    public function isPrivate()
   {    {
       return $this->private === true;        return $this->private === true;
   }    }
   
   /**    /**
    * Determines if the calls are public.     * Determines if the calls are public.
    *     *
    * @return bool     * @return bool
    */      */ 
   public function isPublic()    public function isPublic()
   {    {
       return !$this->isPrivate();        return !$this->isPrivate();
   }    }
   
   /**    /**
    * Sets the Guzzle client for the API calls (allows for override with your own).     * Sets the Guzzle client for the API calls (allows for override with your own).
    *     *
    * @param \GuzzleHttp\Client $client The Guzzle client     * @param \GuzzleHttp\Client $client The Guzzle client
    *     *
    * @return self     * @return self
    */      */ 
   public function setClient(Client $client)    public function setClient(Client $client)
   {    {
       $this->client = $client;        $this->client = $client;
   
       return $this;        return $this;
   }    }
   
   /**    /**
    * Sets the version of Shopify API to use.     * Sets the version of Shopify API to use.
    *     *
    * @param string $version     * @param string $version
    *     *
    * @return self     * @return self
    */      */ 
   public function setVersion(string $version)    public function setVersion(string $version)
   {    {
       if (!preg_match(self::VERSION_PATTERN, $version)) {        if (!preg_match(self::VERSION_PATTERN, $version)) {
           // Invalid version string            // Invalid version string
           throw new Exception('Version string must be of YYYY-MM or unstable');            throw new Exception('Version string must be of YYYY-MM or unstable');
       }        }
   
       $this->version = $version;        $this->version = $version;
   
       return $this;        return $this;
   }    }
   
   /**    /**
    * Returns the current in-use API version.     * Returns the current in-use API version.
    *     *
    * @return string     * @return string
    */      */ 
   public function getVersion()    public function getVersion()
   {    {
       return $this->version;        return $this->version;
   }    }
   
   /**    /**
    * Sets the Shopify domain (*.myshopify.com) we're working with.     * Sets the Shopify domain (*.myshopify.com) we're working with.
    *     *
    * @param string $shop The myshopify domain     * @param string $shop The myshopify domain
    *     *
    * @return self     * @return self
    */      */ 
   public function setShop(string $shop)    public function setShop(string $shop)
   {    {
       $this->shop = $shop;        $this->shop = $shop;
   
       return $this;        return $this;
   }    }
   
   /**    /**
    * Gets the Shopify domain (*.myshopify.com) we're working with.     * Gets the Shopify domain (*.myshopify.com) we're working with.
    *     *
    * @return string     * @return string
    */      */ 
   public function getShop()    public function getShop()
   {    {
       return $this->shop;        return $this->shop;
   }    }
   
   /**    /**
    * Sets the access token for use with the Shopify API (public apps).     * Sets the access token for use with the Shopify API (public apps).
    *     *
    * @param string $accessToken The access token     * @param string $accessToken The access token
    *     *
    * @return self     * @return self
    */      */ 
   public function setAccessToken(string $accessToken)    public function setAccessToken(string $accessToken)
   {    {
       $this->accessToken = $accessToken;        $this->accessToken = $accessToken;
   
       return $this;        return $this;
   }    }
   
   /**    /**
    * Gets the access token.     * Gets the access token.
    *     *
    * @return string     * @return string
    */      */ 
   public function getAccessToken()    public function getAccessToken()
   {    {
       return $this->accessToken;        return $this->accessToken;
   }    }
   
   /**    /**
    * Sets the API key for use with the Shopify API (public or private apps).     * Sets the API key for use with the Shopify API (public or private apps).
    *     *
    * @param string $apiKey The API key     * @param string $apiKey The API key
    *     *
    * @return self     * @return self
    */      */ 
   public function setApiKey(string $apiKey)    public function setApiKey(string $apiKey)
   {    {
       $this->apiKey = $apiKey;        $this->apiKey = $apiKey;
   
       return $this;        return $this;
   }    }
   
   /**    /**
    * Sets the API secret for use with the Shopify API (public apps).     * Sets the API secret for use with the Shopify API (public apps).
    *     *
    * @param string $apiSecret The API secret key     * @param string $apiSecret The API secret key
    *     *
    * @return self     * @return self
    */      */ 
   public function setApiSecret(string $apiSecret)    public function setApiSecret(string $apiSecret)
   {    {
       $this->apiSecret = $apiSecret;        $this->apiSecret = $apiSecret;
   
       return $this;        return $this;
   }    }
   
   /**    /**
    * Sets the API password for use with the Shopify API (private apps).     * Sets the API password for use with the Shopify API (private apps).
    *     *
    * @param string $apiPassword The API password     * @param string $apiPassword The API password
    *     *
    * @return self     * @return self
    */      */ 
   public function setApiPassword(string $apiPassword)    public function setApiPassword(string $apiPassword)
   {    {
       $this->apiPassword = $apiPassword;        $this->apiPassword = $apiPassword;
   
       return $this;        return $this;
   }    }
   
   /**    /**
    * Sets the user (public apps).     * Sets the user (public apps).
    *     *
    * @param stdClass $user The user returned from the access request.     * @param stdClass $user The user returned from the access request.
    *     *
    * @return self     * @return self
    */      */ 
   public function setUser(stdClass $user)    public function setUser(stdClass $user)
   {    {
       $this->user = $user;        $this->user = $user;
   
       return $this;        return $this;
   }    }
   
   /**    /**
    * Gets the user.     * Gets the user.
    *     *
    * @return stdClass     * @return stdClass
    */      */ 
   public function getUser()    public function getUser()
   {    {
       return $this->user;        return $this->user;
   }    }
   
   /**    /**
    * Checks if we have a user.     * Checks if we have a user.
    *     *
    * @return bool     * @return bool
    */      */ 
   public function hasUser()    public function hasUser()
   {    {
       return $this->user !== null;        return $this->user !== null;
   }    }
   
   /**    /**
    * Set the rate limiting state to enabled.     * Set the rate limiting state to enabled.
    *     *
    * @param int|null $cycle  The rate limiting cycle (in ms, default 500ms).     * @param int|null $cycle  The rate limiting cycle (in ms, default 500ms).
    * @param int|null $buffer The rate limiting cycle buffer (in ms, default 100ms).     * @param int|null $buffer The rate limiting cycle buffer (in ms, default 100ms).
    *     *
    * @return self     * @return self
    */      */ 
   public function enableRateLimiting(int $cycle = null, int $buffer = null)    public function enableRateLimiting(int $cycle = null, int $buffer = null)
   {    {
       $this->rateLimitingEnabled = true;        $this->rateLimitingEnabled = true;
   
       if (!is_null($cycle)) {        if (!is_null($cycle)) {
           $this->rateLimitCycle = $cycle;            $this->rateLimitCycle = $cycle;
       }        }
   
       if (!is_null($cycle)) {        if (!is_null($cycle)) {
           $this->rateLimitCycleBuffer = $buffer;            $this->rateLimitCycleBuffer = $buffer;
       }        }
   
       return $this;        return $this;
   }    }
   
   /**    /**
    * Set the rate limiting state to disabled.     * Set the rate limiting state to disabled.
    *     *
    * @return self     * @return self
    */      */ 
   public function disableRateLimiting()    public function disableRateLimiting()
   {    {
       $this->rateLimitingEnabled = false;        $this->rateLimitingEnabled = false;
   
       return $this;        return $this;
   }    }
   
   /**    /**
    * Determines if rate limiting is enabled.     * Determines if rate limiting is enabled.
    *     *
    * @return bool     * @return bool
    */      */ 
   public function isRateLimitingEnabled()    public function isRateLimitingEnabled()
   {    {
       return $this->rateLimitingEnabled === true;        return $this->rateLimitingEnabled === true;
   }    }
   
   /**    /**
    * Simple quick method to set shop and access token in one shot.     * Simple quick method to set shop and access token in one shot.
    *     *
    * @param string $shop        The shop's domain     * @param string $shop        The shop's domain
    * @param string $accessToken The access token for API requests     * @param string $accessToken The access token for API requests
    *     *
    * @return self     * @return self
    */      */ 
   public function setSession(string $shop, string $accessToken)    public function setSession(string $shop, string $accessToken)
   {    {
       $this->setShop($shop);        $this->setShop($shop);
       $this->setAccessToken($accessToken);        $this->setAccessToken($accessToken);
   
       return $this;        return $this;
   }    }
   
   /**    /**
    * Accepts a closure to do isolated API calls for a shop.     * Accepts a closure to do isolated API calls for a shop.
    *     *
    * @param string  $shop        The shop's domain     * @param string  $shop        The shop's domain
    * @param string  $accessToken The access token for API requests     * @param string  $accessToken The access token for API requests
    * @param Closure $closure     The closure to run isolated     * @param Closure $closure     The closure to run isolated
    *     *
    * @throws \Exception When closure is missing or not callable     * @throws \Exception When closure is missing or not callable
    *     *
    * @return self     * @return self
    */      */ 
   public function withSession(string $shop, string $accessToken, Closure $closure)    public function withSession(string $shop, string $accessToken, Closure $closure)
   {    {
       $this->log("WithSession started for {$shop}");        $this->log("WithSession started for {$shop}");
   
       // Clone the API class and bind it to the closure        // Clone the API class and bind it to the closure
       $clonedApi = clone $this;        $clonedApi = clone $this;
       $clonedApi->setSession($shop, $accessToken);        $clonedApi->setSession($shop, $accessToken);
   
       return $closure->call($clonedApi);        return $closure->call($clonedApi);
   }    }
   
   /**    /**
    * Returns the base URI to use.     * Returns the base URI to use.
    *     *
    * @return \Guzzle\Psr7\Uri     * @return \Guzzle\Psr7\Uri
    */      */ 
   public function getBaseUri()    public function getBaseUri()
   {    {
       if ($this->shop === null) {        if ($this->shop === null) {
           // Shop is required            // Shop is required
           throw new Exception('Shopify domain missing for API calls');            throw new Exception('Shopify domain missing for API calls');
       }        }
   
       return new Uri("https://{$this->shop}");        return new Uri("https://{$this->shop}");
   }    }
   
   /**    /**
    * Gets the authentication URL for Shopify to allow the user to accept the app (for public apps).     * Gets the authentication URL for Shopify to allow the user to accept the app (for public apps).
    *     *
    * @param string|array $scopes      The API scopes as a comma seperated string or array     * @param string|array $scopes      The API scopes as a comma seperated string or array
    * @param string       $redirectUri The valid redirect URI for after acceptance of the permissions.     * @param string       $redirectUri The valid redirect URI for after acceptance of the permissions.
    *                                  It must match the redirect_uri in your app settings.     *                                  It must match the redirect_uri in your app settings.
    * @param string|null  $mode        The API access mode, offline or per-user.     * @param string|null  $mode        The API access mode, offline or per-user.
    *     *
    * @return string Formatted URL     * @return string Formatted URL
    */      */ 
   public function getAuthUrl($scopes, string $redirectUri, string $mode = 'offline')    public function getAuthUrl($scopes, string $redirectUri, string $mode = 'offline')
   {    {
       if ($this->apiKey === null) {        if ($this->apiKey === null) {
           throw new Exception('API key is missing');            throw new Exception('API key is missing');
       }        }
   
       if (is_array($scopes)) {        if (is_array($scopes)) {
           $scopes = implode(',', $scopes);            $scopes = implode(',', $scopes);
       }        }
   
       $query = [        $query = [
           'client_id'    => $this->apiKey,            'client_id'    => $this->apiKey,
           'scope'        => $scopes,            'scope'        => $scopes,
           'redirect_uri' => $redirectUri,            'redirect_uri' => $redirectUri,
       ];        ];
   
       if ($mode !== null && $mode !== 'offline') {        if ($mode !== null && $mode !== 'offline') {
           $query['grant_options'] = [$mode];            $query['grant_options'] = [$mode];
       }        }
   
       return (string) $this->getBaseUri()        return (string) $this->getBaseUri()
           ->withPath('/admin/oauth/authorize')            ->withPath('/admin/oauth/authorize')
           ->withQuery(            ->withQuery(
               preg_replace('/\%5B\d+\%5D/', '%5B%5D', http_build_query($query))                preg_replace('/\%5B\d+\%5D/', '%5B%5D', http_build_query($query))
           );            );
   }    }
   
   /**    /**
    * Verify the request is from Shopify using the HMAC signature (for public apps).     * Verify the request is from Shopify using the HMAC signature (for public apps).
    *     *
    * @param array $params The request parameters (ex. $_GET)     * @param array $params The request parameters (ex. $_GET)
    *     *
    * @return bool If the HMAC is validated     * @return bool If the HMAC is validated
    */      */ 
   public function verifyRequest(array $params)    public function verifyRequest(array $params)
   {    {
       if ($this->apiSecret === null) {        if ($this->apiSecret === null) {
           // Secret is required            // Secret is required
           throw new Exception('API secret is missing');            throw new Exception('API secret is missing');
       }        }
   
       // Ensure shop, timestamp, and HMAC are in the params        // Ensure shop, timestamp, and HMAC are in the params
       if (array_key_exists('shop', $params)        if (array_key_exists('shop', $params)
           && array_key_exists('timestamp', $params)            && array_key_exists('timestamp', $params)
           && array_key_exists('hmac', $params)            && array_key_exists('hmac', $params)
       ) {        ) {
           // Grab the HMAC, remove it from the params, then sort the params for hashing            // Grab the HMAC, remove it from the params, then sort the params for hashing
           $hmac = $params['hmac'];            $hmac = $params['hmac'];
           unset($params['hmac']);            unset($params['hmac']);
           ksort($params);            ksort($params);
   
           // Encode and hash the params (without HMAC), add the API secret, and compare to the HMAC from params            // Encode and hash the params (without HMAC), add the API secret, and compare to the HMAC from params
           return $hmac === hash_hmac('sha256', urldecode(http_build_query($params)), $this->apiSecret);            return $hmac === hash_hmac('sha256', urldecode(http_build_query($params)), $this->apiSecret);
       }        }
   
       // Not valid        // Not valid
       return false;        return false;
   }    }
   
   /**    /**
    * Gets the access object from a "code" supplied by Shopify request after successfull authentication (for public apps).     * Gets the access object from a "code" supplied by Shopify request after successfull authentication (for public apps).
    *     *
    * @param string $code The code from Shopify     * @param string $code The code from Shopify
    *     *
    * @throws \Exception When API secret is missing     * @throws \Exception When API secret is missing
    *     *
    * @return array The access object     * @return array The access object
    */      */ 
   public function requestAccess(string $code)    public function requestAccess(string $code)
   {    {
       if ($this->apiSecret === null || $this->apiKey === null) {        if ($this->apiSecret === null || $this->apiKey === null) {
           // Key and secret required            // Key and secret required
           throw new Exception('API key or secret is missing');            throw new Exception('API key or secret is missing');
       }        }
   
       // Do a JSON POST request to grab the access token        // Do a JSON POST request to grab the access token
       $request = $this->client->request(        $request = $this->client->request(
           'POST',            'POST',
           $this->getBaseUri()->withPath('/admin/oauth/access_token'),            $this->getBaseUri()->withPath('/admin/oauth/access_token'),
           [            [
               'json' => [                'json' => [
                   'client_id'     => $this->apiKey,                    'client_id'     => $this->apiKey,
                   'client_secret' => $this->apiSecret,                    'client_secret' => $this->apiSecret,
                   'code'          => $code,                    'code'          => $code,
               ],                ],
           ]            ]
       );        );
   
       // Decode the response body        // Decode the response body
       $body = json_decode($request->getBody());        $body = json_decode($request->getBody());
   
       $this->log('RequestAccess response: '.json_encode($body));        $this->log('RequestAccess response: '.json_encode($body));
   
       return $body;        return $body;
   }    }
   
   /**    /**
    * Gets the access token from a "code" supplied by Shopify request after successfull authentication (for public apps).     * Gets the access token from a "code" supplied by Shopify request after successfull authentication (for public apps).
    *     *
    * @param string $code The code from Shopify     * @param string $code The code from Shopify
    *     *
    * @return string The access token     * @return string The access token
    */      */ 
   public function requestAccessToken(string $code)    public function requestAccessToken(string $code)
   {    {
       return $this->requestAccess($code)->access_token;        return $this->requestAccess($code)->access_token;
   }    }
   
   /**    /**
    * Gets the access object from a "code" and sets it to the instance (for public apps).     * Gets the access object from a "code" and sets it to the instance (for public apps).
    *     *
    * @param string $code The code from Shopify     * @param string $code The code from Shopify
    *     *
    * @return void     * @return void
    */      */ 
   public function requestAndSetAccess(string $code)    public function requestAndSetAccess(string $code)
   {    {
       $access = $this->requestAccess($code);        $access = $this->requestAccess($code);
   
       // Set the access token        // Set the access token
       $this->setAccessToken($access->access_token);        $this->setAccessToken($access->access_token);
   
       if (property_exists($access, 'associated_user')) {        if (property_exists($access, 'associated_user')) {
           // Set the user if applicable            // Set the user if applicable
           $this->setUser($access->associated_user);            $this->setUser($access->associated_user);
           $this->log('User access: '.json_encode($access->associated_user));            $this->log('User access: '.json_encode($access->associated_user));
       }        }
   }    }
   
   /**    /**
    * Alias for REST method for backwards compatibility.     * Alias for REST method for backwards compatibility.
    *     *
    * @see rest     * @see rest
    */      */ 
   public function request()    public function request()
   {    {
       return call_user_func_array([$this, 'rest'], func_get_args());        return call_user_func_array([$this, 'rest'], func_get_args());
   }    }
   
   /**    /**
    * Returns the current API call limits.     * Returns the current API call limits.
    *     *
    * @param string|null $key The key to grab (left, made, limit, etc)     * @param string|null $key The key to grab (left, made, limit, etc)
    *     *
    * @throws \Exception When attempting to grab a key that doesn't exist     * @throws \Exception When attempting to grab a key that doesn't exist
    *     *
    * @return array An array of the Guzzle response, and JSON-decoded body     * @return array An array of the Guzzle response, and JSON-decoded body
    */      */ 
   public function getApiCalls(string $type = 'rest', string $key = null)    public function getApiCalls(string $type = 'rest', string $key = null)
   {    {
       if ($key) {        if ($key) {
           $keys = array_keys($this->apiCallLimits[$type]);            $keys = array_keys($this->apiCallLimits[$type]);
           if (!in_array($key, $keys)) {            if (!in_array($key, $keys)) {
               // No key like that in array                // No key like that in array
               throw new Exception('Invalid API call limit key. Valid keys are: '.implode(', ', $keys));                throw new Exception('Invalid API call limit key. Valid keys are: '.implode(', ', $keys));
           }            }
   
           // Return the key value requested            // Return the key value requested
           return $this->apiCallLimits[$type][$key];            return $this->apiCallLimits[$type][$key];
       }        }
   
       // Return all the values        // Return all the values
       return $this->apiCallLimits[$type];        return $this->apiCallLimits[$type];
   }    }
   
   /**    /**
    * Runs a request to the Shopify API.     * Runs a request to the Shopify API.
    *     *
    * @param string $query     The GraphQL query     * @param string $query     The GraphQL query
    * @param array  $variables The optional variables for the query     * @param array  $variables The optional variables for the query
    *     *
    * @throws \Exception When missing api password is missing for private apps     * @throws \Exception When missing api password is missing for private apps
    * @throws \Exception When missing access key is missing for public apps     * @throws \Exception When missing access key is missing for public apps
    *     *
    * @return object An Object of the Guzzle response, and JSON-decoded body     * @return object An Object of the Guzzle response, and JSON-decoded body
    */      */ 
   public function graph(string $query, array $variables = [])    public function graph(string $query, array $variables = [])
   {    {
       // Build the request        // Build the request
       $request = ['query' => $query];        $request = ['query' => $query];
       if (count($variables) > 0) {        if (count($variables) > 0) {
           $request['variables'] = $variables;            $request['variables'] = $variables;
       }        }
   
       // Update the timestamp of the request        // Update the timestamp of the request
       $tmpTimestamp = $this->requestTimestamp;        $tmpTimestamp = $this->requestTimestamp;
       $this->requestTimestamp = microtime(true);        $this->requestTimestamp = microtime(true);
   
       // Create the request, pass the access token and optional parameters        // Create the request, pass the access token and optional parameters
       $req = json_encode($request);        $req = json_encode($request);
       $response = $this->client->request(        $response = $this->client->request(
           'POST',            'POST',
           $this->getBaseUri()->withPath(            $this->getBaseUri()->withPath(
               $this->versionPath('/admin/api/graphql.json')                $this->versionPath('/admin/api/graphql.json')
           ),            ),
           ['body' => $req]            ['body' => $req]
       );        );
       $this->log("Graph request: {$req}");        $this->log("Graph request: {$req}");
   
       // Grab the data result and extensions        // Grab the data result and extensions
       $body = $this->jsonDecode($response->getBody());        $body = $this->jsonDecode($response->getBody());
       if (property_exists($body, 'extensions') && property_exists($body->extensions, 'cost')) {        if (property_exists($body, 'extensions') && property_exists($body->extensions, 'cost')) {
           // Update the API call information            // Update the API call information
           $calls = $body->extensions->cost;            $calls = $body->extensions->cost;
           $this->apiCallLimits['graph'] = [            $this->apiCallLimits['graph'] = [
               'left'          => (int) $calls->throttleStatus->currentlyAvailable,                'left'          => (int) $calls->throttleStatus->currentlyAvailable,
               'made'          => (int) ($calls->throttleStatus->maximumAvailable - $calls->throttleStatus->currentlyAvailable),                'made'          => (int) ($calls->throttleStatus->maximumAvailable - $calls->throttleStatus->currentlyAvailable),
               'limit'         => (int) $calls->throttleStatus->maximumAvailable,                'limit'         => (int) $calls->throttleStatus->maximumAvailable,
               'restoreRate'   => (int) $calls->throttleStatus->restoreRate,                'restoreRate'   => (int) $calls->throttleStatus->restoreRate,
               'requestedCost' => (int) $calls->requestedQueryCost,                'requestedCost' => (int) $calls->requestedQueryCost,
               'actualCost'    => (int) $calls->actualQueryCost,                'actualCost'    => (int) $calls->actualQueryCost,
           ];            ];
       }        }
   
       $this->log('Graph response: '.json_encode(property_exists($body, 'errors') ? $body->errors : $body->data));        $this->log('Graph response: '.json_encode(property_exists($body, 'errors') ? $body->errors : $body->data));
   
       // Return Guzzle response and JSON-decoded body        // Return Guzzle response and JSON-decoded body
       return (object) [        return (object) [
           'response'   => $response,            'response'   => $response,
           'body'       => property_exists($body, 'errors') ? $body->errors : $body->data,            'body'       => property_exists($body, 'errors') ? $body->errors : $body->data,
           'errors'     => property_exists($body, 'errors'),            'errors'     => property_exists($body, 'errors'),
           'timestamps' => [$tmpTimestamp, $this->requestTimestamp],            'timestamps' => [$tmpTimestamp, $this->requestTimestamp],
       ];        ];
   }    }
   
   /**    /**
    * Runs a request to the Shopify API.     * Runs a request to the Shopify API.
    *     *
    * @param string     $type   The type of request... GET, POST, PUT, DELETE     * @param string     $type   The type of request... GET, POST, PUT, DELETE
    * @param string     $path   The Shopify API path... /admin/xxxx/xxxx.json     * @param string     $path   The Shopify API path... /admin/xxxx/xxxx.json
    * @param array|null $params Optional parameters to send with the request     * @param array|null $params Optional parameters to send with the request
    *     *
    * @throws Exception     * @throws Exception
    *     *
    * @return object An Object of the Guzzle response, and JSON-decoded body     * @return object An Object of the Guzzle response, and JSON-decoded body
    */      */ 
   public function rest(string $type, string $path, array $params = null)    public function rest(string $type, string $path, array $params = null)
   {    {
       // Check the rate limit before firing the request        // Check the rate limit before firing the request
       if ($this->isRateLimitingEnabled() && $this->requestTimestamp) {        if ($this->isRateLimitingEnabled() && $this->requestTimestamp) {
           // Calculate in milliseconds the duration the API call took            // Calculate in milliseconds the duration the API call took
           $duration = round(microtime(true) - $this->requestTimestamp, 3) * 1000;            $duration = round(microtime(true) - $this->requestTimestamp, 3) * 1000;
           $waitTime = ($this->rateLimitCycle - $duration) + $this->rateLimitCycleBuffer;            $waitTime = ($this->rateLimitCycle - $duration) + $this->rateLimitCycleBuffer;
   
           if ($waitTime > 0) {            if ($waitTime > 0) {
               // Do the sleep for X mircoseconds (convert from milliseconds)                // Do the sleep for X mircoseconds (convert from milliseconds)
               $this->log('Rest rate limit hit');                $this->log('Rest rate limit hit');
               usleep($waitTime * 1000);                usleep($waitTime * 1000);
           }            }
       }        }
   
       // Update the timestamp of the request        // Update the timestamp of the request
       $tmpTimestamp = $this->requestTimestamp;        $tmpTimestamp = $this->requestTimestamp;
       $this->requestTimestamp = microtime(true);        $this->requestTimestamp = microtime(true);
   
       $errors = false;        $errors = false;
       $response = null;        $response = null;
       $body = null;        $body = null;
   
       try {        try {
           // Build URI and try the request            // Build URI and try the request
           $uri = $this->getBaseUri()->withPath($this->versionPath($path));            $uri = $this->getBaseUri()->withPath($this->versionPath($path));
   
           // Build the request parameters for Guzzle            // Build the request parameters for Guzzle
           $guzzleParams = [];            $guzzleParams = [];
           if ($params !== null) {            if ($params !== null) {
               $guzzleParams[strtoupper($type) === 'GET' ? 'query' : 'json'] = $params;                $guzzleParams[strtoupper($type) === 'GET' ? 'query' : 'json'] = $params;
           }            }
   
           $this->log("[{$uri}:{$type}] Request Params: ".json_encode($params));            $this->log("[{$uri}:{$type}] Request Params: ".json_encode($params));
   
           // Set the response            // Set the response
           $response = $this->client->request($type, $uri, $guzzleParams);            $response = $this->client->request($type, $uri, $guzzleParams);
           $body = $response->getBody();            $body = $response->getBody();
       } catch (Exception $e) {        } catch (Exception $e) {
           if ($e instanceof ClientException || $e instanceof ServerException) {            if ($e instanceof ClientException || $e instanceof ServerException) {
               // 400 or 500 level error, set the response                // 400 or 500 level error, set the response
               $response = $e->getResponse();                $response = $e->getResponse();
               $body = $response->getBody();                $body = $response->getBody();
   
               // Build the error object                // Build the error object
               $errors = (object) [                $errors = (object) [
                   'status'    => $response->getStatusCode(),                    'status'    => $response->getStatusCode(),
                   'body'      => $this->jsonDecode($body),                    'body'      => $this->jsonDecode($body),
                   'exception' => $e,                    'exception' => $e,
               ];                ];
   
               $this->log("[{$uri}:{$type}] {$response->getStatusCode()} Error: {$body}");                $this->log("[{$uri}:{$type}] {$response->getStatusCode()} Error: {$body}");
           } else {            } else {
               // Else, rethrow                // Else, rethrow
               throw $e;                throw $e;
           }            }
       }        }
   
       // Grab the API call limit header returned from Shopify        // Grab the API call limit header returned from Shopify
       $callLimitHeader = $response->getHeader('http_x_shopify_shop_api_call_limit');        $callLimitHeader = $response->getHeader('http_x_shopify_shop_api_call_limit');
       if ($callLimitHeader) {        if ($callLimitHeader) {
           $calls = explode('/', $callLimitHeader[0]);            $calls = explode('/', $callLimitHeader[0]);
           $this->apiCallLimits['rest'] = [            $this->apiCallLimits['rest'] = [
               'left'  => (int) $calls[1] - $calls[0],                'left'  => (int) $calls[1] - $calls[0],
               'made'  => (int) $calls[0],                'made'  => (int) $calls[0],
               'limit' => (int) $calls[1],                'limit' => (int) $calls[1],
           ];            ];
       }        }
   
       $this->log("[{$uri}:{$type}] {$response->getStatusCode()}: ".json_encode($errors ? $body->getContents() : $body));        $this->log("[{$uri}:{$type}] {$response->getStatusCode()}: ".json_encode($errors ? $body->getContents() : $body));
.          
         if(!empty($errors) && empty($body->getContents())){ 
              
             if($errors->status == '429'){ 
                 Log::info("Api rate limit exceeds"); 
                 if ($this->retryCall <=3){ 
                     $this->retryCall++; 
                     Log::info("Retrying the API due to rate limit exceeds"); 
                     return $this->rest($type, $path, $params); 
                 }  
             } 
         } 
       // Return Guzzle response and JSON-decoded body        // Return Guzzle response and JSON-decoded body
       return (object) [        return (object) [
           'response'   => $response,            'response'   => $response,
           'errors'     => $errors,            'errors'     => $errors,
           'body'       => $errors ? $body->getContents() : $this->jsonDecode($body),            'body'       => $errors ? $body->getContents() : $this->jsonDecode($body),
           'timestamps' => [$tmpTimestamp, $this->requestTimestamp],            'timestamps' => [$tmpTimestamp, $this->requestTimestamp],
       ];        ];
   }    }
   
   /**    /**
    * Ensures we have the proper request for private and public calls.     * Ensures we have the proper request for private and public calls.
    * Also modifies issues with redirects.     * Also modifies issues with redirects.
    *     *
    * @param Request $request     * @param Request $request
    *     *
    * @return void     * @return void
    */      */ 
   public function authRequest(Request $request)    public function authRequest(Request $request)
   {    {
       // Get the request URI        // Get the request URI
       $uri = $request->getUri();        $uri = $request->getUri();
   
       if ($this->isAuthableRequest((string) $uri)) {        if ($this->isAuthableRequest((string) $uri)) {
           if ($this->isRestRequest((string) $uri)) {            if ($this->isRestRequest((string) $uri)) {
               // Checks for REST                // Checks for REST
               if ($this->private && ($this->apiKey === null || $this->apiPassword === null)) {                if ($this->private && ($this->apiKey === null || $this->apiPassword === null)) {
                   // Key and password are required for private API calls                    // Key and password are required for private API calls
                   throw new Exception('API key and password required for private Shopify REST calls');                    throw new Exception('API key and password required for private Shopify REST calls');
               }                }
   
               // Private: Add auth for REST calls                // Private: Add auth for REST calls
               if ($this->private) {                if ($this->private) {
                   // Add the basic auth header                    // Add the basic auth header
                   return $request->withHeader(                    return $request->withHeader(
                       'Authorization',                        'Authorization',
                       'Basic '.base64_encode("{$this->apiKey}:{$this->apiPassword}")                        'Basic '.base64_encode("{$this->apiKey}:{$this->apiPassword}")
                   );                    );
               }                }
   
               // Public: Add the token header                // Public: Add the token header
               return $request->withHeader(                return $request->withHeader(
                   'X-Shopify-Access-Token',                    'X-Shopify-Access-Token',
                   $this->accessToken                    $this->accessToken
               );                );
           } else {            } else {
               // Checks for Graph                // Checks for Graph
               if ($this->private && ($this->apiPassword === null && $this->accessToken === null)) {                if ($this->private && ($this->apiPassword === null && $this->accessToken === null)) {
                   // Private apps need password for use as access token                    // Private apps need password for use as access token
                   throw new Exception('API password/access token required for private Shopify GraphQL calls');                    throw new Exception('API password/access token required for private Shopify GraphQL calls');
               } elseif (!$this->private && $this->accessToken === null) {                } elseif (!$this->private && $this->accessToken === null) {
                   // Need access token for public calls                    // Need access token for public calls
                   throw new Exception('Access token required for public Shopify GraphQL calls');                    throw new Exception('Access token required for public Shopify GraphQL calls');
               }                }
   
               // Public/Private: Add the token header                // Public/Private: Add the token header
               return $request->withHeader(                return $request->withHeader(
                   'X-Shopify-Access-Token',                    'X-Shopify-Access-Token',
                   $this->apiPassword ?? $this->accessToken                    $this->apiPassword ?? $this->accessToken
               );                );
           }            }
       }        }
   
       return $request;        return $request;
   }    }
   
   /**    /**
    * Sets a logger instance on the object.     * Sets a logger instance on the object.
    *     *
    * @param LoggerInterface $logger     * @param LoggerInterface $logger
    *     *
    * @return void     * @return void
    */      */ 
   public function setLogger(LoggerInterface $logger)    public function setLogger(LoggerInterface $logger)
   {    {
       $this->logger = $logger;        $this->logger = $logger;
   }    }
   
   /**    /**
    * Log a message to the logger.     * Log a message to the logger.
    *     *
    * @param string $msg   The message to send.     * @param string $msg   The message to send.
    * @param int    $level The level of message.     * @param int    $level The level of message.
    *     *
    * @return bool     * @return bool
    */      */ 
   public function log(string $msg, string $level = LogLevel::DEBUG)    public function log(string $msg, string $level = LogLevel::DEBUG)
   {    {
       if ($this->logger === null) {        if ($this->logger === null) {
           // No logger, do nothing            // No logger, do nothing
           return false;            return false;
       }        }
   
       // Call the logger by level and pass the message        // Call the logger by level and pass the message
       call_user_func([$this->logger, $level], self::LOG_KEY.' '.$msg);        call_user_func([$this->logger, $level], self::LOG_KEY.' '.$msg);
   
       return true;        return true;
   }    }
   
   /**    /**
    * Decodes the JSON body.     * Decodes the JSON body.
    *     *
    * @param string $json The JSON body     * @param string $json The JSON body
    *     *
    * @return object The decoded JSON     * @return object The decoded JSON
    */      */ 
   protected function jsonDecode($json)    protected function jsonDecode($json)
   {    {
       // From firebase/php-jwt        // From firebase/php-jwt
       if (!(defined('JSON_C_VERSION') && PHP_INT_SIZE > 4)) {        if (!(defined('JSON_C_VERSION') && PHP_INT_SIZE > 4)) {
           /**            /**
            * In PHP >=5.4.0, json_decode() accepts an options parameter, that allows you             * In PHP >=5.4.0, json_decode() accepts an options parameter, that allows you
            * to specify that large ints (like Steam Transaction IDs) should be treated as             * to specify that large ints (like Steam Transaction IDs) should be treated as
            * strings, rather than the PHP default behaviour of converting them to floats.             * strings, rather than the PHP default behaviour of converting them to floats.
            */              */ 
           $obj = json_decode($json, false, 512, JSON_BIGINT_AS_STRING);            $obj = json_decode($json, false, 512, JSON_BIGINT_AS_STRING);
       } else {        } else {
           // @codeCoverageIgnoreStart            // @codeCoverageIgnoreStart
           /**            /**
            * Not all servers will support that, however, so for older versions we must             * Not all servers will support that, however, so for older versions we must
            * manually detect large ints in the JSON string and quote them (thus converting             * manually detect large ints in the JSON string and quote them (thus converting
            * them to strings) before decoding, hence the preg_replace() call.             * them to strings) before decoding, hence the preg_replace() call.
            * Currently not sure how to test this so I ignored it for now.             * Currently not sure how to test this so I ignored it for now.
            */              */ 
           $maxIntLength = strlen((string) PHP_INT_MAX) - 1;            $maxIntLength = strlen((string) PHP_INT_MAX) - 1;
           $jsonWithoutBigints = preg_replace('/:\s*(-?\d{'.$maxIntLength.',})/', ': "$1"', $json);            $jsonWithoutBigints = preg_replace('/:\s*(-?\d{'.$maxIntLength.',})/', ': "$1"', $json);
           $obj = json_decode($jsonWithoutBigints);            $obj = json_decode($jsonWithoutBigints);
           // @codeCoverageIgnoreEnd            // @codeCoverageIgnoreEnd
       }        }
   
       return $obj;        return $obj;
   }    }
   
   /**    /**
    * Determines if the request is to Graph API.     * Determines if the request is to Graph API.
    *     *
    * @param string $uri     * @param string $uri
    *     *
    * @return bool     * @return bool
    */      */ 
   protected function isGraphRequest(string $uri)    protected function isGraphRequest(string $uri)
   {    {
       return strpos($uri, 'graphql.json') !== false;        return strpos($uri, 'graphql.json') !== false;
   }    }
   
   /**    /**
    * Determines if the request is to REST API.     * Determines if the request is to REST API.
    *     *
    * @param string $uri     * @param string $uri
    *     *
    * @return bool     * @return bool
    */      */ 
   protected function isRestRequest(string $uri)    protected function isRestRequest(string $uri)
   {    {
       return $this->isGraphRequest($uri) === false;        return $this->isGraphRequest($uri) === false;
   }    }
   
   /**    /**
    * Determines if the request requires auth headers.     * Determines if the request requires auth headers.
    *     *
    * @param string $uri     * @param string $uri
    *     *
    * @return bool     * @return bool
    */      */ 
   protected function isAuthableRequest(string $uri)    protected function isAuthableRequest(string $uri)
   {    {
       return preg_match('/\/admin\/oauth\/(authorize|access_token|access_scopes)/', $uri) === 0;        return preg_match('/\/admin\/oauth\/(authorize|access_token|access_scopes)/', $uri) === 0;
   }    }
   
   /**    /**
    * Versions the API call with the set version.     * Versions the API call with the set version.
    *     *
    * @param string $uri     * @param string $uri
    *     *
    * @return string     * @return string
    */      */ 
   protected function versionPath(string $uri)    protected function versionPath(string $uri)
   {    {
       if ($this->version === null || preg_match(self::VERSION_PATTERN, $uri) || !$this->isAuthableRequest($uri)) {        if ($this->version === null || preg_match(self::VERSION_PATTERN, $uri) || !$this->isAuthableRequest($uri)) {
           // No version set, or already versioned... nothing to do            // No version set, or already versioned... nothing to do
           return $uri;            return $uri;
       }        }
   
       // Graph request        // Graph request
       if ($this->isGraphRequest($uri)) {        if ($this->isGraphRequest($uri)) {
           return str_replace('/admin/api', "/admin/api/{$this->version}", $uri);            return str_replace('/admin/api', "/admin/api/{$this->version}", $uri);
       }        }
   
       // REST request        // REST request
       return preg_replace('/\/admin(\/api)?\//', "/admin/api/{$this->version}/", $uri);        return preg_replace('/\/admin(\/api)?\//', "/admin/api/{$this->version}/", $uri);
   }    }
} }