Completed
Pull Request — master (#45)
by Chad
04:07
created

Client::startIndex()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 5
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
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
     * Performs a request to the given URI and returns the response.
287
     *
288
     * @param string $method The HTTP method of the request to send.
289
     * @param string $uri    A relative api URI to which the POST request will be made.
290
     * @param array  $data   Array of data to be sent as the POST body.
291
     *
292
     * @return Response
293
     */
294
    public function send(string $method, string $uri, array $data = null) : Response
295
    {
296
        return $this->end($this->startSend($method, $uri, $data));
297
    }
298
299
    /**
300
     * Starts a request to the given URI.
301
     *
302
     * @param string $method The HTTP method of the request to send.
303
     * @param string $uri    A relative api URI to which the POST request will be made.
304
     * @param array  $data   Array of data to be sent as the POST body.
305
     *
306
     * @return string opaque handle to be given to endDelete()
307
     */
308
    public function startSend(string $method, string $uri, array $data = null) : string
309
    {
310
        $url = "{$this->baseUrl}/{$uri}";
311
        $json = $data !== null ? json_encode($data) : null;
312
        return $this->start($url, $method, $json, ['Content-Type' => 'application/json']);
313
    }
314
315
    /**
316
     * Get response of start*() method
317
     *
318
     * @param string $handle opaque handle from start*()
319
     *
320
     * @return Response
321
     */
322
    public function end(string $handle) : Response
323
    {
324
        Util::ensure(
325
            true,
326
            array_key_exists($handle, $this->handles),
327
            '\InvalidArgumentException',
328
            ['$handle not found']
329
        );
330
331
        list($cachedResponse, $adapterHandle, $request) = $this->handles[$handle];
332
        unset($this->handles[$handle]);
333
334
        if ($cachedResponse !== null) {
335
            return Response::fromPsr7Response($cachedResponse);
336
        }
337
338
        $response = $this->adapter->end($adapterHandle);
339
340
        if (self::isExpiredToken($response)) {
341
            $this->refreshAccessToken();
342
            $headers = $request->getHeaders();
343
            $headers['Authorization'] = "Bearer {$this->accessToken}";
344
            $request = new Request(
345
                $request->getMethod(),
346
                $request->getUri(),
347
                $headers,
348
                $request->getBody()
349
            );
350
            $response = $this->adapter->end($this->adapter->start($request));
351
        }
352
353 View Code Duplication
        if (($this->cacheMode === self::CACHE_MODE_REFRESH
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
354
                || $this->cacheMode & self::CACHE_MODE_GET)
355
                && $request->getMethod() === 'GET') {
356
            $this->cache->set($this->getCacheKey($request), $response);
357
        }
358
359
        return Response::fromPsr7Response($response);
360
    }
361
362
    /**
363
     * Set the default headers
364
     *
365
     * @param array The default headers
366
     *
367
     * @return void
368
     */
369
    public function setDefaultHeaders(array $defaultHeaders)
370
    {
371
        $this->defaultHeaders = $defaultHeaders;
372
    }
373
374
    private static function isExpiredToken(ResponseInterface $response) : bool
375
    {
376
        if ($response->getStatusCode() !== 401) {
377
            return false;
378
        }
379
380
        $parsedJson = json_decode((string)$response->getBody(), true);
381
        $error = Arrays::get($parsedJson, 'error');
382
383
        if (is_array($error)) {
384
            $error = Arrays::get($error, 'code');
385
        }
386
387
        //This detects expired access tokens on Apigee
388
        if ($error !== null) {
389
            return $error === 'invalid_grant' || $error === 'invalid_token';
390
        }
391
392
        $fault = Arrays::get($parsedJson, 'fault');
393
        if ($fault === null) {
394
            return false;
395
        }
396
397
        $error = strtolower(Arrays::get($fault, 'faultstring', ''));
398
399
        return $error === 'invalid access token' || $error === 'access token expired';
400
    }
401
402
    /**
403
     * Obtains a new access token from the API
404
     *
405
     * @return void
406
     */
407
    private function refreshAccessToken()
408
    {
409
        $request = $this->authentication->getTokenRequest($this->baseUrl, $this->refreshToken);
410
        $response = $this->adapter->end($this->adapter->start($request));
411
412
        list($this->accessToken, $this->refreshToken, $expires) = Authentication::parseTokenResponse($response);
413
414 View Code Duplication
        if ($this->cache === self::CACHE_MODE_REFRESH || $this->cacheMode & self::CACHE_MODE_TOKEN) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
415
            $this->cache->set($this->getCacheKey($request), $response, $expires);
416
            return;
417
        }
418
    }
419
420
    /**
421
     * Helper method to set this clients access token from cache
422
     *
423
     * @return void
424
     */
425
    private function setTokenFromCache()
426
    {
427
        if (($this->cacheMode & self::CACHE_MODE_TOKEN) === 0) {
428
            return;
429
        }
430
431
        $cachedResponse = $this->cache->get(
432
            $this->getCacheKey($this->authentication->getTokenRequest($this->baseUrl, $this->refreshToken))
433
        );
434
        if ($cachedResponse === null) {
435
            return;
436
        }
437
438
        list($this->accessToken, $this->refreshToken, ) = Authentication::parseTokenResponse($cachedResponse);
439
    }
440
441
    /**
442
     * Calls adapter->start() using caches
443
     *
444
     * @param string $url
445
     * @param string $method
446
     * @param string|null $body
447
     * @param array $headers Authorization key will be overwritten with the bearer token, and Accept-Encoding wil be
448
     *                       overwritten with gzip.
449
     *
450
     * @return string opaque handle to be given to end()
451
     */
452
    private function start(string $url, string $method, string $body = null, array $headers = [])
453
    {
454
        $headers += $this->defaultHeaders;
455
        $headers['Accept-Encoding'] = 'gzip';
456
        if ($this->accessToken === null) {
457
            $this->setTokenFromCache();
458
        }
459
460
        if ($this->accessToken === null) {
461
            $this->refreshAccessToken();
462
        }
463
464
        $headers['Authorization'] = "Bearer {$this->accessToken}";
465
466
        $request = new Request($method, $url, $headers, $body);
467
468
        if ($request->getMethod() === 'GET' && $this->cacheMode & self::CACHE_MODE_GET) {
469
            $cached = $this->cache->get($this->getCacheKey($request));
470
            if ($cached !== null) {
471
                //The response is cache. Generate a key for the handles array
472
                $key = uniqid();
473
                $this->handles[$key] = [$cached, null, $request];
474
                return $key;
475
            }
476
        }
477
478
        $key = $this->adapter->start($request);
479
        $this->handles[$key] = [null, $key, $request];
480
        return $key;
481
    }
482
483
    private function getCacheKey(RequestInterface $request) : string
484
    {
485
        return CacheHelper::getCacheKey($request);
486
    }
487
}
488