Completed
Push — master ( d75a47...7295a1 )
by Chad
10s
created

Client::startGet()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 9
rs 9.6666
c 0
b 0
f 0
cc 2
eloc 5
nc 2
nop 3
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
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...
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) {
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...
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