diff --git a/.gitignore b/.gitignore index 432b975..8f721c1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ vendor /nbproject/private/ -.idea/ \ No newline at end of file +.idea/ +.DS_Store \ No newline at end of file diff --git a/README.md b/README.md index 481989e..6a69c9f 100644 --- a/README.md +++ b/README.md @@ -14,12 +14,26 @@ composer require phpclassic/php-shopify PHPShopify uses curl extension for handling http calls. So you need to have the curl extension installed and enabled with PHP. >However if you prefer to use any other available package library for handling HTTP calls, you can easily do so by modifying 1 line in each of the `get()`, `post()`, `put()`, `delete()` methods in `PHPShopify\HttpRequestJson` class. +You can pass additional curl configuration to `ShopifySDK` +```php +$config = array( + 'ShopUrl' => 'yourshop.myshopify.com', + 'ApiKey' => '***YOUR-PRIVATE-API-KEY***', + 'Password' => '***YOUR-PRIVATE-API-PASSWORD***', + 'Curl' => array( + CURLOPT_TIMEOUT => 10, + CURLOPT_FOLLOWLOCATION => true + ) +); + +PHPShopify\ShopifySDK::config($config); +``` ## Usage You can use PHPShopify in a pretty simple object oriented way. #### Configure ShopifySDK -If you are using your own private API, provide the ApiKey and Password. +If you are using your own private API (except GraphQL), provide the ApiKey and Password. ```php $config = array( @@ -31,7 +45,8 @@ $config = array( PHPShopify\ShopifySDK::config($config); ``` -For Third party apps, use the permanent access token. +For Third party apps, use the permanent access token. +> For GraphQL, AccessToken is required. If you are using private API for GraphQL, use your password as AccessToken here. ```php $config = array( @@ -39,6 +54,17 @@ $config = array( 'AccessToken' => '***ACCESS-TOKEN-FOR-THIRD-PARTY-APP***', ); +PHPShopify\ShopifySDK::config($config); +``` +You can use specific Shopify API Version by adding in the config array + +```php +$config = array( + 'ShopUrl' => 'yourshop.myshopify.com', + 'AccessToken' => '***ACCESS-TOKEN-FOR-THIRD-PARTY-APP***', + 'ApiVersion' => '2022-07', +); + PHPShopify\ShopifySDK::config($config); ``` ##### How to get the permanent access token for a shop? @@ -171,7 +197,7 @@ $shopify->Order($orderID)->put($updateInfo); ```php $webHookID = 453487303; -$shopify->Webhook($webHookID)->delete()); +$shopify->Webhook($webHookID)->delete(); ``` @@ -302,7 +328,8 @@ Some resources are available directly, some resources are only available through - Blog -> Article -> [Metafield](https://help.shopify.com/api/reference/metafield) - Blog -> [Event](https://help.shopify.com/api/reference/event/) - Blog -> [Metafield](https://help.shopify.com/api/reference/metafield) -- [CarrierService](https://help.shopify.com/api/reference/carrierservice/) +- [CarrierService](https://help.shopify.com/api/reference/carrierservice/)- +- [Cart](https://shopify.dev/docs/themes/ajax-api/reference/cart) (read only) - [Collect](https://help.shopify.com/api/reference/collect/) - [Comment](https://help.shopify.com/api/reference/comment/) - Comment -> [Event](https://help.shopify.com/api/reference/event/) @@ -359,6 +386,8 @@ Some resources are available directly, some resources are only available through - [Shop](https://help.shopify.com/api/reference/shop) _(read only)_ - [SmartCollection](https://help.shopify.com/api/reference/smartcollection) - SmartCollection -> [Event](https://help.shopify.com/api/reference/event/) +- [ShopifyPayment](https://shopify.dev/docs/admin-api/rest/reference/shopify_payments/) +- ShopifyPayment -> [Dispute](https://shopify.dev/docs/admin-api/rest/reference/shopify_payments/dispute/) _(read only)_ - [Theme](https://help.shopify.com/api/reference/theme) - Theme -> [Asset](https://help.shopify.com/api/reference/asset/) - [User](https://help.shopify.com/api/reference/user) _(read only, Shopify Plus Only)_ @@ -483,6 +512,15 @@ The custom methods are specific to some resources which may not be available for - [current()](https://help.shopify.com/api/reference/user#current) Get the current logged-in user +### Shopify API features headers +To send `X-Shopify-Api-Features` headers while using the SDK, you can use the following: + +``` +$config['ShopifyApiFeatures'] = ['include-presentment-prices']; +$shopify = new PHPShopify\ShopifySDK($config); +``` + + ## Reference - [Shopify API Reference](https://help.shopify.com/api/reference/) diff --git a/lib/AccessScope.php b/lib/AccessScope.php new file mode 100644 index 0000000..0ce0c97 --- /dev/null +++ b/lib/AccessScope.php @@ -0,0 +1,36 @@ +getResourcePath() . '.json'; + } +} diff --git a/lib/ApplicationCredit.php b/lib/ApplicationCredit.php new file mode 100644 index 0000000..58f8551 --- /dev/null +++ b/lib/ApplicationCredit.php @@ -0,0 +1,26 @@ + + * Created at 8/18/16 9:50 AM UTC+06:00 + * + * @see https://help.shopify.com/api/reference/applicationcharge Shopify API Reference for ApplicationCharge + */ + +namespace PHPShopify; + + +class ApplicationCredit extends ShopifyResource +{ + /** + * @inheritDoc + */ + protected $resourceKey = 'application_credit'; + + /** + * @inheritDoc + */ + public $countEnabled = false; + + +} diff --git a/lib/AuthHelper.php b/lib/AuthHelper.php index e879964..de076b9 100644 --- a/lib/AuthHelper.php +++ b/lib/AuthHelper.php @@ -29,7 +29,10 @@ public static function getCurrentUrl() $protocol = 'http'; } - return "$protocol://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]"; + $url = $protocol . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; + $url = false !== ($qsPos = strpos($url, '?')) ? substr($url, 0, $qsPos) : $url; // remove query params + + return $url; } /** @@ -170,6 +173,10 @@ public static function getAccessToken() $response = HttpRequestJson::post($config['AdminUrl'] . 'oauth/access_token', $data); + if (CurlRequest::$lastHttpCode >= 400) { + throw new SdkException("The shop is invalid or the authorization code has already been used."); + } + return isset($response['access_token']) ? $response['access_token'] : null; } else { throw new SdkException("This request is not initiated from a valid shopify shop!"); diff --git a/lib/Balance.php b/lib/Balance.php new file mode 100644 index 0000000..4144c5c --- /dev/null +++ b/lib/Balance.php @@ -0,0 +1,51 @@ + + * @author Matthew Crigger + * + * @see https://help.shopify.com/en/api/reference/shopify_payments/balance Shopify API Reference for Shopify Payment Balance + */ + +namespace PHPShopify; + +/** + * -------------------------------------------------------------------------- + * ShopifyPayment -> Child Resources + * -------------------------------------------------------------------------- + * + * + */ +class Balance extends ShopifyResource +{ + /** + * @inheritDoc + */ + protected $resourceKey = 'balance'; + + /** + * Get the pluralized version of the resource key + * + * Normally its the same as $resourceKey appended with 's', when it's different, the specific resource class will override this function + * + * @return string + */ + protected function pluralizeKey() + { + return $this->resourceKey; + } + + /** + * If the resource is read only. (No POST / PUT / DELETE actions) + * + * @var boolean + */ + public $readOnly = true; + + /** + * @inheritDoc + */ + protected $childResource = array( + 'Transactions' + ); +} \ No newline at end of file diff --git a/lib/Batch.php b/lib/Batch.php new file mode 100644 index 0000000..49e125b --- /dev/null +++ b/lib/Batch.php @@ -0,0 +1,33 @@ + Batch action + * -------------------------------------------------------------------------- + * + */ + +class Batch extends ShopifyResource +{ + /** + * @inheritDoc + */ + protected $resourceKey = 'batch'; + + protected function getResourcePath() + { + return $this->resourceKey; + } + + protected function wrapData($dataArray, $dataKey = null) + { + return ['discount_codes' => $dataArray]; + } + +} diff --git a/lib/Cart.php b/lib/Cart.php new file mode 100644 index 0000000..15ad000 --- /dev/null +++ b/lib/Cart.php @@ -0,0 +1,24 @@ + + * Created at 8/19/16 2:59 PM UTC+06:00 + * + * @see https://help.shopify.com/api/reference/sales-channels/checkout Shopify API Reference for Checkout + */ + +namespace PHPShopify; + + +/** + * -------------------------------------------------------------------------- + * Order -> Child Resources + * -------------------------------------------------------------------------- + * @property-read ShippingRate $ShippingRate + * + * @method ShippingRate ShippingRate(integer $id = null) + * + + * -------------------------------------------------------------------------- + * Checkout -> Custom actions + * -------------------------------------------------------------------------- + * @method array complete() Completes Checkout without payment + * + */ +class Checkout extends ShopifyResource +{ + /** + * @inheritDoc + */ + protected $resourceKey = 'checkout'; + + /** + * @inheritDoc + */ + public $countEnabled = false; + + /** + * @inheritDoc + */ + protected $childResource = array ( + 'ShippingRate', + ); + /** + * @inheritDoc + */ + protected $customPostActions = array( + 'complete', + 'payments' + ); +} diff --git a/lib/Collection.php b/lib/Collection.php index 1d2c84a..0c21a40 100644 --- a/lib/Collection.php +++ b/lib/Collection.php @@ -8,8 +8,10 @@ * -------------------------------------------------------------------------- * * @property-read Product $Product + * @property-read Metafield $Metafield * * @method Product Product(integer $id = null) + * @method Metafield Metafield(integer $id = null) * * @see https://shopify.dev/docs/admin-api/rest/reference/products/collection * @@ -31,5 +33,6 @@ class Collection extends ShopifyResource */ protected $childResource = array( 'Product', + 'Metafield', ); -} \ No newline at end of file +} diff --git a/lib/CollectionListing.php b/lib/CollectionListing.php new file mode 100644 index 0000000..bc80f84 --- /dev/null +++ b/lib/CollectionListing.php @@ -0,0 +1,32 @@ + + * Created at: 6/2/18 1:38 PM UTC+06:00 + * + * @see https://help.shopify.com/api/reference/sales_channels/collectionlisting + */ + +namespace PHPShopify; +/** + * -------------------------------------------------------------------------- + * CollectionListing -> Custom actions + * -------------------------------------------------------------------------- + * @method array productIds() Sets the address as default for the customer + */ + +class CollectionListing extends ShopifyResource +{ + /** + * @inheritDoc + */ + protected $resourceKey = 'collection_listing'; + + /** + * @inheritDoc + */ + protected $customGetActions = array( + 'product_ids' => 'productIds', + ); + +} diff --git a/lib/CurlRequest.php b/lib/CurlRequest.php index 73567e1..648296d 100644 --- a/lib/CurlRequest.php +++ b/lib/CurlRequest.php @@ -35,6 +35,13 @@ class CurlRequest */ public static $lastHttpResponseHeaders = array(); + /** + * Curl additional configuration + * + * @var array + */ + protected static $config = array(); + /** * Initialize the curl resource * @@ -57,6 +64,10 @@ protected static function init($url, $httpHeaders = array()) curl_setopt($ch, CURLOPT_HEADER, true); curl_setopt($ch, CURLOPT_USERAGENT, 'PHPClassic/PHPShopify'); + foreach (self::$config as $option => $value) { + curl_setopt($ch, $option, $value); + } + $headers = array(); foreach ($httpHeaders as $key => $value) { $headers[] = "$key: $value"; @@ -139,6 +150,16 @@ public static function delete($url, $httpHeaders = array()) return self::processRequest($ch); } + /** + * Set curl additional configuration + * + * @param array $config + */ + public static function config($config = array()) + { + self::$config = $config; + } + /** * Execute a request, release the resource and return output * @@ -160,13 +181,22 @@ protected static function processRequest($ch) break; } - $limitHeader = explode('/', $response->getHeader('X-Shopify-Shop-Api-Call-Limit'), 2); + $apiCallLimit = $response->getHeader('X-Shopify-Shop-Api-Call-Limit'); + + if (!empty($apiCallLimit)) { + $limitHeader = explode('/', $apiCallLimit, 2); + if (isset($limitHeader[1]) && $limitHeader[0] < $limitHeader[1]) { + throw new ResourceRateLimitException($response->getBody()); + } + } + + $retryAfter = $response->getHeader('Retry-After'); - if (isset($limitHeader[1]) && $limitHeader[0] < $limitHeader[1]) { - throw new ResourceRateLimitException($response->getBody()); + if ($retryAfter === null) { + break; } - usleep(500000); + sleep((float)$retryAfter); } if (curl_errno($ch)) { diff --git a/lib/CurlResponse.php b/lib/CurlResponse.php index 02a9eec..b6922e9 100644 --- a/lib/CurlResponse.php +++ b/lib/CurlResponse.php @@ -53,6 +53,7 @@ public function getHeaders() */ public function getHeader($key) { + $key = strtolower($key); return isset($this->headers[$key]) ? $this->headers[$key] : null; } diff --git a/lib/Customer.php b/lib/Customer.php index a058624..e358ccc 100644 --- a/lib/Customer.php +++ b/lib/Customer.php @@ -22,7 +22,7 @@ * -------------------------------------------------------------------------- * Customer -> Custom actions * -------------------------------------------------------------------------- - * @method array search() Search for customers matching supplied query + * @method array search(string $query = '') Search for customers matching supplied query */ class Customer extends ShopifyResource { @@ -60,4 +60,21 @@ public function send_invite($customer_invite = array()) return $this->post($dataArray, $url, false); } -} \ No newline at end of file + + /** + * Create account_activation_link for customer. + * + * @param array $customer_id + * + * @return array + */ + public function account_activation_url($customer_id = 0) + { + if (!(int)$customer_id > 0) { + return false; + } + + $url = $this->generateUrl(array(), $customer_id.'/account_activation_url'); + return $this->post(array(), $url, false); + } +} diff --git a/lib/Dispute.php b/lib/Dispute.php new file mode 100644 index 0000000..e769872 --- /dev/null +++ b/lib/Dispute.php @@ -0,0 +1,30 @@ + + * Created at 01/06/2020 16:45 AM UTC+03:00 + * + * @see https://shopify.dev/docs/admin-api/rest/reference/shopify_payments/dispute Shopify API Reference for Dispute + */ + +namespace PHPShopify; + + +/** + * -------------------------------------------------------------------------- + * ShopifyPayment -> Child Resources + * -------------------------------------------------------------------------- + * @property-read ShopifyResource $DiscountCode + * + * @method ShopifyResource DiscountCode(integer $id = null) + * + */ +class Dispute extends ShopifyResource +{ + /** + * @inheritDoc + */ + public $resourceKey = 'dispute'; + + +} \ No newline at end of file diff --git a/lib/DraftOrder.php b/lib/DraftOrder.php index 902cee5..9e3be91 100644 --- a/lib/DraftOrder.php +++ b/lib/DraftOrder.php @@ -12,6 +12,14 @@ /** + * -------------------------------------------------------------------------- + * DraftOrder -> Child Resources + * -------------------------------------------------------------------------- + * + * @property-read Metafield $Metafield + * + * @method Metafield Metafield(integer $id = null) + * * -------------------------------------------------------------------------- * DraftOrder -> Custom actions * -------------------------------------------------------------------------- @@ -39,4 +47,11 @@ class DraftOrder extends ShopifyResource protected $customPutActions = array( 'complete', ); -} \ No newline at end of file + + /** + * @inheritDoc + */ + protected $childResource = array( + 'Metafield', + ); +} diff --git a/lib/Fulfillment.php b/lib/Fulfillment.php index 18138d2..ec9cd75 100644 --- a/lib/Fulfillment.php +++ b/lib/Fulfillment.php @@ -24,6 +24,7 @@ * @method array complete() Complete a fulfillment * @method array open() Open a pending fulfillment * @method array cancel() Cancel a fulfillment + * @method array update_tracking(array $data) Updates the tracking information for a fulfillment. * */ class Fulfillment extends ShopifyResource @@ -47,5 +48,6 @@ class Fulfillment extends ShopifyResource 'complete', 'open', 'cancel', + 'update_tracking', ); -} \ No newline at end of file +} diff --git a/lib/FulfillmentOrder.php b/lib/FulfillmentOrder.php new file mode 100644 index 0000000..45de89a --- /dev/null +++ b/lib/FulfillmentOrder.php @@ -0,0 +1,49 @@ + + * Created at 5/21/21 11:27 AM UTC+10:00 + * + * @see https://shopify.dev/docs/admin-api/rest/reference/shipping-and-fulfillment/fulfillmentorder Shopify API Reference for Fulfillment Order + */ + +namespace PHPShopify; + + +/** + * -------------------------------------------------------------------------- + * FulfillmentOrder -> Child Resources + * -------------------------------------------------------------------------- + * + * -------------------------------------------------------------------------- + * Fulfillment -> Custom actions + * -------------------------------------------------------------------------- + * @method array cancel() Cancel a fulfillment order + * @method array open() Open a fulfillment order + * @method array close() Close a fulfillment order + * @method array move() Move a fulfilment order to a new location + * @method array reschedule() Reschedule fulfill_at_time of a scheduled fulfillment order + * @method array hold(array $data) Hold a fulfillment order + * @method array release_hold() Release hold on a fulfillment order + */ +class FulfillmentOrder extends ShopifyResource +{ + /** + * @inheritDoc + */ + protected $resourceKey = 'fulfillment_order'; + + + /** + * @inheritDoc + */ + protected $customPostActions = array( + 'close', + 'open', + 'cancel', + 'move', + 'reschedule', + 'hold', + 'release_hold' + ); +} \ No newline at end of file diff --git a/lib/GiftCard.php b/lib/GiftCard.php index 2391064..e749b2d 100644 --- a/lib/GiftCard.php +++ b/lib/GiftCard.php @@ -29,6 +29,13 @@ class GiftCard extends ShopifyResource */ public $searchEnabled = true; + /** + * @inheritDoc + */ + protected $childResource = array( + 'GiftCardAdjustment' => 'Adjustment' + ); + /** * Disable a gift card. * Disabling a gift card is permanent and cannot be undone. @@ -45,4 +52,4 @@ public function disable() return $this->post($dataArray, $url); } -} \ No newline at end of file +} diff --git a/lib/GiftCardAdjustment.php b/lib/GiftCardAdjustment.php new file mode 100644 index 0000000..1592913 --- /dev/null +++ b/lib/GiftCardAdjustment.php @@ -0,0 +1,36 @@ + + * Created at 8/19/16 12:07 PM UTC+06:00 + * + * @see https://help.shopify.com/api/reference/customeraddress Shopify API Reference for CustomerAddress + */ + +namespace PHPShopify; + + +/** + * -------------------------------------------------------------------------- + * GiftCardAdjustment -> Custom actions + * -------------------------------------------------------------------------- + * @method array makeDefault() Sets the address as default for the customer + * + */ +class GiftCardAdjustment extends ShopifyResource +{ + /** + * @inheritDoc + */ + protected $resourceKey = 'adjustment'; + + /** + * @inheritDoc + */ + protected function pluralizeKey() + { + return 'adjustments'; + } + + +} diff --git a/lib/GraphQL.php b/lib/GraphQL.php index 185593b..00f4f4f 100644 --- a/lib/GraphQL.php +++ b/lib/GraphQL.php @@ -45,7 +45,7 @@ public function post($graphQL, $url = null, $wrapData = false, $variables = null if (!$url) $url = $this->generateUrl(); $response = HttpRequestGraphQL::post($url, $graphQL, $this->httpHeaders, $variables); - + return $this->processResponse($response); } @@ -75,4 +75,4 @@ public function delete($urlParams = array(), $url = null) { throw new SdkException("Only POST method is allowed for GraphQL!"); } -} \ No newline at end of file +} diff --git a/lib/HttpRequestGraphQL.php b/lib/HttpRequestGraphQL.php index 8aef907..1dc27d8 100644 --- a/lib/HttpRequestGraphQL.php +++ b/lib/HttpRequestGraphQL.php @@ -44,14 +44,16 @@ protected static function prepareRequest($httpHeaders = array(), $data = array() throw new SdkException("The GraphQL Admin API requires an access token for making authenticated requests!"); } - self::$httpHeaders = $httpHeaders; - if (is_array($variables)) { self::$postDataGraphQL = json_encode(['query' => $data, 'variables' => $variables]); - self::$httpHeaders['Content-type'] = 'application/json'; + $httpHeaders['Content-type'] = 'application/json'; } else { - self::$httpHeaders['Content-type'] = 'application/graphql'; + $httpHeaders['Content-type'] = 'application/graphql'; } + + $httpHeaders['X-Shopify-Access-Token'] = $httpHeaders['X-Shopify-Access-Token']; + + self::$httpHeaders = $httpHeaders; } /** @@ -66,10 +68,11 @@ protected static function prepareRequest($httpHeaders = array(), $data = array() */ public static function post($url, $data, $httpHeaders = array(), $variables = null) { + self::prepareRequest($httpHeaders, $data, $variables); - $response = CurlRequest::post($url, self::$postDataGraphQL, self::$httpHeaders); + self::$postDataJSON = self::$postDataGraphQL; - return self::processResponse($response); + return self::processRequest('POST', $url); } -} \ No newline at end of file +} diff --git a/lib/HttpRequestJson.php b/lib/HttpRequestJson.php index ceab78f..642e4ec 100644 --- a/lib/HttpRequestJson.php +++ b/lib/HttpRequestJson.php @@ -19,7 +19,6 @@ */ class HttpRequestJson { - /** * HTTP request headers * @@ -32,7 +31,7 @@ class HttpRequestJson * * @var string */ - private static $postDataJSON; + protected static $postDataJSON; /** @@ -68,9 +67,7 @@ public static function get($url, $httpHeaders = array()) { self::prepareRequest($httpHeaders); - $response = CurlRequest::get($url, self::$httpHeaders); - - return self::processResponse($response); + return self::processRequest('GET', $url); } /** @@ -86,9 +83,7 @@ public static function post($url, $dataArray, $httpHeaders = array()) { self::prepareRequest($httpHeaders, $dataArray); - $response = CurlRequest::post($url, self::$postDataJSON, self::$httpHeaders); - - return self::processResponse($response); + return self::processRequest('POST', $url); } /** @@ -104,9 +99,7 @@ public static function put($url, $dataArray, $httpHeaders = array()) { self::prepareRequest($httpHeaders, $dataArray); - $response = CurlRequest::put($url, self::$postDataJSON, self::$httpHeaders); - - return self::processResponse($response); + return self::processRequest('PUT', $url); } /** @@ -121,9 +114,68 @@ public static function delete($url, $httpHeaders = array()) { self::prepareRequest($httpHeaders); - $response = CurlRequest::delete($url, self::$httpHeaders); + return self::processRequest('DELETE', $url); + } - return self::processResponse($response); + /** + * Process a curl request and return decoded JSON response + * + * @param string $method Request http method ('GET', 'POST', 'PUT' or 'DELETE') + * @param string $url Request URL + * + * @throws CurlException if response received with unexpected HTTP code. + * + * @return array + */ + public static function processRequest($method, $url) { + $retry = 0; + $raw = null; + + while(true) { + try { + switch($method) { + case 'GET': + $raw = CurlRequest::get($url, self::$httpHeaders); + break; + case 'POST': + $raw = CurlRequest::post($url, self::$postDataJSON, self::$httpHeaders); + break; + case 'PUT': + $raw = CurlRequest::put($url, self::$postDataJSON, self::$httpHeaders); + break; + case 'DELETE': + $raw = CurlRequest::delete($url, self::$httpHeaders); + break; + default: + throw new \Exception("unexpected request method '$method'"); + } + + return self::processResponse($raw); + } catch(\Exception $e) { + if (!self::shouldRetry($raw, $e, $retry++)) { + throw $e; + } + } + } + } + + /** + * Evaluate if send again a request + * + * @param string $response Raw request response + * @param exception $error the request error occured + * @param integer $retry the current number of retry + * + * @return bool + */ + public static function shouldRetry($response, $error, $retry) { + $config = ShopifySDK::$config; + + if (isset($config['RequestRetryCallback'])) { + return $config['RequestRetryCallback']($response, $error, $retry); + } + + return false; } /** @@ -131,12 +183,33 @@ public static function delete($url, $httpHeaders = array()) * * @param string $response * - * @return array + * @return string */ protected static function processResponse($response) { + $responseArray = json_decode($response, true); - return json_decode($response, true); - } + if ($responseArray === null) { + //Something went wrong, Checking HTTP Codes + $httpOK = 200; //Request Successful, OK. + $httpCreated = 201; //Create Successful. + $httpDeleted = 204; //Delete Successful + $httpOther = 303; //See other (headers). + + $lastHttpResponseHeaders = CurlRequest::$lastHttpResponseHeaders; + + //should be null if any other library used for http calls + $httpCode = CurlRequest::$lastHttpCode; + + if ($httpCode == $httpOther && array_key_exists('location', $lastHttpResponseHeaders)) { + return ['location' => $lastHttpResponseHeaders['location']]; + } -} \ No newline at end of file + if ($httpCode != null && $httpCode != $httpOK && $httpCode != $httpCreated && $httpCode != $httpDeleted) { + throw new Exception\CurlException("Request failed with HTTP Code $httpCode.", $httpCode); + } + } + + return $responseArray; + } +} diff --git a/lib/Order.php b/lib/Order.php index 98efd2a..2a26fab 100644 --- a/lib/Order.php +++ b/lib/Order.php @@ -49,6 +49,7 @@ class Order extends ShopifyResource */ protected $childResource = array ( 'Fulfillment', + 'FulfillmentOrder', 'OrderRisk' => 'Risk', 'Refund', 'Transaction', diff --git a/lib/Payouts.php b/lib/Payouts.php new file mode 100644 index 0000000..e43245b --- /dev/null +++ b/lib/Payouts.php @@ -0,0 +1,25 @@ + + * @author Matthew Crigger + * + * @see https://help.shopify.com/en/api/reference/shopify_payments/payout Shopify API Reference for Shopify Payment Payouts + */ + +namespace PHPShopify; + +/** + * -------------------------------------------------------------------------- + * ShopifyPayment -> Child Resources + * -------------------------------------------------------------------------- + * + * + */ +class Payouts extends ShopifyResource +{ + /** + * @inheritDoc + */ + protected $resourceKey = 'payout'; +} \ No newline at end of file diff --git a/lib/PriceRule.php b/lib/PriceRule.php index de08014..15c77a9 100644 --- a/lib/PriceRule.php +++ b/lib/PriceRule.php @@ -17,6 +17,7 @@ * @property-read ShopifyResource $DiscountCode * * @method ShopifyResource DiscountCode(integer $id = null) + * @method ShopifyResource Batch() * */ class PriceRule extends ShopifyResource @@ -26,15 +27,11 @@ class PriceRule extends ShopifyResource */ public $resourceKey = 'price_rule'; - /** - * @inheritDoc - */ - public $countEnabled = false; - /** * @inheritDoc */ protected $childResource = array( - 'DiscountCode' + 'DiscountCode', + 'Batch', ); -} \ No newline at end of file +} diff --git a/lib/Refund.php b/lib/Refund.php index c91edbe..2fe6e50 100644 --- a/lib/Refund.php +++ b/lib/Refund.php @@ -14,7 +14,7 @@ * -------------------------------------------------------------------------- * Refund -> Custom actions * -------------------------------------------------------------------------- - * @method array calculate() Calculate a Refund. + * @method array calculate(array $calculation = null) Calculate a Refund. * */ class Refund extends ShopifyResource @@ -30,4 +30,4 @@ class Refund extends ShopifyResource protected $customPostActions = array ( 'calculate', ); -} \ No newline at end of file +} diff --git a/lib/ShippingRate.php b/lib/ShippingRate.php new file mode 100644 index 0000000..e5de310 --- /dev/null +++ b/lib/ShippingRate.php @@ -0,0 +1,19 @@ + + * Created at 8/19/16 7:27 PM UTC+06:00 + * + * @see https://help.shopify.com/api/reference/shipping_rates Shopify API Reference for ShippingRate + */ + +namespace PHPShopify; + + +class ShippingRate extends ShopifyResource +{ + /** + * @inheritDoc + */ + protected $resourceKey = 'shipping_rate'; +} diff --git a/lib/ShopifyPayment.php b/lib/ShopifyPayment.php new file mode 100644 index 0000000..54fc6b3 --- /dev/null +++ b/lib/ShopifyPayment.php @@ -0,0 +1,53 @@ + + * Created at 01/06/2020 16:45 AM UTC+03:00 + * + * @see https://shopify.dev/docs/admin-api/rest/reference/shopify_payments Shopify API Reference for ShopifyPayment + */ + +namespace PHPShopify; + + +/** + * -------------------------------------------------------------------------- + * ShopifyPayment -> Child Resources + * -------------------------------------------------------------------------- + * @property-read ShopifyResource $Dispute + * + * @method ShopifyResource Dispute(integer $id = null) + * + * @property-read ShopifyResource $Balance + * + * @method ShopifyResource Balance(integer $id = null) + * + * @property-read ShopifyResource $Payouts + * + * @method ShopifyResource Payouts(integer $id = null) + * + + */ +class ShopifyPayment extends ShopifyResource +{ + /** + * @inheritDoc + */ + public $resourceKey = 'shopify_payment'; + + /** + * If the resource is read only. (No POST / PUT / DELETE actions) + * + * @var boolean + */ + public $readOnly = true; + + /** + * @inheritDoc + */ + protected $childResource = array( + 'Balance', + 'Dispute', + 'Payouts', + ); +} \ No newline at end of file diff --git a/lib/ShopifyResource.php b/lib/ShopifyResource.php index 66ce2b7..19bc198 100644 --- a/lib/ShopifyResource.php +++ b/lib/ShopifyResource.php @@ -136,6 +136,18 @@ abstract class ShopifyResource */ private $prevLink = null; + /** + * HTTP code used to check if we need to poll or not + */ + public $httpCode = null; + + /** + * Response Header Location, used for discount code lookup + * @see: https://shopify.dev/docs/admin-api/rest/reference/discounts/discountcode?api[version]=2020-04#lookup-2020-04 + * @var string $discountLocation + */ + private $discountLocation = null; + public function __construct($id = null, $parentResourceUrl = '') { $this->id = $id; @@ -149,6 +161,12 @@ public function __construct($id = null, $parentResourceUrl = '') } elseif (!isset($config['ApiKey']) || !isset($config['Password'])) { throw new SdkException("Either AccessToken or ApiKey+Password Combination (in case of private API) is required to access the resources. Please check SDK configuration!"); } + + if (isset($config['ShopifyApiFeatures'])) { + foreach($config['ShopifyApiFeatures'] as $apiFeature) { + $this->httpHeaders['X-Shopify-Api-Features'] = $apiFeature; + } + } } /** @@ -313,7 +331,7 @@ protected function getResourcePath() */ public function generateUrl($urlParams = array(), $customAction = null) { - return $this->resourceUrl . ($customAction ? "/$customAction" : '') . '.json' . (!empty($urlParams) ? '?' . http_build_query($urlParams) : ''); + return $this->resourceUrl . ($customAction ? "/$customAction" : '') . '.json' . (!empty($urlParams) ? '?' . preg_replace('/\%5B\d+\%5D/', '%5B%5D', http_build_query($urlParams)) : ''); } /** @@ -508,7 +526,7 @@ protected function castString($array) /** * Process the request response * - * @param array $responseArray Request response in array format + * @param array $response Request response in array format * @param string $dataKey Keyname to fetch data from response array * * @throws ApiException if the response has an error specified @@ -516,45 +534,51 @@ protected function castString($array) * * @return array */ - public function processResponse($responseArray, $dataKey = null) + public function processResponse($response, $dataKey = null) { + self::$lastHttpResponseHeaders = CurlRequest::$lastHttpResponseHeaders; - if ($responseArray === null) { - //Something went wrong, Checking HTTP Codes - $httpOK = 200; //Request Successful, OK. - $httpCreated = 201; //Create Successful. - $httpDeleted = 204; //Delete Successful + $lastResponseHeaders = CurlRequest::$lastHttpResponseHeaders; + + $this->getLinks($lastResponseHeaders); - //should be null if any other library used for http calls - $httpCode = CurlRequest::$lastHttpCode; + $this->getLocationHeader($lastResponseHeaders); - if ($httpCode != null && $httpCode != $httpOK && $httpCode != $httpCreated && $httpCode != $httpDeleted) { - throw new Exception\CurlException("Request failed with HTTP Code $httpCode."); - } - } + $httpCode = CurlRequest::$lastHttpCode; + $this->httpCode = $httpCode; - $lastResponseHeaders = CurlRequest::$lastHttpResponseHeaders; - $this->getLinks($lastResponseHeaders); + if (isset($response['errors'])) { + $message = $this->castString($response['errors']); - if (isset($responseArray['errors'])) { - $message = $this->castString($responseArray['errors']); + //check account already enabled or not + if($message=='account already enabled'){ + return array('account_activation_url'=>false); + } - throw new ApiException($message, CurlRequest::$lastHttpCode); + throw new ApiException($message, $httpCode); } - if ($dataKey && isset($responseArray[$dataKey])) { - return $responseArray[$dataKey]; + if ($dataKey && isset($response[$dataKey])) { + return $response[$dataKey]; } else { - return $responseArray; + return $response; } } - public function getLinks($responseHeaders){ + public function getLinks($responseHeaders) { $this->nextLink = $this->getLink($responseHeaders,'next'); $this->prevLink = $this->getLink($responseHeaders,'previous'); } + public function getLocationHeader($responseHeaders) { + + if(!empty($responseHeaders['location'])) { + $this->discountLocation = $responseHeaders['location']; + } + + } + public function getLink($responseHeaders, $type='next'){ if(array_key_exists('x-shopify-api-version', $responseHeaders) @@ -590,6 +614,10 @@ public function getNextLink(){ return $this->nextLink; } + public function getDiscountLocation(){ + return $this->discountLocation; + } + public function getUrlParams($url) { if ($url) { $parts = parse_url($url); diff --git a/lib/ShopifySDK.php b/lib/ShopifySDK.php index e6e66f5..aa50e05 100644 --- a/lib/ShopifySDK.php +++ b/lib/ShopifySDK.php @@ -67,9 +67,13 @@ use PHPShopify\Exception\SdkException; /** + * @property-read ApplicationCredit $applicationCredit * @property-read AbandonedCheckout $AbandonedCheckout + * @property-read AccessScope $AccessScope + * @property-read ApplicationCharge $ApplicationCharge * @property-read Blog $Blog * @property-read CarrierService $CarrierService + * @property-read Cart $Cart * @property-read Collect $Collect * @property-read Collection $Collection * @property-read Comment $Comment @@ -81,8 +85,9 @@ * @property-read Discount $Discount * @property-read DiscountCode $DiscountCode * @property-read DraftOrder $DraftOrder - * @property-read PriceRule $PriceRule + * @property-read Checkout $Checkout * @property-read Event $Event + * @property-read Fulfillment $Fulfillment * @property-read FulfillmentService $FulfillmentService * @property-read GiftCard $GiftCard * @property-read InventoryItem $InventoryItem @@ -95,21 +100,30 @@ * @property-read Policy $Policy * @property-read Product $Product * @property-read ProductListing $ProductListing + * @property-read CollectionListing $CollectionListing * @property-read ProductVariant $ProductVariant + * @property-read PriceRule $PriceRule * @property-read RecurringApplicationCharge $RecurringApplicationCharge * @property-read Redirect $Redirect + * @property-read Report $Report * @property-read ScriptTag $ScriptTag * @property-read ShippingZone $ShippingZone * @property-read Shop $Shop * @property-read SmartCollection $SmartCollection + * @property-read ShopifyPayment $ShopifyPayment + * @property-read TenderTransaction $TenderTransaction * @property-read Theme $Theme * @property-read User $User * @property-read Webhook $Webhook * @property-read GraphQL $GraphQL * + * @method ApplicationCredit ApplicationCredit(integer $id = null) * @method AbandonedCheckout AbandonedCheckout(integer $id = null) + * @method AccessScope AccessScope() + * @method ApplicationCharge ApplicationCharge(integer $id = null) * @method Blog Blog(integer $id = null) * @method CarrierService CarrierService(integer $id = null) + * @method Cart Cart(string $cart_token = null) * @method Collect Collect(integer $id = null) * @method Collection Collection(integer $id = null) * @method Comment Comment(integer $id = null) @@ -121,9 +135,10 @@ * @method Discount Discount(integer $id = null) * @method DraftOrder DraftOrder(integer $id = null) * @method DiscountCode DiscountCode(integer $id = null) - * @method PriceRule PriceRule(integer $id = null) * @method Event Event(integer $id = null) + * @method Fulfillment Fulfillment(integer $id = null) * @method FulfillmentService FulfillmentService(integer $id = null) + * @method FulfillmentOrder FulfillmentOrder(integer $id = null) * @method GiftCard GiftCard(integer $id = null) * @method InventoryItem InventoryItem(integer $id = null) * @method InventoryLevel InventoryLevel(integer $id = null) @@ -131,17 +146,23 @@ * @method Metafield Metafield(integer $id = null) * @method Multipass Multipass(integer $id = null) * @method Order Order(integer $id = null) + * @method Checkout Checkout(integer $id = null) * @method Page Page(integer $id = null) * @method Policy Policy(integer $id = null) * @method Product Product(integer $id = null) * @method ProductListing ProductListing(integer $id = null) * @method ProductVariant ProductVariant(integer $id = null) + * @method CollectionListing CollectionListing(integer $id = null) + * @method PriceRule PriceRule(integer $id = null) * @method RecurringApplicationCharge RecurringApplicationCharge(integer $id = null) * @method Redirect Redirect(integer $id = null) + * @method Report Report(integer $id = null) * @method ScriptTag ScriptTag(integer $id = null) * @method ShippingZone ShippingZone(integer $id = null) * @method Shop Shop(integer $id = null) + * @method ShopifyPayment ShopifyPayment() * @method SmartCollection SmartCollection(integer $id = null) + * @method TenderTransaction TenderTransaction() * @method Theme Theme(int $id = null) * @method User User(integer $id = null) * @method Webhook Webhook(integer $id = null) @@ -156,9 +177,13 @@ class ShopifySDK */ protected $resources = array( 'AbandonedCheckout', + 'Adjustment', + 'ApplicationCredit', + 'AccessScope', 'ApplicationCharge', 'Blog', 'CarrierService', + 'Cart', 'Collect', 'Collection', 'Comment', @@ -171,7 +196,9 @@ class ShopifySDK 'DiscountCode', 'DraftOrder', 'Event', + 'Fulfillment', 'FulfillmentService', + 'FulfillmentOrder', 'GiftCard', 'InventoryItem', 'InventoryLevel', @@ -179,11 +206,13 @@ class ShopifySDK 'Metafield', 'Multipass', 'Order', + 'Checkout', 'Page', 'Policy', 'Product', 'ProductListing', 'ProductVariant', + 'CollectionListing', 'PriceRule', 'RecurringApplicationCharge', 'Redirect', @@ -192,6 +221,8 @@ class ShopifySDK 'ShippingZone', 'Shop', 'SmartCollection', + 'ShopifyPayment', + 'TenderTransaction', 'Theme', 'User', 'Webhook', @@ -211,7 +242,7 @@ class ShopifySDK /** * @var string Default Shopify API version */ - public static $defaultApiVersion = '2020-01'; + public static $defaultApiVersion = '2022-07'; /** * Shop / API configurations @@ -229,16 +260,22 @@ class ShopifySDK protected $childResources = array( 'Article' => 'Blog', 'Asset' => 'Theme', + 'Balance' => 'ShopifyPayment', 'CustomerAddress' => 'Customer', + 'GiftCardAdjustment'=> 'GiftCard', + 'Dispute' => 'ShopifyPayment', 'Fulfillment' => 'Order', 'FulfillmentEvent' => 'Fulfillment', 'OrderRisk' => 'Order', + 'Payouts' => 'ShopifyPayment', 'ProductImage' => 'Product', 'ProductVariant' => 'Product', 'DiscountCode' => 'PriceRule', 'Province' => 'Country', 'Refund' => 'Order', 'Transaction' => 'Order', + 'ShippingRate' => 'Checkout', + 'Transactions' => 'Balance', 'UsageCharge' => 'RecurringApplicationCharge', ); @@ -334,6 +371,10 @@ public static function config($config) static::$timeAllowedForEachApiCall = $config['AllowedTimePerCall']; } + if (isset($config['Curl']) && is_array($config['Curl'])) { + CurlRequest::config($config['Curl']); + } + return new ShopifySDK; } diff --git a/lib/SmartCollection.php b/lib/SmartCollection.php index d39925c..2d7a347 100644 --- a/lib/SmartCollection.php +++ b/lib/SmartCollection.php @@ -15,8 +15,10 @@ * SmartCollection -> Child Resources * -------------------------------------------------------------------------- * @property-read Event $Event + * @property-read Metafield $Metafield * * @method Event Event(integer $id = null) + * @method Metafield Metafield(integer $id = null) * * -------------------------------------------------------------------------- * SmartCollection -> Custom actions diff --git a/lib/TenderTransaction.php b/lib/TenderTransaction.php new file mode 100644 index 0000000..4265c41 --- /dev/null +++ b/lib/TenderTransaction.php @@ -0,0 +1,18 @@ + + * @author Matthew Crigger + * + * @see https://help.shopify.com/en/api/reference/shopify_payments/transaction Shopify API Reference for Shopify Payment Transactions + */ + +namespace PHPShopify; + + +class Transactions extends ShopifyResource +{ + /** + * @inheritDoc + */ + protected $resourceKey = 'transaction'; + + /** + * If the resource is read only. (No POST / PUT / DELETE actions) + * + * @var boolean + */ + public $readOnly = true; +} diff --git a/tests/CountryTest.php b/tests/CountryTest.php index 1a0ddc2..1637cde 100644 --- a/tests/CountryTest.php +++ b/tests/CountryTest.php @@ -22,7 +22,7 @@ class CountryTest extends TestSimpleResource * @inheritDoc */ public $putArray = array( - "tax" => 0.01, + "tax" => 0.15, ); /** @@ -32,4 +32,4 @@ class CountryTest extends TestSimpleResource public function testGet() { $this->assertEquals(1, 1); } -} \ No newline at end of file +} diff --git a/tests/GraphQLTest.php b/tests/GraphQLTest.php new file mode 100644 index 0000000..fcc12c4 --- /dev/null +++ b/tests/GraphQLTest.php @@ -0,0 +1,39 @@ +GraphQL->post($graphQL); + + $this->assertNotEmpty($return['data']['shop']); + } + + +} diff --git a/tests/MetafieldTest.php b/tests/MetafieldTest.php index 5038381..b58877a 100644 --- a/tests/MetafieldTest.php +++ b/tests/MetafieldTest.php @@ -17,7 +17,7 @@ class MetafieldTest extends TestSimpleResource "namespace" => "inventory", "key" => "warehouse", "value" => 25, - "value_type" => "integer", + "type" => "integer", ); /** @@ -25,6 +25,6 @@ class MetafieldTest extends TestSimpleResource */ public $putArray = array( "value" => "something new", - "value_type" => "string", + "type" => "string", ); } \ No newline at end of file diff --git a/tests/TestResource.php b/tests/TestResource.php index 1c9a268..3fd3b2d 100644 --- a/tests/TestResource.php +++ b/tests/TestResource.php @@ -23,6 +23,7 @@ public static function setUpBeforeClass() 'ShopUrl' => getenv('SHOPIFY_SHOP_URL'), //Your shop URL 'ApiKey' => getenv('SHOPIFY_API_KEY'), //Your Private API Key 'Password' => getenv('SHOPIFY_API_PASSWORD'), //Your Private API Password + 'AccessToken' => getenv('SHOPIFY_API_PASSWORD'), //Your Access Token(Private API Password) ); self::$shopify = ShopifySDK::config($config);