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