diff --git a/composer.json b/composer.json index 154b492..0007305 100644 --- a/composer.json +++ b/composer.json @@ -14,11 +14,13 @@ } ], "require": { - "guzzlehttp/guzzle": "~5.2", - "guzzlehttp/cache-subscriber": "0.1.*@dev", - "php": ">=5.3.3" + "php": ">=7.4", + "ext-json": "*", + "psr/http-message": "^1.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0" }, "autoload": { - "psr-0": {"Foxy\\FoxyClient": "src/"} + "psr-4": {"Foxy\\FoxyClient\\": "src/"} } } diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..44fa485 --- /dev/null +++ b/composer.lock @@ -0,0 +1,636 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "8838325f3931ecb2ca6e6ca584a28708", + "packages": [ + { + "name": "kriswallsmith/buzz", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/kriswallsmith/Buzz.git", + "reference": "2db23c3627ae7a86240ef2e68c6f8bb2c622e90d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/kriswallsmith/Buzz/zipball/2db23c3627ae7a86240ef2e68c6f8bb2c622e90d", + "reference": "2db23c3627ae7a86240ef2e68c6f8bb2c622e90d", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/httplug": "^1.1 || ^2.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0", + "symfony/options-resolver": "^3.4 || ^4.0 || ^5.0 || ^6.0" + }, + "provide": { + "php-http/client-implementation": "1.0", + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "nyholm/psr7": "^1.0", + "php-http/client-integration-tests": "^3.0", + "phpunit/phpunit": "^7.5 || ^9.4", + "psr/log": "^1.0" + }, + "suggest": { + "ext-curl": "To use our cUrl clients", + "nyholm/psr7": "For PSR-7 and PSR-17 implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Buzz\\": "lib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kris Wallsmith", + "email": "kris.wallsmith@gmail.com", + "homepage": "https://kriswallsmith.net/" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://tnyholm.se/" + } + ], + "description": "Lightweight HTTP client", + "homepage": "https://github.com/kriswallsmith/Buzz", + "keywords": [ + "curl", + "http client" + ], + "support": { + "issues": "https://github.com/kriswallsmith/Buzz/issues", + "source": "https://github.com/kriswallsmith/Buzz/tree/1.2.1" + }, + "time": "2022-03-25T13:55:55+00:00" + }, + { + "name": "nyholm/psr7", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/Nyholm/psr7.git", + "reference": "1461e07a0f2a975a52082ca3b769ca912b816226" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Nyholm/psr7/zipball/1461e07a0f2a975a52082ca3b769ca912b816226", + "reference": "1461e07a0f2a975a52082ca3b769ca912b816226", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "php-http/message-factory": "^1.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "http-interop/http-factory-tests": "^0.9", + "php-http/psr7-integration-tests": "^1.0", + "phpunit/phpunit": "^7.5 || 8.5 || 9.4", + "symfony/error-handler": "^4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-4": { + "Nyholm\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + }, + { + "name": "Martijn van der Ven", + "email": "martijn@vanderven.se" + } + ], + "description": "A fast PHP7 implementation of PSR-7", + "homepage": "https://tnyholm.se", + "keywords": [ + "psr-17", + "psr-7" + ], + "support": { + "issues": "https://github.com/Nyholm/psr7/issues", + "source": "https://github.com/Nyholm/psr7/tree/1.5.0" + }, + "funding": [ + { + "url": "https://github.com/Zegnat", + "type": "github" + }, + { + "url": "https://github.com/nyholm", + "type": "github" + } + ], + "time": "2022-02-02T18:37:57+00:00" + }, + { + "name": "php-http/httplug", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/httplug.git", + "reference": "f640739f80dfa1152533976e3c112477f69274eb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/httplug/zipball/f640739f80dfa1152533976e3c112477f69274eb", + "reference": "f640739f80dfa1152533976e3c112477f69274eb", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/promise": "^1.1", + "psr/http-client": "^1.0", + "psr/http-message": "^1.0" + }, + "require-dev": { + "friends-of-phpspec/phpspec-code-coverage": "^4.1", + "phpspec/phpspec": "^5.1 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eric GELOEN", + "email": "geloen.eric@gmail.com" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "HTTPlug, the HTTP client abstraction for PHP", + "homepage": "http://httplug.io", + "keywords": [ + "client", + "http" + ], + "support": { + "issues": "https://github.com/php-http/httplug/issues", + "source": "https://github.com/php-http/httplug/tree/2.3.0" + }, + "time": "2022-02-21T09:52:22+00:00" + }, + { + "name": "php-http/message-factory", + "version": "v1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-http/message-factory.git", + "reference": "a478cb11f66a6ac48d8954216cfed9aa06a501a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/message-factory/zipball/a478cb11f66a6ac48d8954216cfed9aa06a501a1", + "reference": "a478cb11f66a6ac48d8954216cfed9aa06a501a1", + "shasum": "" + }, + "require": { + "php": ">=5.4", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Factory interfaces for PSR-7 HTTP Message", + "homepage": "http://php-http.org", + "keywords": [ + "factory", + "http", + "message", + "stream", + "uri" + ], + "support": { + "issues": "https://github.com/php-http/message-factory/issues", + "source": "https://github.com/php-http/message-factory/tree/master" + }, + "time": "2015-12-19T14:08:53+00:00" + }, + { + "name": "php-http/promise", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/promise.git", + "reference": "4c4c1f9b7289a2ec57cde7f1e9762a5789506f88" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/promise/zipball/4c4c1f9b7289a2ec57cde7f1e9762a5789506f88", + "reference": "4c4c1f9b7289a2ec57cde7f1e9762a5789506f88", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "friends-of-phpspec/phpspec-code-coverage": "^4.3.2", + "phpspec/phpspec": "^5.1.2 || ^6.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-4": { + "Http\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Joel Wurtz", + "email": "joel.wurtz@gmail.com" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Promise used for asynchronous HTTP requests", + "homepage": "http://httplug.io", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/php-http/promise/issues", + "source": "https://github.com/php-http/promise/tree/1.1.0" + }, + "time": "2020-07-07T09:29:14+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", + "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client/tree/master" + }, + "time": "2020-06-29T06:28:15+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "shasum": "" + }, + "require": { + "php": ">=7.0.0", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory/tree/master" + }, + "time": "2019-04-30T12:38:16+00:00" + }, + { + "name": "psr/http-message", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/master" + }, + "time": "2016-08-06T14:39:51+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "07f1b9cc2ffee6aaafcf4b710fbc38ff736bd918" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/07f1b9cc2ffee6aaafcf4b710fbc38ff736bd918", + "reference": "07f1b9cc2ffee6aaafcf4b710fbc38ff736bd918", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.1-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-02-25T11:15:52+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v6.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "a3016f5442e28386ded73c43a32a5b68586dd1c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/a3016f5442e28386ded73c43a32a5b68586dd1c4", + "reference": "a3016f5442e28386ded73c43a32a5b68586dd1c4", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.1|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an improved replacement for the array_replace PHP function", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v6.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-02-25T11:15:52+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=7.4", + "ext-json": "*" + }, + "platform-dev": [], + "plugin-api-version": "2.2.0" +} diff --git a/src/Exceptions/JsonException.php b/src/Exceptions/JsonException.php new file mode 100644 index 0000000..2ac0f43 --- /dev/null +++ b/src/Exceptions/JsonException.php @@ -0,0 +1,5 @@ +getStatusCode() : 0; + parent::__construct($message, $code, $previous); + $this->request = $request; + $this->response = $response; + } + + public function getRequest(): RequestInterface + { + return $this->request; + } + + public function getResponse(): ?ResponseInterface + { + return $this->response; + } + + public function hasResponse(): bool + { + return $this->response !== null; + } +} \ No newline at end of file diff --git a/src/Exceptions/ResponseException.php b/src/Exceptions/ResponseException.php new file mode 100644 index 0000000..053f8c3 --- /dev/null +++ b/src/Exceptions/ResponseException.php @@ -0,0 +1,29 @@ +get(); -die("
" . print_r($result, 1) . ""); - -//Get Authenticated Store -$fc = new FoxyClient($guzzle, array( - 'client_id' => $client_id, - 'client_secret' => $client_secret, - 'refresh_token' => $refresh_token, - 'access_token' => $access_token, // optional - 'access_token_expires' => $access_token_expires // optional - ) -); -$fc->get(); -$result = $fc->get($fc->getLink("fx:store")); -die("
" . print_r($result, 1) . ""); - -Credits: David Hollander of https://foxytools.com/ got this project going and is awesome. -*/ -class FoxyClient -{ - const PRODUCTION_API_HOME = 'https://api.foxycart.com'; - const SANDBOX_API_HOME = 'https://api-sandbox.foxycart.com'; - const LINK_RELATIONSHIPS_BASE_URI = 'https://api.foxycart.com/rels/'; - const PRODUCTION_AUTHORIZATION_ENDPOINT = 'https://my.foxycart.com/authorize'; - const SANDBOX_AUTHORIZATION_ENDPOINT = 'https://my-sandbox.foxycart.com/authorize'; - const DEFAULT_ACCEPT_CONTENT_TYPE = 'application/hal+json'; - - /** - * OAuth Access Token (can have various scopes such as client_full_access, user_full_access, store_full_access) - */ - private $access_token = ''; - /** - * The timestamp for when the access token expires - */ - private $access_token_expires = 0; - /** - * OAuth Refresh token used to obtain a new Access Token - */ - private $refresh_token = ''; - /** - * OAuth endpoint for getting a new Access Token - */ - private $oauth_token_endpoint = ''; - /** - * Used by OAuth for getting a new Access Token - */ - private $client_id = ''; - /** - * Used by OAuth for getting a new Access Token - */ - private $client_secret = ''; - - private $guzzle; - private $last_response = ''; - private $registered_link_relations = array('self', 'first', 'prev', 'next', 'last'); - private $links = array(); - private $use_sandbox = false; - private $obtaining_updated_access_token = false; - private $include_auth_header = true; - private $handle_exceptions = true; - private $accept_content_type = ''; - private $api_home = ''; - private $authorization_endpoint = ''; - - public function __construct(\GuzzleHttp\Client $guzzle, array $config = array()) - { - $this->guzzle = $guzzle; - $this->api_home = static::PRODUCTION_API_HOME; - $this->authorization_endpoint = static::PRODUCTION_AUTHORIZATION_ENDPOINT; - $this->updateFromConfig($config); - } - - public function updateFromConfig($config) - { - $valid_config_options = array( - 'api_home', - 'authorization_endpoint', - 'use_sandbox', - 'access_token', - 'access_token_expires', - 'refresh_token', - 'client_id', - 'client_secret', - 'handle_exceptions' - ); - foreach($valid_config_options as $valid_config_option) { - if (array_key_exists($valid_config_option, $config)) { - $this->{$valid_config_option} = $config[$valid_config_option]; - } - } - if ($this->use_sandbox && (!array_key_exists('api_home', $config) || !array_key_exists('authorization_endpoint', $config))) { - $this->api_home = static::SANDBOX_API_HOME; - $this->authorization_endpoint = static::SANDBOX_AUTHORIZATION_ENDPOINT; - } - } - - public function clearCredentials() - { - $config = array( - 'access_token' => '', - 'access_token_expires' => '', - 'refresh_token' => '', - 'client_id' => '', - 'client_secret' => '' - ); - $this->updateFromConfig($config); - } - - public function setAccessToken($access_token) - { - $this->access_token = $access_token; - } - public function getAccessToken() - { - return $this->access_token; - } - - public function setAccessTokenExpires($access_token_expires) - { - $this->access_token_expires = $access_token_expires; - } - public function getAccessTokenExpires() - { - return $this->access_token_expires; - } - - public function setRefreshToken($refresh_token) - { - $this->refresh_token = $refresh_token; - } - public function getRefreshToken() - { - return $this->refresh_token; - } - - public function setClientId($client_id) - { - $this->client_id = $client_id; - } - public function getClientId() - { - return $this->client_id; - } - - public function setClientSecret($client_secret) - { - $this->client_secret = $client_secret; - } - public function getClientSecret() - { - return $this->client_secret; - } - - public function setUseSandbox($use_sandbox) - { - $this->use_sandbox = $use_sandbox; - } - public function getUseSandbox() - { - return $this->use_sandbox; - } - - public function getApiHome() - { - return $this->api_home; - } - - public function getAuthorizationEndpoint() - { - return $this->authorization_endpoint; - } - - public function get($uri = "", $post = null) - { - return $this->go('GET', $uri, $post); - } - - public function put($uri, $post = null) - { - return $this->go('PUT', $uri, $post); - } - - public function post($uri, $post = null) - { - return $this->go('POST', $uri, $post); - } - - public function patch($uri, $post = null) - { - return $this->go('PATCH', $uri, $post); - } - - public function delete($uri, $post = null) - { - return $this->go('DELETE', $uri, $post); - } - - public function options($uri, $post = null) - { - return $this->go('OPTIONS', $uri, $post); - } - - public function head($uri, $post = null) - { - return $this->go('HEAD', $uri, $post); - } - - private function go($method, $uri, $post, $is_retry = false) - { - if (!$this->obtaining_updated_access_token) { - $this->refreshTokenAsNeeded(); - } - - //Nothing passed in, set uri to homepage - if (!$uri) { - $uri = $this->getApiHome(); - } - - //Setup Guzzle Details - $guzzle_args = array( - 'headers' => $this->getHeaders(), - 'connect_timeout' => 30, - ); - - //Set Query or Body - if ($method === "GET" && $post !== null) { - $guzzle_args['query'] = $post; - } elseif ($post !== null) { - $guzzle_args['body'] = $post; - } - - if (!$this->handle_exceptions) { - return $this->processRequest($method, $uri, $post, $guzzle_args, $is_retry); - } else { - try { - return $this->processRequest($method, $uri, $post, $guzzle_args, $is_retry); - //Catch Errors - http error - } catch (\GuzzleHttp\Exception\RequestException $e) { - return array("error_description" => $e->getMessage()); - //Catch Errors - not JSON - } catch (\GuzzleHttp\Exception\ParseException $e) { - return array("error_description" => $e->getMessage()); - } - } - } - - private function processRequest($method, $uri, $post, $guzzle_args, $is_retry = false) - { - // special case for PATCHing a Downloadable File - if ($post !== null && is_array($post) && array_key_exists('file', $post) && $method == 'PATCH') { - $method = 'POST'; - $guzzle_args['headers']['X-HTTP-Method-Override'] = 'PATCH'; - } - - $api_request = $this->guzzle->createRequest($method, $uri, $guzzle_args); - $this->last_response = $this->guzzle->send($api_request); - $data = $this->last_response->json(); - $this->saveLinks($data); - if ($this->hasExpiredAccessTokenError($data) && !$this->shouldRefreshToken()) { - if (!$is_retry) { - // we should have gotten a refresh token... looks like our access_token_expires was incorrect - // so we'll clear it out to force a refresh - $this->access_token_expires = 0; - return $this->go($method, $uri, $post, true); // try one more time - } else { - return array("error_description" => 'An error occurred attempting to update your access token. Please verify your refresh token and OAuth client credentials.'); - } - } - return $data; - } - - //Clear any saved links - public function clearLinks() - { - $this->links = array(); - } - - //Save Links to the Object For Easy Retrieval Later - public function saveLinks($data) - { - if (isset($data['_links'])) { - foreach ($data['_links'] as $rel => $link) { - if (!in_array($rel, $this->registered_link_relations) - && $rel != 'curies' - && $rel.'/' != static::LINK_RELATIONSHIPS_BASE_URI) { - $this->links[$rel] = $link['href']; - } - } - } else if (isset($data['links'])) { - foreach ($data['links'] as $link) { - foreach ($link['rel'] as $rel) { - if (!in_array($rel, $this->registered_link_relations) - && $rel.'/' != static::LINK_RELATIONSHIPS_BASE_URI) { - $this->links[$rel] = $link['href']; - } - } - } - } - } - - // Get a link out of the internally stored links - // The link relationship base uri can be excluded ("fx:" for HAL, full URI for Siren) - public function getLink($link_rel_string) - { - $search_string = $link_rel_string; - if (!in_array($link_rel_string, $this->registered_link_relations) - && strpos($link_rel_string, "fx:") === FALSE - && strpos($link_rel_string, static::LINK_RELATIONSHIPS_BASE_URI) === FALSE) { - if ($this->getAcceptContentType() == static::DEFAULT_ACCEPT_CONTENT_TYPE) { - $search_string = 'fx:' . $search_string; - } else { - $search_string = static::LINK_RELATIONSHIPS_BASE_URI . $search_string; - } - } - if (isset($this->links[$search_string])) { - return $this->links[$search_string]; - } else { - return ""; - } - } - - // Get stored links (excluding link rel base uri) - public function getLinks() - { - $links = array(); - foreach($this->links as $rel => $href) { - $simple_rel = $rel; - $base_uris = array("fx:", static::LINK_RELATIONSHIPS_BASE_URI); - foreach($base_uris as $base_uri) { - $pos = strpos($simple_rel, $base_uri); - if ($pos !== FALSE && ($simple_rel.'/' != $base_uri)) { - $simple_rel = substr($simple_rel, strlen($base_uri)); - } - } - $links[$simple_rel] = $href; - } - return $links; - } - - //Return any errors that exist in the response data. - public function getErrors($data) - { - $errors = array(); - if ($this->getLastStatusCode() >= 400) { - if (isset($data['error_description'])) { - $errors[] = $data['error_description']; - } elseif (isset($data['_embedded']['fx:errors'])) { - foreach ($data['_embedded']['fx:errors'] as $error) { - $errors[] = $error['message']; - } - } else { - $errors[] = 'No data returned.'; - } - } - return $errors; - } - - public function hasExpiredAccessTokenError($data) - { - $errors = $this->getErrors($data); - if (in_array('The access token provided has expired', $errors)) { - return true; - } - return false; - } - - // Set a custom supported content type (application/hal+json, application/vnd.siren+json, etc) - public function setAcceptContentType($accept_content_type) - { - $this->accept_content_type = $accept_content_type; - } - - public function getAcceptContentType() - { - if ($this->accept_content_type == '') { - return static::DEFAULT_ACCEPT_CONTENT_TYPE; - } - - return $this->accept_content_type; - } - - //Get headers for this call - public function getHeaders() - { - $headers = array( - 'FOXY-API-VERSION' => 1 - ); - if ($this->access_token && $this->include_auth_header) { - $headers['Authorization'] = "Bearer " . $this->access_token; - } - $headers['Accept'] = $this->getAcceptContentType(); - - return $headers; - } - - //Get Last Status Code - public function getLastStatusCode() - { - if ($this->last_response == '') { - return ''; - } - return $this->last_response->getStatusCode(); - } - //Get Last Response Header - public function getLastResponseHeader($header) - { - if ($this->last_response == '') { - return ''; - } - return $this->last_response->getHeader($header); - } - - public function hasOAuthCredentialsForTokenRefresh() - { - return ($this->client_id && $this->client_secret && $this->refresh_token); - } - - public function accessTokenNeedsRefreshing() - { - return (!$this->access_token_expires || (($this->access_token_expires - 30) < time())); - } - - public function shouldRefreshToken() - { - return ($this->hasOAuthCredentialsForTokenRefresh() && $this->accessTokenNeedsRefreshing()); - } - - public function obtainingToken() - { - $this->obtaining_updated_access_token = true; - $this->include_auth_header = false; - } - - public function obtainingTokenDone() - { - $this->obtaining_updated_access_token = false; - $this->include_auth_header = true; - } - - public function refreshTokenAsNeeded() - { - if ($this->shouldRefreshToken()) { - $refresh_token_data = array( - 'grant_type' => 'refresh_token', - 'refresh_token' => $this->refresh_token, - 'client_id' => $this->client_id, - 'client_secret' => $this->client_secret - ); - $this->obtainingToken(); - $data = $this->post($this->getOAuthTokenEndpoint(),$refresh_token_data); - $this->obtainingTokenDone(); - if ($this->getLastStatusCode() == '200') { - $this->access_token_expires = time() + $data['expires_in']; - $this->access_token = $data['access_token']; - } - } - } - - public function getAccessTokenFromClientCredentials() - { - $client_credentials_request_data = array( - 'grant_type' => 'client_credentials', - 'scope' => 'client_full_access', - 'client_id' => $this->client_id, - 'client_secret' => $this->client_secret - ); - $this->obtainingToken(); - $data = $this->post($this->getOAuthTokenEndpoint(),$client_credentials_request_data); - $this->obtainingTokenDone(); - if ($this->getLastStatusCode() == '200') { - $this->access_token_expires = time() + $data['expires_in']; - $this->access_token = $data['access_token']; - } - return $data; - } - - public function getAccessTokenFromAuthorizationCode($code) - { - $authorize_code_request_data = array( - 'grant_type' => 'authorization_code', - 'code' => $code, - 'client_id' => $this->client_id, - 'client_secret' => $this->client_secret - ); - $this->obtainingToken(); - $data = $this->post($this->getOAuthTokenEndpoint(),$authorize_code_request_data); - $this->obtainingTokenDone(); - if ($this->getLastStatusCode() == '200') { - $this->access_token_expires = time() + $data['expires_in']; - $this->access_token = $data['access_token']; - $this->refresh_token = $data['refresh_token']; - } - return $data; - } - - public function getOAuthTokenEndpoint() - { - if ($this->oauth_token_endpoint == '') { - $this->obtainingToken(); - $data = $this->get(); - if ($this->getLastStatusCode() == '200') { - $this->oauth_token_endpoint = $this->getLink("token"); - } else { - trigger_error('ERROR IN getOAuthTokenEndpoint: ' . $this->getLastStatusCode()); - //trigger_error(serialize($this->last_response->json())); - //die or hard code the endpoint? - $this->oauth_token_endpoint = $this->api_home . '/token'; - } - } - return $this->oauth_token_endpoint; - } - -} diff --git a/src/FoxyClient.php b/src/FoxyClient.php new file mode 100644 index 0000000..562c2f5 --- /dev/null +++ b/src/FoxyClient.php @@ -0,0 +1,636 @@ +get(); +die("
" . print_r($result, 1) . ""); + +//Get Authenticated Store +$fc = new FoxyClient($psr17Factory, $psr18Client, [ + 'client_id' => $client_id, + 'client_secret' => $client_secret, + 'refresh_token' => $refresh_token, + 'access_token' => $access_token, // optional + 'access_token_expires' => $access_token_expires // optional + ] +); +$fc->get(); +$result = $fc->get($fc->getLink("fx:store")); +die("
" . print_r($result, 1) . ""); + +Credits: David Hollander of https://foxytools.com/ got this project going and is awesome. +*/ + +use Exception; +use Foxy\FoxyClient\Exceptions\JsonException; +use Foxy\FoxyClient\Exceptions\ResponseException; +use Psr\Http\Client\ClientExceptionInterface; +use Psr\Http\Client\ClientInterface; +use Psr\Http\Message\RequestFactoryInterface; +use Psr\Http\Message\ResponseInterface; + +class FoxyClient +{ + const PRODUCTION_API_HOME = 'https://api.foxycart.com'; + const SANDBOX_API_HOME = 'https://api-sandbox.foxycart.com'; + const LINK_RELATIONSHIPS_BASE_URI = 'https://api.foxycart.com/rels/'; + const PRODUCTION_AUTHORIZATION_ENDPOINT = 'https://my.foxycart.com/authorize'; + const SANDBOX_AUTHORIZATION_ENDPOINT = 'https://my-sandbox.foxycart.com/authorize'; + const DEFAULT_ACCEPT_CONTENT_TYPE = 'application/hal+json'; + private static array $valid_config_options = [ + 'api_home', + 'authorization_endpoint', + 'use_sandbox', + 'access_token', + 'access_token_expires', + 'refresh_token', + 'client_id', + 'client_secret', + 'handle_exceptions' + ]; + /** + * OAuth Access Token (can have various scopes such as client_full_access, user_full_access, store_full_access) + */ + private string $access_token = ''; + /** + * The timestamp for when the access token expires + */ + private int $access_token_expires = 0; + /** + * OAuth Refresh token used to obtain new Access Token + */ + private string $refresh_token = ''; + /** + * OAuth endpoint for getting new Access Token + */ + private ?string $oauth_token_endpoint = null; + /** + * Used by OAuth for getting new Access Token + */ + private string $client_id = ''; + /** + * Used by OAuth for getting new Access Token + */ + private string $client_secret = ''; + private ClientInterface $client; + private RequestFactoryInterface $requestFactory; + private ?ResponseInterface $last_response = null; + private array $registered_link_relations = ['self', 'first', 'prev', 'next', 'last']; + private array $links = []; + private bool $use_sandbox = false; + private bool $obtaining_updated_access_token = false; + private bool $include_auth_header = true; + private bool $handle_exceptions = true; + private string $accept_content_type = ''; + private string $api_home = ''; + private string $authorization_endpoint = ''; + + public function __construct( + RequestFactoryInterface $requestFactory, + ClientInterface $client, + array $config = [] + ) { + $this->client = $client; + $this->requestFactory = $requestFactory; + $this->api_home = static::PRODUCTION_API_HOME; + $this->authorization_endpoint = static::PRODUCTION_AUTHORIZATION_ENDPOINT; + $this->updateFromConfig($config); + } + + public function updateFromConfig(array $config): void + { + foreach (self::$valid_config_options as $valid_config_option) { + if (array_key_exists($valid_config_option, $config)) { + $this->{$valid_config_option} = $config[$valid_config_option]; + } + } + if ($this->use_sandbox && (!array_key_exists('api_home', $config) || !array_key_exists( + 'authorization_endpoint', + $config + ))) { + $this->api_home = static::SANDBOX_API_HOME; + $this->authorization_endpoint = static::SANDBOX_AUTHORIZATION_ENDPOINT; + } + } + + public function clearCredentials(): void + { + $this->updateFromConfig([ + 'access_token' => '', + 'access_token_expires' => '', + 'refresh_token' => '', + 'client_id' => '', + 'client_secret' => '' + ]); + } + + public function getAccessToken(): string + { + return $this->access_token; + } + + public function setAccessToken(string $access_token): void + { + $this->access_token = $access_token; + } + + public function getAccessTokenExpires(): int + { + return $this->access_token_expires; + } + + public function setAccessTokenExpires(int $access_token_expires): void + { + $this->access_token_expires = $access_token_expires; + } + + public function getRefreshToken(): string + { + return $this->refresh_token; + } + + public function setRefreshToken(string $refresh_token): void + { + $this->refresh_token = $refresh_token; + } + + public function getClientId(): string + { + return $this->client_id; + } + + public function setClientId(string $client_id): void + { + $this->client_id = $client_id; + } + + public function getClientSecret(): string + { + return $this->client_secret; + } + + public function setClientSecret(string $client_secret): void + { + $this->client_secret = $client_secret; + } + + public function getUseSandbox(): bool + { + return $this->use_sandbox; + } + + public function setUseSandbox(bool $use_sandbox): void + { + $this->use_sandbox = $use_sandbox; + } + + public function getAuthorizationEndpoint(): string + { + return $this->authorization_endpoint; + } + + public function get(string $uri = "", array $post = null): array + { + return $this->go('GET', $uri, $post); + } + + private function go(string $method, string $uri, ?array $post, bool $is_retry = false): array + { + if (!$this->obtaining_updated_access_token) { + $this->refreshTokenAsNeeded(); + } + + //Nothing passed in, set uri to homepage + if (!$uri) { + $uri = $this->getApiHome(); + } + + //Setup Guzzle Details + $guzzle_args = [ + 'headers' => $this->getHeaders(), + ]; + + //Set Query or Body + if ($method === "GET" && $post !== null) { + $guzzle_args['query'] = $post; + } elseif ($post !== null) { + $guzzle_args['form_params'] = $post; + } + + if (!$this->handle_exceptions) { + return $this->processRequest($method, $uri, $post, $guzzle_args, $is_retry); + } + + try { + return $this->processRequest($method, $uri, $post, $guzzle_args, $is_retry); + } catch (JsonException $e) { + return [ + "error_description" => $e->getMessage(), + "error_code" => 400, + "error_contents" => $e->getResponse()->getBody()->getContents(), + ]; + } catch (ResponseException $e) { + $response = $e->getResponse(); + $responseContent = $response->getBody()->getContents(); + $parsedResponseContent = json_decode($responseContent, true); + + if (!empty($parsedResponseContent)) { + if ($this->hasExpiredAccessTokenError($parsedResponseContent) && !$this->shouldRefreshToken()) { + if (!$is_retry) { + // we should have gotten a refresh token... looks like our access_token_expires was incorrect + // so we'll clear it out to force a refresh + $this->access_token_expires = 0; + return $this->go($method, $uri, $post, true); // try one more time + } + + return ["error_description" => 'An error occurred attempting to update your access token. Please verify your refresh token and OAuth client credentials.']; + } + } + + return [ + "error_description" => $e->getMessage(), + "error_code" => $response->getStatusCode(), + "error_contents" => $responseContent, + "error_contents_parsed" => $parsedResponseContent, + ]; + } catch (ClientExceptionInterface $e) { + return ["error_description" => $e->getMessage()]; + } catch (Exception $e) { + return ["error_description" => $e->getMessage()]; + } + } + + public function refreshTokenAsNeeded(): void + { + if (!$this->shouldRefreshToken()) { + return; + } + + $refresh_token_data = [ + 'grant_type' => 'refresh_token', + 'refresh_token' => $this->refresh_token, + 'client_id' => $this->client_id, + 'client_secret' => $this->client_secret + ]; + + $this->obtainingToken(); + $data = $this->post($this->getOAuthTokenEndpoint(), $refresh_token_data); + $this->obtainingTokenDone(); + + if ($this->getLastStatusCode() === 200) { + $this->access_token_expires = time() + $data['expires_in']; + $this->access_token = $data['access_token']; + } + } + + public function shouldRefreshToken(): bool + { + return ($this->hasOAuthCredentialsForTokenRefresh() && $this->accessTokenNeedsRefreshing()); + } + + public function hasOAuthCredentialsForTokenRefresh(): bool + { + return ($this->client_id && $this->client_secret && $this->refresh_token); + } + + public function accessTokenNeedsRefreshing(): bool + { + return (!$this->access_token_expires || (($this->access_token_expires - 30) < time())); + } + + public function obtainingToken(): void + { + $this->obtaining_updated_access_token = true; + $this->include_auth_header = false; + } + + public function post(string $uri, array $post = null): array + { + return $this->go('POST', $uri, $post); + } + + public function getOAuthTokenEndpoint(): string + { + if ($this->oauth_token_endpoint !== null) { + return $this->oauth_token_endpoint; + } + + $this->obtainingToken(); + $this->get(); + + if ($this->getLastStatusCode() === 200) { + $this->oauth_token_endpoint = $this->getLink("token"); + return $this->oauth_token_endpoint; + } + + trigger_error('ERROR IN getOAuthTokenEndpoint: ' . $this->getLastStatusCode()); + $this->oauth_token_endpoint = $this->api_home . '/token'; + return $this->oauth_token_endpoint; + } + + public function getLastStatusCode(): ?int + { + if ($this->last_response === null) { + return null; + } + + return $this->last_response->getStatusCode(); + } + + //Clear any saved links + + public function getLink(string $link_rel_string): string + { + $search_string = $link_rel_string; + + if (!in_array($link_rel_string, $this->registered_link_relations) + && strpos($link_rel_string, "fx:") === false + && strpos($link_rel_string, static::LINK_RELATIONSHIPS_BASE_URI) === false) { + if ($this->getAcceptContentType() == static::DEFAULT_ACCEPT_CONTENT_TYPE) { + $search_string = 'fx:' . $search_string; + } else { + $search_string = static::LINK_RELATIONSHIPS_BASE_URI . $search_string; + } + } + + return $this->links[$search_string] ?? ""; + } + + //Save Links to the Object For Easy Retrieval Later + + public function getAcceptContentType(): string + { + if ($this->accept_content_type == '') { + return static::DEFAULT_ACCEPT_CONTENT_TYPE; + } + + return $this->accept_content_type; + } + + // Get a link out of the internally stored links + // The link relationship base uri can be excluded ("fx:" for HAL, full URI for Siren) + + public function setAcceptContentType(string $accept_content_type): void + { + $this->accept_content_type = $accept_content_type; + } + + // Get stored links (excluding link rel base uri) + + public function obtainingTokenDone(): void + { + $this->obtaining_updated_access_token = false; + $this->include_auth_header = true; + } + + //Return any errors that exist in the response data. + + public function getApiHome(): string + { + return $this->api_home; + } + + public function getHeaders(): array + { + $headers = [ + 'FOXY-API-VERSION' => 1 + ]; + + if ($this->access_token && $this->include_auth_header) { + $headers['Authorization'] = "Bearer " . $this->access_token; + } + + $headers['Accept'] = $this->getAcceptContentType(); + + return $headers; + } + + // Set a custom supported content type (application/hal+json, application/vnd.siren+json, etc) + + /** + * @throws ClientExceptionInterface + * @throws JsonException + */ + private function processRequest( + string $method, + string $uri, + ?array $post, + array $options + ): array { + // special case for PATCHing a Downloadable File + if (is_array($post) && array_key_exists('file', $post) && $method == 'PATCH') { + $method = 'POST'; + $options['headers']['X-HTTP-Method-Override'] = 'PATCH'; + } + + $request = $this->requestFactory->createRequest($method, $uri); + + if (!empty($options['query'])) { + $uri = $request->getUri(); + $uri = $uri->withQuery(http_build_query($options['query'])); + $request = $request->withUri($uri); + } + + if (isset($options['form_params'])) { + $body = $request->getBody(); + $body->write(http_build_query($options['form_params'], '', '&')); + $request = $request->withBody($body); + } + + if (is_array($options['headers']) && !empty($options['headers'])) { + foreach ($options['headers'] as $headerName => $headerValue) { + $request = $request->withAddedHeader($headerName, $headerValue); + } + } + + $response = $this->client->sendRequest($request); + $this->last_response = $response; + + if ($response->getStatusCode() >= 400) { + throw new ResponseException('Error completing request', $request, $response); + } + + try { + $data = json_decode($response->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR); + $this->saveLinks($data); + + return $data; + } catch (\JsonException $e) { + throw new JsonException($e->getMessage(), $request, $response); + } + } + + public function saveLinks(array $data): void + { + if (isset($data['_links'])) { + foreach ($data['_links'] as $rel => $link) { + if (!in_array($rel, $this->registered_link_relations) + && $rel != 'curies' + && $rel . '/' != static::LINK_RELATIONSHIPS_BASE_URI) { + $this->links[$rel] = $link['href']; + } + } + + return; + } + + if (isset($data['links'])) { + foreach ($data['links'] as $link) { + foreach ($link['rel'] as $rel) { + if (!in_array($rel, $this->registered_link_relations) + && $rel . '/' != static::LINK_RELATIONSHIPS_BASE_URI) { + $this->links[$rel] = $link['href']; + } + } + } + } + } + + //Get headers for this call + + public function hasExpiredAccessTokenError(array $data): bool + { + $errors = $this->getErrors($data); + + if (in_array('The access token provided has expired', $errors)) { + return true; + } + + return false; + } + + public function getErrors(array $data): array + { + $errors = []; + + if ($this->getLastStatusCode() >= 400) { + if (isset($data['error_description'])) { + $errors[] = $data['error_description']; + } elseif (isset($data['_embedded']['fx:errors'])) { + foreach ($data['_embedded']['fx:errors'] as $error) { + $errors[] = $error['message']; + } + } else { + $errors[] = 'No data returned.'; + } + } + + return $errors; + } + + public function put(string $uri, array $post = null): array + { + return $this->go('PUT', $uri, $post); + } + + public function patch(string $uri, array $post = null): array + { + return $this->go('PATCH', $uri, $post); + } + + public function delete(string $uri, array $post = null): array + { + return $this->go('DELETE', $uri, $post); + } + + public function options(string $uri, array $post = null): array + { + return $this->go('OPTIONS', $uri, $post); + } + + public function head(string $uri, array $post = null): array + { + return $this->go('HEAD', $uri, $post); + } + + public function clearLinks(): void + { + $this->links = []; + } + + public function getLinks(): array + { + $links = []; + foreach ($this->links as $rel => $href) { + $simple_rel = $rel; + $base_uris = ["fx:", static::LINK_RELATIONSHIPS_BASE_URI]; + + foreach ($base_uris as $base_uri) { + $pos = strpos($simple_rel, $base_uri); + if ($pos !== false && ($simple_rel . '/' != $base_uri)) { + $simple_rel = substr($simple_rel, strlen($base_uri)); + } + } + + $links[$simple_rel] = $href; + } + + return $links; + } + + public function getLastResponseHeader(string $header): ?array + { + if ($this->last_response === null) { + return null; + } + + return $this->last_response->getHeader($header); + } + + public function getAccessTokenFromClientCredentials(): array + { + $client_credentials_request_data = [ + 'grant_type' => 'client_credentials', + 'scope' => 'client_full_access', + 'client_id' => $this->client_id, + 'client_secret' => $this->client_secret + ]; + + $this->obtainingToken(); + $data = $this->post($this->getOAuthTokenEndpoint(), $client_credentials_request_data); + $this->obtainingTokenDone(); + + if ($this->getLastStatusCode() === 200) { + $this->access_token_expires = time() + $data['expires_in']; + $this->access_token = $data['access_token']; + } + + return $data; + } + + public function getAccessTokenFromAuthorizationCode(string $code): array + { + $authorize_code_request_data = [ + 'grant_type' => 'authorization_code', + 'code' => $code, + 'client_id' => $this->client_id, + 'client_secret' => $this->client_secret + ]; + + $this->obtainingToken(); + $data = $this->post($this->getOAuthTokenEndpoint(), $authorize_code_request_data); + $this->obtainingTokenDone(); + + if ($this->getLastStatusCode() === 200) { + $this->access_token_expires = time() + $data['expires_in']; + $this->access_token = $data['access_token']; + $this->refresh_token = $data['refresh_token']; + } + + return $data; + } + +}