1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace TraderInteractive\Api; |
4
|
|
|
|
5
|
|
|
use DominionEnterprises\Util; |
6
|
|
|
use DominionEnterprises\Util\Arrays; |
7
|
|
|
use DominionEnterprises\Util\Http; |
8
|
|
|
|
9
|
|
|
/** |
10
|
|
|
* Client for apis |
11
|
|
|
*/ |
12
|
|
|
final class Client implements ClientInterface |
13
|
|
|
{ |
14
|
|
|
/** |
15
|
|
|
* Flag to cache no requests |
16
|
|
|
* |
17
|
|
|
* @const int |
18
|
|
|
*/ |
19
|
|
|
const CACHE_MODE_NONE = 0; |
20
|
|
|
|
21
|
|
|
/** |
22
|
|
|
* Flag to cache only GET requests |
23
|
|
|
* |
24
|
|
|
* @const int |
25
|
|
|
*/ |
26
|
|
|
const CACHE_MODE_GET = 1; |
27
|
|
|
|
28
|
|
|
/** |
29
|
|
|
* Flag to cache only TOKEN requests |
30
|
|
|
* |
31
|
|
|
* @const int |
32
|
|
|
*/ |
33
|
|
|
const CACHE_MODE_TOKEN = 2; |
34
|
|
|
|
35
|
|
|
/** |
36
|
|
|
* Flag to cache ALL requests |
37
|
|
|
* |
38
|
|
|
* @const int |
39
|
|
|
*/ |
40
|
|
|
const CACHE_MODE_ALL = 3; |
41
|
|
|
|
42
|
|
|
/** |
43
|
|
|
* Flag to refresh cache on ALL requests |
44
|
|
|
* |
45
|
|
|
* @const int |
46
|
|
|
*/ |
47
|
|
|
const CACHE_MODE_REFRESH = 4; |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* Base url of the API server |
51
|
|
|
* |
52
|
|
|
* @var string |
53
|
|
|
*/ |
54
|
|
|
private $baseUrl; |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* HTTP Adapter for sending request to the api |
58
|
|
|
* |
59
|
|
|
* @var Adapter |
60
|
|
|
*/ |
61
|
|
|
private $adapter; |
62
|
|
|
|
63
|
|
|
/** |
64
|
|
|
* Oauth authentication implementation |
65
|
|
|
* |
66
|
|
|
* @var Authentication |
67
|
|
|
*/ |
68
|
|
|
private $authentication; |
69
|
|
|
|
70
|
|
|
/** |
71
|
|
|
* API access token |
72
|
|
|
* |
73
|
|
|
* @var string |
74
|
|
|
*/ |
75
|
|
|
private $accessToken; |
76
|
|
|
|
77
|
|
|
/** |
78
|
|
|
* API refresh token |
79
|
|
|
* |
80
|
|
|
* @var string |
81
|
|
|
*/ |
82
|
|
|
private $refreshToken; |
83
|
|
|
|
84
|
|
|
/** |
85
|
|
|
* Storage for cached API responses |
86
|
|
|
* |
87
|
|
|
* @var Cache |
88
|
|
|
*/ |
89
|
|
|
private $cache; |
90
|
|
|
|
91
|
|
|
/** |
92
|
|
|
* Strategy for caching |
93
|
|
|
* |
94
|
|
|
* @var int |
95
|
|
|
*/ |
96
|
|
|
private $cacheMode; |
97
|
|
|
|
98
|
|
|
/** |
99
|
|
|
* Handles set in start() |
100
|
|
|
* |
101
|
|
|
* @var array like [opaqueKey => [cached response (Response), adapter handle (opaque), Request]] |
102
|
|
|
*/ |
103
|
|
|
private $handles = []; |
104
|
|
|
|
105
|
|
|
/** |
106
|
|
|
* Array of headers that are passed on every request unless they are overridden |
107
|
|
|
* |
108
|
|
|
* @var array |
109
|
|
|
*/ |
110
|
|
|
private $defaultHeaders = []; |
111
|
|
|
|
112
|
|
|
/** |
113
|
|
|
* Create a new instance of Client |
114
|
|
|
* |
115
|
|
|
* @param Adapter $adapter |
116
|
|
|
* @param Authentication $authentication |
117
|
|
|
* @param string $baseUrl |
118
|
|
|
* @param int $cacheMode |
119
|
|
|
* @param Cache $cache |
120
|
|
|
* @param string $accessToken |
121
|
|
|
* @param string $refreshToken |
122
|
|
|
* |
123
|
|
|
* @throws \InvalidArgumentException Thrown if $baseUrl is not a non-empty string |
124
|
|
|
* @throws \InvalidArgumentException Thrown if $cacheMode is not one of the cache mode constants |
125
|
|
|
*/ |
126
|
|
|
public function __construct( |
127
|
|
|
Adapter $adapter, |
128
|
|
|
Authentication $authentication, |
129
|
|
|
$baseUrl, |
130
|
|
|
$cacheMode = self::CACHE_MODE_NONE, |
131
|
|
|
Cache $cache = null, |
132
|
|
|
$accessToken = null, |
133
|
|
|
$refreshToken = null |
134
|
|
|
) { |
135
|
|
|
Util::throwIfNotType(['string' => [$baseUrl]], true); |
136
|
|
|
Util::throwIfNotType(['string' => [$accessToken, $refreshToken]], true, true); |
137
|
|
|
Util::ensure( |
138
|
|
|
true, |
139
|
|
|
in_array( |
140
|
|
|
$cacheMode, |
141
|
|
|
[ |
142
|
|
|
self::CACHE_MODE_NONE, |
143
|
|
|
self::CACHE_MODE_GET, |
144
|
|
|
self::CACHE_MODE_TOKEN, |
145
|
|
|
self::CACHE_MODE_ALL, |
146
|
|
|
self::CACHE_MODE_REFRESH, |
147
|
|
|
], |
148
|
|
|
true |
149
|
|
|
), |
150
|
|
|
'\InvalidArgumentException', |
151
|
|
|
['$cacheMode must be a valid cache mode constant'] |
152
|
|
|
); |
153
|
|
|
|
154
|
|
|
if ($cacheMode !== self::CACHE_MODE_NONE) { |
155
|
|
|
Util::ensureNot( |
156
|
|
|
null, |
157
|
|
|
$cache, |
158
|
|
|
'\InvalidArgumentException', |
159
|
|
|
['$cache must not be null if $cacheMode is not CACHE_MODE_NONE'] |
160
|
|
|
); |
161
|
|
|
} |
162
|
|
|
|
163
|
|
|
$this->adapter = $adapter; |
164
|
|
|
$this->baseUrl = $baseUrl; |
165
|
|
|
$this->authentication = $authentication; |
166
|
|
|
$this->cache = $cache; |
167
|
|
|
$this->cacheMode = $cacheMode; |
168
|
|
|
$this->accessToken = $accessToken; |
169
|
|
|
$this->refreshToken = $refreshToken; |
170
|
|
|
} |
171
|
|
|
|
172
|
|
|
/** |
173
|
|
|
* Get access token and refresh token |
174
|
|
|
* |
175
|
|
|
* @return array two string values, access token and refresh token |
176
|
|
|
*/ |
177
|
|
|
public function getTokens() |
178
|
|
|
{ |
179
|
|
|
return [$this->accessToken, $this->refreshToken]; |
180
|
|
|
} |
181
|
|
|
|
182
|
|
|
/** |
183
|
|
|
* Search the API resource using the specified $filters |
184
|
|
|
* |
185
|
|
|
* @param string $resource |
186
|
|
|
* @param array $filters |
187
|
|
|
* |
188
|
|
|
* @return mixed opaque handle to be given to endIndex() |
189
|
|
|
*/ |
190
|
|
|
public function startIndex($resource, array $filters = []) |
191
|
|
|
{ |
192
|
|
|
Util::throwIfNotType(['string' => [$resource]], true); |
193
|
|
|
$url = "{$this->baseUrl}/" . urlencode($resource) . '?' . Http::buildQueryString($filters); |
194
|
|
|
return $this->start($url, 'GET'); |
195
|
|
|
} |
196
|
|
|
|
197
|
|
|
/** |
198
|
|
|
* @see startIndex() |
199
|
|
|
*/ |
200
|
|
|
public function index($resource, array $filters = []) |
201
|
|
|
{ |
202
|
|
|
return $this->end($this->startIndex($resource, $filters)); |
203
|
|
|
} |
204
|
|
|
|
205
|
|
|
/** |
206
|
|
|
* Get the details of an API resource based on $id |
207
|
|
|
* |
208
|
|
|
* @param string $resource |
209
|
|
|
* @param string $id |
210
|
|
|
* @param array $parameters |
211
|
|
|
* |
212
|
|
|
* @return mixed opaque handle to be given to endGet() |
213
|
|
|
*/ |
214
|
|
|
public function startGet($resource, $id, array $parameters = []) |
215
|
|
|
{ |
216
|
|
|
Util::throwIfNotType(['string' => [$resource, $id]], true); |
217
|
|
|
$url = "{$this->baseUrl}/" . urlencode($resource) . '/' . urlencode($id); |
218
|
|
|
if (!empty($parameters)) { |
219
|
|
|
$url .= '?' . Http::buildQueryString($parameters); |
220
|
|
|
} |
221
|
|
|
|
222
|
|
|
return $this->start($url, 'GET'); |
223
|
|
|
} |
224
|
|
|
|
225
|
|
|
/** |
226
|
|
|
* @see startGet() |
227
|
|
|
*/ |
228
|
|
|
public function get($resource, $id, array $parameters = []) |
229
|
|
|
{ |
230
|
|
|
return $this->end($this->startGet($resource, $id, $parameters)); |
231
|
|
|
} |
232
|
|
|
|
233
|
|
|
/** |
234
|
|
|
* Create a new instance of an API resource using the provided $data |
235
|
|
|
* |
236
|
|
|
* @param string $resource |
237
|
|
|
* @param array $data |
238
|
|
|
* |
239
|
|
|
* @return mixed opaque handle to be given to endPost() |
240
|
|
|
*/ |
241
|
|
|
public function startPost($resource, array $data) |
242
|
|
|
{ |
243
|
|
|
Util::throwIfNotType(['string' => [$resource]], true); |
244
|
|
|
$url = "{$this->baseUrl}/" . urlencode($resource); |
245
|
|
|
return $this->start($url, 'POST', json_encode($data), ['Content-Type' => 'application/json']); |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
/** |
249
|
|
|
* @see startPost() |
250
|
|
|
*/ |
251
|
|
|
public function post($resource, array $data) |
252
|
|
|
{ |
253
|
|
|
return $this->end($this->startPost($resource, $data)); |
254
|
|
|
} |
255
|
|
|
|
256
|
|
|
/** |
257
|
|
|
* Update an existing instance of an API resource specified by $id with the provided $data |
258
|
|
|
* |
259
|
|
|
* @param string $resource |
260
|
|
|
* @param string $id |
261
|
|
|
* @param array $data |
262
|
|
|
* |
263
|
|
|
* @return mixed opaque handle to be given to endPut() |
264
|
|
|
*/ |
265
|
|
|
public function startPut($resource, $id, array $data) |
266
|
|
|
{ |
267
|
|
|
Util::throwIfNotType(['string' => [$resource, $id]], true); |
268
|
|
|
$url = "{$this->baseUrl}/" . urlencode($resource) . '/' . urlencode($id); |
269
|
|
|
return $this->start($url, 'PUT', json_encode($data), ['Content-Type' => 'application/json']); |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
/** |
273
|
|
|
* @see startPut() |
274
|
|
|
*/ |
275
|
|
|
public function put($resource, $id, array $data) |
276
|
|
|
{ |
277
|
|
|
return $this->end($this->startPut($resource, $id, $data)); |
278
|
|
|
} |
279
|
|
|
|
280
|
|
|
/** |
281
|
|
|
* Delete an existing instance of an API resource specified by $id |
282
|
|
|
* |
283
|
|
|
* @param string $resource |
284
|
|
|
* @param string $id |
285
|
|
|
* @param array $data |
286
|
|
|
* |
287
|
|
|
* @return mixed opaque handle to be given to endDelete() |
288
|
|
|
*/ |
289
|
|
|
public function startDelete($resource, $id = null, array $data = null) |
290
|
|
|
{ |
291
|
|
|
Util::throwIfNotType(['string' => [$resource]], true); |
292
|
|
|
$url = "{$this->baseUrl}/" . urlencode($resource); |
293
|
|
|
if ($id !== null) { |
294
|
|
|
Util::throwIfNotType(['string' => [$id]], true); |
295
|
|
|
$url .= '/' . urlencode($id); |
296
|
|
|
} |
297
|
|
|
|
298
|
|
|
$json = $data !== null ? json_encode($data) : null; |
299
|
|
|
return $this->start($url, 'DELETE', $json, ['Content-Type' => 'application/json']); |
300
|
|
|
} |
301
|
|
|
|
302
|
|
|
/** |
303
|
|
|
* @see startDelete() |
304
|
|
|
*/ |
305
|
|
|
public function delete($resource, $id = null, array $data = null) |
306
|
|
|
{ |
307
|
|
|
return $this->end($this->startDelete($resource, $id, $data)); |
308
|
|
|
} |
309
|
|
|
|
310
|
|
|
/** |
311
|
|
|
* Get response of start*() method |
312
|
|
|
* |
313
|
|
|
* @param mixed $handle opaque handle from start*() |
314
|
|
|
* |
315
|
|
|
* @return Response |
316
|
|
|
*/ |
317
|
|
|
public function end($handle) |
318
|
|
|
{ |
319
|
|
|
Util::ensure( |
320
|
|
|
true, |
321
|
|
|
array_key_exists($handle, $this->handles), |
322
|
|
|
'\InvalidArgumentException', |
323
|
|
|
['$handle not found'] |
324
|
|
|
); |
325
|
|
|
|
326
|
|
|
list($cachedResponse, $adapterHandle, $request) = $this->handles[$handle]; |
327
|
|
|
unset($this->handles[$handle]); |
328
|
|
|
|
329
|
|
|
if ($cachedResponse !== null) { |
330
|
|
|
return $cachedResponse; |
331
|
|
|
} |
332
|
|
|
|
333
|
|
|
$response = $this->adapter->end($adapterHandle); |
334
|
|
|
|
335
|
|
|
if (self::isExpiredToken($response)) { |
336
|
|
|
$this->refreshAccessToken(); |
337
|
|
|
$headers = $request->getHeaders(); |
338
|
|
|
$headers['Authorization'] = "Bearer {$this->accessToken}"; |
339
|
|
|
$request = new Request($request->getUrl(), $request->getMethod(), $request->getBody(), $headers); |
340
|
|
|
$response = $this->adapter->end($this->adapter->start($request)); |
341
|
|
|
} |
342
|
|
|
|
343
|
|
View Code Duplication |
if (($this->cacheMode === self::CACHE_MODE_REFRESH |
|
|
|
|
344
|
|
|
|| $this->cacheMode & self::CACHE_MODE_GET) |
345
|
|
|
&& $request->getMethod() === 'GET') { |
346
|
|
|
$this->cache->set($request, $response); |
347
|
|
|
} |
348
|
|
|
|
349
|
|
|
return $response; |
350
|
|
|
} |
351
|
|
|
|
352
|
|
|
/** |
353
|
|
|
* Set the default headers |
354
|
|
|
* |
355
|
|
|
* @param array The default headers |
356
|
|
|
* |
357
|
|
|
* @return void |
358
|
|
|
*/ |
359
|
|
|
public function setDefaultHeaders($defaultHeaders) |
360
|
|
|
{ |
361
|
|
|
$this->defaultHeaders = $defaultHeaders; |
362
|
|
|
} |
363
|
|
|
|
364
|
|
|
private static function isExpiredToken(Response $response) |
365
|
|
|
{ |
366
|
|
|
if ($response->getHttpCode() !== 401) { |
367
|
|
|
return false; |
368
|
|
|
} |
369
|
|
|
|
370
|
|
|
$parsedJson = $response->getResponse(); |
371
|
|
|
$error = Arrays::get($parsedJson, 'error'); |
372
|
|
|
|
373
|
|
|
if (is_array($error)) { |
374
|
|
|
$error = Arrays::get($error, 'code'); |
375
|
|
|
} |
376
|
|
|
|
377
|
|
|
//This detects expired access tokens on Apigee |
378
|
|
|
if ($error !== null) { |
379
|
|
|
return $error === 'invalid_grant' || $error === 'invalid_token'; |
380
|
|
|
} |
381
|
|
|
|
382
|
|
|
$fault = Arrays::get($parsedJson, 'fault'); |
383
|
|
|
if ($fault === null) { |
384
|
|
|
return false; |
385
|
|
|
} |
386
|
|
|
|
387
|
|
|
$error = strtolower(Arrays::get($fault, 'faultstring', '')); |
388
|
|
|
|
389
|
|
|
return $error === 'invalid access token' || $error === 'access token expired'; |
390
|
|
|
} |
391
|
|
|
|
392
|
|
|
/** |
393
|
|
|
* Obtains a new access token from the API |
394
|
|
|
* |
395
|
|
|
* @return void |
396
|
|
|
*/ |
397
|
|
|
private function refreshAccessToken() |
398
|
|
|
{ |
399
|
|
|
$request = $this->authentication->getTokenRequest($this->baseUrl, $this->refreshToken); |
400
|
|
|
$response = $this->adapter->end($this->adapter->start($request)); |
401
|
|
|
|
402
|
|
|
list($this->accessToken, $this->refreshToken, $expires) = Authentication::parseTokenResponse($response); |
403
|
|
|
|
404
|
|
View Code Duplication |
if ($this->cache === self::CACHE_MODE_REFRESH || $this->cacheMode & self::CACHE_MODE_TOKEN) { |
|
|
|
|
405
|
|
|
$this->cache->set($request, $response, $expires); |
406
|
|
|
} |
407
|
|
|
} |
408
|
|
|
|
409
|
|
|
/** |
410
|
|
|
* Helper method to set this clients access token from cache |
411
|
|
|
* |
412
|
|
|
* @return void |
413
|
|
|
*/ |
414
|
|
|
private function setTokenFromCache() |
415
|
|
|
{ |
416
|
|
|
if (($this->cacheMode & self::CACHE_MODE_TOKEN) === 0) { |
417
|
|
|
return; |
418
|
|
|
} |
419
|
|
|
|
420
|
|
|
$cachedResponse = $this->cache->get( |
421
|
|
|
$this->authentication->getTokenRequest($this->baseUrl, $this->refreshToken) |
422
|
|
|
); |
423
|
|
|
if ($cachedResponse === null) { |
424
|
|
|
return; |
425
|
|
|
} |
426
|
|
|
|
427
|
|
|
list($this->accessToken, $this->refreshToken, ) = Authentication::parseTokenResponse($cachedResponse); |
428
|
|
|
} |
429
|
|
|
|
430
|
|
|
/** |
431
|
|
|
* Calls adapter->start() using caches |
432
|
|
|
* |
433
|
|
|
* @param string $url |
434
|
|
|
* @param string $method |
435
|
|
|
* @param string|null $body |
436
|
|
|
* @param array $headers Authorization key will be overwritten with the bearer token, and Accept-Encoding wil be |
437
|
|
|
* overwritten with gzip. |
438
|
|
|
* |
439
|
|
|
* @return mixed opaque handle to be given to end() |
440
|
|
|
*/ |
441
|
|
|
private function start($url, $method, $body = null, array $headers = []) |
442
|
|
|
{ |
443
|
|
|
$headers += $this->defaultHeaders; |
444
|
|
|
$headers['Accept-Encoding'] = 'gzip'; |
445
|
|
|
if ($this->accessToken === null) { |
446
|
|
|
$this->setTokenFromCache(); |
447
|
|
|
} |
448
|
|
|
|
449
|
|
|
if ($this->accessToken === null) { |
450
|
|
|
$this->refreshAccessToken(); |
451
|
|
|
} |
452
|
|
|
|
453
|
|
|
$headers['Authorization'] = "Bearer {$this->accessToken}"; |
454
|
|
|
|
455
|
|
|
$request = new Request($url, $method, $body, $headers); |
456
|
|
|
|
457
|
|
|
if ($request->getMethod() === 'GET' && $this->cacheMode & self::CACHE_MODE_GET) { |
458
|
|
|
$cached = $this->cache->get($request); |
459
|
|
|
if ($cached !== null) { |
460
|
|
|
//The response is cache. Generate a key for the handles array |
461
|
|
|
$key = uniqid(); |
462
|
|
|
$this->handles[$key] = [$cached, null, $request]; |
463
|
|
|
return $key; |
464
|
|
|
} |
465
|
|
|
} |
466
|
|
|
|
467
|
|
|
$key = $this->adapter->start($request); |
468
|
|
|
$this->handles[$key] = [null, $key, $request]; |
469
|
|
|
return $key; |
470
|
|
|
} |
471
|
|
|
} |
472
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.