├── .gitignore
├── composer.json
├── license.md
├── phpunit.xml
├── readme.md
└── src
└── RocketCode
└── Shopify
├── API.php
└── ShopifyServiceProvider.php
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor
2 | composer.phar
3 | composer.lock
4 | .DS_Store
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rocket-code/shopify",
3 | "description": "",
4 | "authors": [
5 | {
6 | "name": "Rocket Code",
7 | "email": "josh@rocketcode.io"
8 | }
9 | ],
10 | "require": {
11 | "laravel/framework": "~5.0"
12 | },
13 | "autoload": {
14 | "psr-4":
15 | {
16 | "RocketCode\\": "src/RocketCode"
17 | }
18 | },
19 | "minimum-stability": "stable"
20 | }
21 |
--------------------------------------------------------------------------------
/license.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 ROCKET CODE LLC
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
Error: ' . $e->getMessage() . ''; 75 | } 76 | 77 | // Save $accessToken 78 | ``` 79 | 80 | ####Verifying OAuth Data 81 | Shopify returns a hashed value to validate the data against. To validate (recommended before calling `getAccessToken()`), utilize `verifyRequest()`. 82 | 83 | ``` 84 | try 85 | { 86 | $verify = $sh->verifyRequest(Input::all()); 87 | if ($verify) 88 | { 89 | $code = Input::get('code'); 90 | $accessToken = $sh->getAccessToken($code); 91 | } 92 | else 93 | { 94 | // Issue with data 95 | } 96 | 97 | } 98 | catch (Exception $e) 99 | { 100 | echo '
Error: ' . $e->getMessage() . ''; 101 | } 102 | 103 | 104 | 105 | ``` 106 | 107 | `verifyRequest()` returns `TRUE` when data is valid, otherwise `FALSE`. It throws an Exception in two cases: If the timestamp generated by Shopify and your server are more than an hour apart, or if the argument passed is not an array or URL-encoded string of key/values. 108 | 109 | If you would like to skip the timestamp check (not recommended unless you cannot correct your server's time), you can pass `TRUE` as a second argument to `verifyRequest()` and timestamps will be ignored: 110 | 111 | ``` 112 | $verify = $sh->verifyRequest(Input::all(), TRUE); 113 | ``` 114 | 115 | ## Private Apps 116 | The API Wrapper does not distinguish between private and public apps. In order to utilize it with a private app, set up everything as you normally would, replacing the OAuth Access Token with the private app's Password. 117 | 118 | ##Calling the API 119 | Once set up, simply pass the data you need to the `call()` method. 120 | 121 | ``` 122 | $result = $sh->call($args); 123 | ``` 124 | 125 | ####`call()` Parameters 126 | The parameters listed below allow you to set required values for an API call as well as override additional default values. 127 | 128 | * `METHOD`: The HTTP method to use for your API call. Different endpoints require different methods. 129 | * Default: `GET` 130 | * `URL`: The URL of the API Endpoint to call. 131 | * Default: `/` (not an actual endpoint) 132 | * `HEADERS`: An array of additional Headers to be sent 133 | * Default: Empty `array()`. Headers that are automatically sent include: 134 | * Accept 135 | * Content-Type 136 | * charset 137 | * X-Shopify-Access-Token 138 | * `CHARSET`: Change the charset if necessary 139 | * Default: `UTF-8` 140 | * `DATA`: An array of data being sent with the call. For example, `$args['DATA'] = array('product' => $product);` For an [`/admin/products.json`](http://docs.shopify.com/api/product#create) product creation `POST`. 141 | * Default: Empty `array()` 142 | * `RETURNARRAY`: Set this to `TRUE` to return data in `array()` format. `FALSE` will return a `stdClass` object. 143 | * Default: `FALSE` 144 | * `ALLDATA`: Set this to `TRUE` if you would like all error and cURL info returned along with your API data (good for debugging). Data will be available in `$result->_ERROR` and `$result->_INFO`, or `$result['_ERROR']` and `$result['_INFO']`, depending if you are having it returned as an object or array. Recommended to be set to `FALSE` in production. 145 | * Default: `FALSE` 146 | * `FAILONERROR`: The value passed to cURL's [CURLOPT_FAILONERROR](http://php.net/manual/en/function.curl-setopt.php) setting. `TRUE` will cause the API Wrapper to throw an Exception if the HTTP code is >= `400`. `FALSE` in combination with `ALLDATA` set to `TRUE` will give you more debug information. 147 | * Default: `TRUE` 148 | 149 | 150 | ##Some Examples 151 | Assume that `$sh` has already been set up as documented above. 152 | 153 | ####Listing Products 154 | ``` 155 | try 156 | { 157 | 158 | $call = $sh->call(['URL' => 'products.json', 'METHOD' => 'GET', 'DATA' => ['limit' => 5, 'published_status' => 'any']]); 159 | } 160 | catch (Exception $e) 161 | { 162 | $call = $e->getMessage(); 163 | } 164 | 165 | echo '
'; 166 | var_dump($call); 167 | echo ''; 168 | 169 | ``` 170 | 171 | `$call` will either contain a `stdClass` object with `products` or an Exception error message. 172 | 173 | ####Creating a snippet from a Laravel View 174 | 175 | ``` 176 | $testData = ['name' => 'Foo', 'location' => 'Bar']; 177 | $view = (string) View::make('snippet', $testData); 178 | 179 | $themeID = 12345678; 180 | 181 | try 182 | { 183 | $call = $sh->call(['URL' => '/admin/themes/' . $themeID . '/assets.json', 'METHOD' => 'PUT', 'DATA' => ['asset' => ['key' => 'snippets/test.liquid', 'value' => $view] ] ]); 184 | } 185 | catch (Exception $e) 186 | { 187 | $call = $e->getMessage(); 188 | } 189 | 190 | echo '
'; 191 | var_dump($call); 192 | echo ''; 193 | ``` 194 | 195 | ####Performing operations on multiple shops 196 | The `setup()` method makes changing the current shop simple. 197 | 198 | ``` 199 | $apiKey = '123'; 200 | $apiSecret = '456'; 201 | 202 | $sh = App::make('ShopifyAPI', ['API_KEY' => $apiKey, 'API_SECRET' => $apiSecret]); 203 | 204 | $shops = array( 205 | 'my-shop.myshopify.com' => 'abc', 206 | 'your-shop.myshopify.com' => 'def', 207 | 'another.myshopify.com' => 'ghi' 208 | ); 209 | 210 | foreach($shops as $domain => $access) 211 | { 212 | $sh->setup(['SHOP_DOMAIN' => $domain, 'ACCESS_TOKEN'' => $access]); 213 | // $sh->call(), etc 214 | 215 | } 216 | 217 | ``` 218 | 219 | 220 | -------------------------------------------------------------------------------- /src/RocketCode/Shopify/API.php: -------------------------------------------------------------------------------- 1 | setup($data); 22 | } 23 | } 24 | 25 | /** 26 | * Verifies data returned by OAuth call 27 | * @param array|string $data 28 | * @return bool 29 | * @throws \Exception 30 | */ 31 | public function verifyRequest($data = NULL, $bypassTimeCheck = FALSE) 32 | { 33 | $da = array(); 34 | if (is_string($data)) 35 | { 36 | $each = explode('&', $data); 37 | foreach($each as $e) 38 | { 39 | list($key, $val) = explode('=', $e); 40 | $da[$key] = $val; 41 | } 42 | } 43 | elseif (is_array($data)) 44 | { 45 | $da = $data; 46 | } 47 | else 48 | { 49 | throw new \Exception('Data passed to verifyRequest() needs to be an array or URL-encoded string of key/value pairs.'); 50 | } 51 | 52 | // Timestamp check; 1 hour tolerance 53 | if (!$bypassTimeCheck) 54 | { 55 | if (($da['timestamp'] - time() > 3600)) 56 | { 57 | throw new \Exception('Timestamp is greater than 1 hour old. To bypass this check, pass TRUE as the second argument to verifyRequest().'); 58 | } 59 | } 60 | 61 | if (array_key_exists('hmac', $da)) 62 | { 63 | // HMAC Validation 64 | $queryString = http_build_query(array('code' => $da['code'], 'shop' => $da['shop'], 'timestamp' => $da['timestamp'])); 65 | $match = $da['hmac']; 66 | $calculated = hash_hmac('sha256', $queryString, $this->_API['API_SECRET']); 67 | } 68 | else 69 | { 70 | // MD5 Validation, to be removed June 1st, 2015 71 | $queryString = http_build_query(array('code' => $da['code'], 'shop' => $da['shop'], 'timestamp' => $da['timestamp']), NULL, ''); 72 | $match = $da['signature']; 73 | $calculated = md5($this->_API['API_SECRET'] . $queryString); 74 | } 75 | 76 | return $calculated === $match; 77 | } 78 | 79 | /** 80 | * Calls API and returns OAuth Access Token, which will be needed for all future requests 81 | * @param string $code 82 | * @return mixed 83 | * @throws \Exception 84 | */ 85 | public function getAccessToken($code = '') 86 | { 87 | $dta = array('client_id' => $this->_API['API_KEY'], 'client_secret' => $this->_API['API_SECRET'], 'code' => $code); 88 | $data = $this->call(['METHOD' => 'POST', 'URL' => 'https://' . $this->_API['SHOP_DOMAIN'] . '/admin/oauth/access_token', 'DATA' => $dta], FALSE); 89 | 90 | return $data->access_token; 91 | } 92 | 93 | /** 94 | * Returns a string of the install URL for the app 95 | * @param array $data 96 | * @return string 97 | */ 98 | public function installURL($data = array()) 99 | { 100 | // https://{shop}.myshopify.com/admin/oauth/authorize?client_id={api_key}&scope={scopes}&redirect_uri={redirect_uri} 101 | return 'https://' . $this->_API['SHOP_DOMAIN'] . '/admin/oauth/authorize?client_id=' . $this->_API['API_KEY'] . '&scope=' . implode(',', $data['permissions']) . (!empty($data['redirect']) ? '&redirect_uri=' . urlencode($data['redirect']) : ''); 102 | } 103 | 104 | /** 105 | * Loops over each of self::$_KEYS, filters provided data, and loads into $this->_API 106 | * @param array $data 107 | */ 108 | public function setup($data = array()) 109 | { 110 | 111 | foreach (self::$_KEYS as $k) 112 | { 113 | if (array_key_exists($k, $data)) 114 | { 115 | $this->_API[$k] = self::verifySetup($k, $data[$k]); 116 | } 117 | } 118 | } 119 | 120 | /** 121 | * Checks that data provided is in proper format 122 | * @example Removes http(s):// from SHOP_DOMAIN 123 | * @param string $key 124 | * @param string $value 125 | * @return string 126 | */ 127 | private static function verifySetup($key = '', $value = '') 128 | { 129 | $value = trim($value); 130 | 131 | switch ($key) 132 | { 133 | 134 | case 'SHOP_DOMAIN': 135 | preg_match('/(https?:\/\/)?([a-zA-Z0-9\-\.])+/', $value, $matched); 136 | return $matched[0]; 137 | break; 138 | 139 | default: 140 | return $value; 141 | } 142 | } 143 | 144 | /** 145 | * Checks that data provided is in proper format 146 | * @example Checks for presence of /admin/ in URL 147 | * @param array $userData 148 | * @return array 149 | */ 150 | private function setupUserData($userData = array()) 151 | { 152 | $returnable = array(); 153 | 154 | foreach($userData as $key => $value) 155 | { 156 | switch($key) 157 | { 158 | case 'URL': 159 | // Remove shop domain 160 | $url = str_replace($this->_API['SHOP_DOMAIN'], '', $value); 161 | 162 | // Verify it contains /admin/ 163 | if (strpos($url, '/admin/') !== 0) 164 | { 165 | $url = str_replace('//', '/', '/admin/' . preg_replace('/\/?admin\/?/', '', $url)); 166 | } 167 | $returnable[$key] = $url; 168 | break; 169 | 170 | default: 171 | $returnable[$key] = $value; 172 | 173 | } 174 | } 175 | 176 | return $returnable; 177 | } 178 | 179 | 180 | /** 181 | * Executes the actual cURL call based on $userData 182 | * @param array $userData 183 | * @return mixed 184 | * @throws \Exception 185 | */ 186 | public function call($userData = array(), $verifyData = TRUE) 187 | { 188 | if ($verifyData) 189 | { 190 | foreach (self::$_KEYS as $k) 191 | { 192 | if ((!array_key_exists($k, $this->_API)) || (empty($this->_API[$k]))) 193 | { 194 | throw new \Exception($k . ' must be set.'); 195 | } 196 | } 197 | } 198 | 199 | $defaults = array( 200 | 'CHARSET' => 'UTF-8', 201 | 'METHOD' => 'GET', 202 | 'URL' => '/', 203 | 'HEADERS' => array(), 204 | 'DATA' => array(), 205 | 'FAILONERROR' => TRUE, 206 | 'RETURNARRAY' => FALSE, 207 | 'ALLDATA' => FALSE 208 | ); 209 | 210 | if ($verifyData) 211 | { 212 | $request = $this->setupUserData(array_merge($defaults, $userData)); 213 | } 214 | else 215 | { 216 | $request = array_merge($defaults, $userData); 217 | } 218 | 219 | 220 | // Send & accept JSON data 221 | $defaultHeaders = array(); 222 | $defaultHeaders[] = 'Content-Type: application/json; charset=' . $request['CHARSET']; 223 | $defaultHeaders[] = 'Accept: application/json'; 224 | if (array_key_exists('ACCESS_TOKEN', $this->_API)) 225 | { 226 | $defaultHeaders[] = 'X-Shopify-Access-Token: ' . $this->_API['ACCESS_TOKEN']; 227 | } 228 | 229 | $headers = array_merge($defaultHeaders, $request['HEADERS']); 230 | 231 | 232 | if ($verifyData) 233 | { 234 | $url = 'https://' . $this->_API['API_KEY'] . ':' . $this->_API['ACCESS_TOKEN'] . '@' . $this->_API['SHOP_DOMAIN'] . $request['URL']; 235 | } 236 | else 237 | { 238 | $url = $request['URL']; 239 | } 240 | 241 | // cURL setup 242 | $ch = curl_init(); 243 | $options = array( 244 | CURLOPT_RETURNTRANSFER => TRUE, 245 | CURLOPT_URL => $url, 246 | CURLOPT_HTTPHEADER => $headers, 247 | CURLOPT_CUSTOMREQUEST => strtoupper($request['METHOD']), 248 | CURLOPT_ENCODING => '', 249 | CURLOPT_USERAGENT => 'RocketCode Shopify API Wrapper', 250 | CURLOPT_FAILONERROR => $request['FAILONERROR'], 251 | CURLOPT_VERBOSE => $request['ALLDATA'], 252 | CURLOPT_HEADER => 1 253 | ); 254 | 255 | // Checks if DATA is being sent 256 | if (!empty($request['DATA'])) 257 | { 258 | if (is_array($request['DATA'])) 259 | { 260 | $options[CURLOPT_POSTFIELDS] = json_encode($request['DATA']); 261 | } 262 | else 263 | { 264 | // Detect if already a JSON object 265 | json_decode($request['DATA']); 266 | if (json_last_error() == JSON_ERROR_NONE) 267 | { 268 | $options[CURLOPT_POSTFIELDS] = $request['DATA']; 269 | } 270 | else 271 | { 272 | throw new \Exception('DATA malformed.'); 273 | } 274 | } 275 | } 276 | 277 | curl_setopt_array($ch, $options); 278 | 279 | $response = curl_exec($ch); 280 | $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE); 281 | 282 | 283 | // Data returned 284 | $result = json_decode(substr($response, $headerSize), $request['RETURNARRAY']); 285 | 286 | // Headers 287 | $info = array_filter(array_map('trim', explode("\n", substr($response, 0, $headerSize)))); 288 | 289 | foreach($info as $k => $header) 290 | { 291 | if (strpos($header, 'HTTP/') > -1) 292 | { 293 | $_INFO['HTTP_CODE'] = $header; 294 | continue; 295 | } 296 | 297 | list($key, $val) = explode(':', $header); 298 | $_INFO[trim($key)] = trim($val); 299 | } 300 | 301 | 302 | // cURL Errors 303 | $_ERROR = array('NUMBER' => curl_errno($ch), 'MESSAGE' => curl_error($ch)); 304 | 305 | curl_close($ch); 306 | 307 | if ($_ERROR['NUMBER']) 308 | { 309 | throw new \Exception('ERROR #' . $_ERROR['NUMBER'] . ': ' . $_ERROR['MESSAGE']); 310 | } 311 | 312 | 313 | // Send back in format that user requested 314 | if ($request['ALLDATA']) 315 | { 316 | if ($request['RETURNARRAY']) 317 | { 318 | $result['_ERROR'] = $_ERROR; 319 | $result['_INFO'] = $_INFO; 320 | } 321 | else 322 | { 323 | $result->_ERROR = $_ERROR; 324 | $result->_INFO = $_INFO; 325 | } 326 | return $result; 327 | } 328 | else 329 | { 330 | return $result; 331 | } 332 | 333 | 334 | } 335 | 336 | } // End of API class -------------------------------------------------------------------------------- /src/RocketCode/Shopify/ShopifyServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->bind('ShopifyAPI', function($app, $config = FALSE) 24 | { 25 | return new API($config); 26 | }); 27 | } 28 | 29 | public function boot() 30 | { 31 | AliasLoader::getInstance()->alias('ShopifyAPI', 'RocketCode\Shopify\API'); 32 | } 33 | 34 | /** 35 | * Get the services provided by the provider. 36 | * 37 | * @return array 38 | */ 39 | public function provides() 40 | { 41 | return ['ShopifyAPI', 'RocketCode\Shopify\API']; 42 | } 43 | 44 | } 45 | --------------------------------------------------------------------------------