Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
58.62% covered (warning)
58.62%
17 / 29
CRAP
45.36% covered (danger)
45.36%
44 / 97
Shopify\Client
0.00% covered (danger)
0.00%
0 / 1
58.62% covered (warning)
58.62%
17 / 29
440.66
45.36% covered (danger)
45.36%
44 / 97
 call
0.00% covered (danger)
0.00%
0 / 1
90
0.00% covered (danger)
0.00%
0 / 22
 callGraphql
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 11
 request
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 8
 getPrevPage
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 hasPrevPage
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 getNextPage
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 hasNextPage
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 parseLinkString
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 3
 getHttpMethods
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 setShop
0.00% covered (danger)
0.00%
0 / 1
2.26
60.00% covered (warning)
60.00%
3 / 5
 getApiVersion
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 setApiVersion
0.00% covered (danger)
0.00%
0 / 1
3.14
75.00% covered (warning)
75.00%
3 / 4
 getShop
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 setRestApiUrl
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
10 / 10
 getRestApiUrl
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 setGraphqlApiUrl
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
3 / 3
 getGraphqlApiUrl
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 setRestApiHeaders
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
4 / 4
 getRestApiHeaders
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 setGraphqlApiHeaders
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
4 / 4
 getGraphqlApiHeaders
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 setApiKey
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 getApiKey
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 setApiPassword
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 getApiPassword
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 setApiSecretKey
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 getApiSecretKey
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 setApiParams
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 getApiParams
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
<?php
namespace Shopify;
use GuzzleHttp\Exception\RequestException;
use Shopify\Common\ClientInterface;
use Shopify\Exception\ApiException;
/**
 * Class Client
 * @package Shopify
 */
class Client implements ClientInterface
{
    /**
     * define constant for current Shopify api version
     */
    const SHOPIFY_API_VERSION = '2020-07';
    /**
     * define rest api call
     */
    const REST_API = 'rest';
    /**
     * define private app
     */
    const PRIVATE_APP = 'private';
    /**
     * header parameter of shopify access token
     */
    const SHOPIFY_ACCESS_TOKEN = 'X-Shopify-Access-Token';
    /**
     * denine response header pagination string
     */
    const PAGINATION_STRING = 'Link';
    /**
     * response header parameter of shopify api limit
     */
    const API_CALL_RATE_LIMIT_HEADER = 'http_x_shopify_shop_api_call_limit';
    /**
     * define graphQL api call
     */
    const GRAPHQL = 'graphql';
    /**
     * Shopify graphql base url
     * @var string
     */
    protected $graphql_api_url = "https://{shopify_domain}/admin/api/{version}/graphql.json";
    /**
     * rest api url for custom/public app
     * @var string
     */
    protected $rest_api_url = 'https://{shopify_domain}/admin/api/{version}/{resource}.json';
    /**
     * Shopify domain name
     * @var string
     */
    protected $shop;
    /**
     * Shopify api key
     * @var string
     */
    protected $api_key;
    /**
     * Shopify password for private app
     * @var string
     */
    protected $password;
    /**
     * Shopify shared secret key for private app
     * @var string
     */
    protected $api_secret_key;
    /**
     * access token for public app
     * @var string
     */
    protected $access_token;
    /**
     * array('version')
     * @var array
     */
    protected $api_params;
    /**
     * Shopify api call url
     * @var array
     */
    protected $base_urls;
    /**
     * get api header array according to private and public app for rest api
     * @var array
     */
    protected $restApiRequestHeaders;
    /**
     * get api header array according to private and public app for graphql api
     * @var array
     */
    protected $graphqlApiRequestHeaders;
    /**
     * Shopify api version
     * @var string
     */
    protected $api_version;
    /**
     * get response header
     * @var string
     */
    protected $next_page;
    /**
     * get response header
     * @var string
     */
    protected $prev_page;
    /**
     * static variable to api is going to reach
     * @var bool
     */
    protected static $wait_next_api_call = false;
    /**
     * prepare data for rest api request
     * @param $method
     * @param $path
     * @param array $params
     * @return array
     * @throws ApiException
     * @throws ClientException
     */
    public function call($method, $path , array $params = [])
    {
        $url = $this->getRestApiUrl();
        $options = [];
        $allowed_http_methods = $this->getHttpMethods();
        if(!in_array($method, $allowed_http_methods)){
            throw new ApiException(implode(",",$allowed_http_methods)." http methods are allowed.",0);
        }
        if(is_array($this->getRestApiHeaders()) && count($this->getRestApiHeaders())) {
            $options['headers'] = $this->getRestApiHeaders();
        }
        $url=strtr($url, [
            '{resource}' => $path,
        ]);
        if(in_array($method,['GET','DELETE'])) {
            $options['query'] = $params;
        }else {
            $options['json'] = $params;
        }
        if(self::$wait_next_api_call)
        {
            usleep(1000000 * rand(3, 6));
        }
        $http_response = $this->request($method,$url,$options);
        if (strtoupper($method) === 'GET'  && $http_response->getHeaderLine(self::PAGINATION_STRING)) {
            $this->next_page = $this->parseLinkString($http_response->getHeaderLine(self::PAGINATION_STRING),'next');
            $this->prev_page = $this->parseLinkString($http_response->getHeaderLine(self::PAGINATION_STRING),'previous');
        }
        if($http_response->getHeaderLine(self::API_CALL_RATE_LIMIT_HEADER)) {
            list($api_call_requested, $api_call_Limit) = explode('/', $http_response->getHeaderLine(self::API_CALL_RATE_LIMIT_HEADER));
            static::$wait_next_api_call = $api_call_requested / $api_call_Limit >= 0.8;
        }
        return \GuzzleHttp\json_decode($http_response->getBody()->getContents(),true);
    }
    /**
     * prepare data for graphql api request
     * @param string $query
     * @return mixed|void
     * @throws ApiException
     * @throws ClientException
     */
    public function callGraphql($query)
    {
        $url = $this->getGraphqlApiUrl();
        $options = [];
        if(is_array($this->getRestApiHeaders()) && count($this->getRestApiHeaders()))   {
            $options['headers'] = $this->getRestApiHeaders();
        }
        $options['body'] = $query;
        $http_response = $this->request('POST', $url, $options);
        $response = \GuzzleHttp\json_decode($http_response->getBody()->getContents(),true);
        if(isset($response['errors']))
        {
            $http_bad_request_code = 400;
            throw new ApiException(\GuzzleHttp\json_encode($response['errors']),$http_bad_request_code);
        }
        return $response;
    }
    /**
     * send http request
     * @param string $method
     * @param string $url
     * @param array $options
     * @return array|mixed
     * @throws ApiException
     */
    public function request($method,$url,array $options)
    {
        try
        {
            $client  = new \GuzzleHttp\Client();
            return $client->request($method, $url, $options);
        }
        catch (RequestException $e)
        {
            if(!empty($e->getResponse()->getBody()->getContents()))
            {
                $json_error = json_decode($e->getResponse()->getBody()->getContents(),true);
                $error_message = isset($json_error['errors'])?$json_error['errors']:\GuzzleHttp\json_encode($json_error);
            }
            else {
                $error_message = $e->getMessage();
            }
            throw new ApiException($error_message,$e->getCode());
        }
    }
    /**
     * get previous page_info for any resource(products/orders)
     * @return string
     */
    public function getPrevPage()
    {
        return $this->prev_page;
    }
    /**
     * check previous page_info for any resource(products/orders)
     * @return string
     */
    public function hasPrevPage()
    {
        return !empty($this->prev_page);
    }
    /**
     * get next page_info for any resource(products/orders)
     * @return string
     */
    public function getNextPage(){
        return $this->next_page;
    }
    /**
     * check next page_info for any resource(products/orders)
     * @return string
     */
    public function hasNextPage(){
        return !empty($this->next_page);
    }
    /**
     * parse header string for previous and next page_info
     * @param $pagination_string
     * @param $page_link
     * @return string
     */
    public function parseLinkString($pagination_string,$page_link)
    {
        $matches = [];
        preg_match("/<(.*page_info=([a-z0-9\-]+).*)>; rel=\"?{$page_link}\"?/i", $pagination_string, $matches);
        return isset($matches[2]) ? $matches[2] : NULL;
    }
    /**
     * return allowed http api methods
     * @return array
     */
    public function getHttpMethods()
    {
        return ['POST', 'PUT','GET', 'DELETE'];
    }
    /**
     * set shopify domain
     * @param $shop
     * Exception for invalid shop name
     * @throws ApiException
     */
    public function setShop($shop)
    {
        if (!preg_match('/^[a-zA-Z0-9\-]{3,100}\.myshopify\.(?:com|io)$/', $shop)) {
            throw new ApiException(
                'Shop name should be 3-100 letters, numbers, or hyphens eg mypetstore.myshopify.com',0
            );
        }
        $this->shop = $shop;
    }
    /**
     * return latest api version
     * @return string
     */
    public function getApiVersion()
    {
        return $this->api_version;
    }
    /**
     * set api version
     * @param api_version
     * Exception for valid value
     * @throws ApiException
     */
    public function setApiVersion($ap_params)
    {
        $this->api_version = !empty($ap_params['version'])?$ap_params['version']:self::SHOPIFY_API_VERSION;
        if (!preg_match('/^[0-9]{4}-[0-9]{2}$|^unstable$/', $this->api_version))
        {
            throw new ApiException('Api Version must be of YYYY-MM or unstable',0);
        }
    }
    /**
     * return Shopify domain
     * @return string
     */
    public function getShop()
    {
        return $this->shop;
    }
    /**
     * set rest api url
     * @param $rest_api_url
     * @param $app_type
     */
    public function setRestApiUrl($rest_api_url, $app_type = '')
    {
        if($app_type == self::PRIVATE_APP)
        {
            $this->rest_api_url = strtr($rest_api_url, [
                '{api_key}' => $this->api_key,
                '{password}' => $this->password,
                '{shopify_domain}' => $this->shop,
                '{version}' => $this->getApiVersion(),
            ]);
        }else{
            $this->rest_api_url = strtr($rest_api_url, [
                '{shopify_domain}' => $this->shop,
                '{version}' => $this->getApiVersion(),
            ]);
        }
    }
    /**
     * get rest api url app
     * @return string
     */
    public function getRestApiUrl(){
        return $this->rest_api_url;
    }
    /**
     * set graphql api url
     * @param $graphql_api_url
     */
    public function setGraphqlApiUrl($graphql_api_url)
    {
        $this->graphql_api_url = strtr($graphql_api_url, [
            '{shopify_domain}' => $this->shop, '{version}' => $this->getApiVersion()
        ]);
    }
    /**
     * get graphql api url
     * @return string
     */
    public function getGraphqlApiUrl(){
        return $this->graphql_api_url;
    }
    /**
     * set rest api headers
     * @return string
     */
    public function setRestApiHeaders($access_token = ''){
        $this->restApiRequestHeaders['Content-Type'] = "application/json";
        if($access_token){
            $this->restApiRequestHeaders[self::SHOPIFY_ACCESS_TOKEN] = $this->access_token;
        }
    }
    /**
     * get rest api headers
     * @return array
     */
    public function getRestApiHeaders(){
        return $this->restApiRequestHeaders;
    }
    /**
     * get graphql api url
     * @return string
     */
    public function setGraphqlApiHeaders($access_token){
        $this->graphqlApiRequestHeaders['Content-Type'] = "application/graphql";
        $this->graphqlApiRequestHeaders['X-GraphQL-Cost-Include-Fields'] = true;
        $this->graphqlApiRequestHeaders[self::SHOPIFY_ACCESS_TOKEN] = $access_token;
    }
    /**
     * get graphql api url
     * @return string
     */
    public function getGraphqlApiHeaders(){
        return $this->graphqlApiRequestHeaders;
    }
    /**
     * set api_key of public or private app
     *  @param $api_key
     */
    public function setApiKey($api_key){
        $this->api_key = $api_key;
    }
    /**
     * get api_key of public or private app
     * @return string
     */
    public function getApiKey(){
        return $this->api_key;
    }
    /**
     * set api_key of public or private app
     *  @param $api_password
     */
    public function setApiPassword($api_password){
        $this->password = $api_password;
    }
    /**
     * get api_password of private app
     * @return string
     */
    public function getApiPassword(){
        return $this->password;
    }
    /**
     * set api secret key for public app
     * @param $api_secret_key
     */
    public function setApiSecretKey($api_secret_key){
        $this->api_secret_key = $api_secret_key;
    }
    /**
     * get api secret key for public app
     * @return string
     */
    public function getApiSecretKey(){
        return $this->api_secret_key;
    }
    /**
     * set api_params of public or private app
     * @param  $api_params
     */
    public function setApiParams($api_params){
        $this->api_params = $api_params;
    }
    /**
     * get api_params of public or private app
     * @return string
     */
    public function getApiParams(){
        return $this->api_params;
    }
}