Completed
Pull Request — master (#47)
by Chad
05:30
created

Client::startDelete()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 10
rs 9.9332
c 0
b 0
f 0
cc 3
nc 4
nop 3
1
<?php
2
3
namespace TraderInteractive\Api;
4
5
use SubjectivePHP\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
     * Start an arbitrary POST request to the API.
404
     *
405
     * @param string $relativePath The URI path relative to the base API url.
406
     * @param array  $data         Array of data to be sent as the POST body.
407
     *
408
     * @return Response
409
     */
410
    public function request(string $relativePath, array $data = null): Response
411
    {
412
        return $this->end($this->startRequest($relativePath, $data));
413
    }
414
415
    /**
416
     * Send an arbitrary POST request to the API.
417
     *
418
     * @param string $relativePath The URI path relative to the base API url.
419
     * @param array  $data         Array of data to be sent as the POST body.
420
     *
421
     * @return string opaque handle to be given to end()
422
     */
423
    public function startRequest(string $relativePath, array $data = null) : string
424
    {
425
        $url = "{$this->baseUrl}/{$relativePath}";
426
        $json = $data === null ? null : json_encode($data);
427
        return $this->start($url, 'POST', $json, ['Content-Type' => 'application/json']);
428
    }
429
430
    /**
431
     * Obtains a new access token from the API
432
     *
433
     * @return void
434
     */
435
    private function refreshAccessToken()
436
    {
437
        $request = $this->authentication->getTokenRequest($this->baseUrl, $this->refreshToken);
438
        $response = $this->adapter->end($this->adapter->start($request));
439
440
        list($this->accessToken, $this->refreshToken, $expires) = Authentication::parseTokenResponse($response);
441
442 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...
443
            $this->cache->set($this->getCacheKey($request), $response, $expires);
444
            return;
445
        }
446
    }
447
448
    /**
449
     * Helper method to set this clients access token from cache
450
     *
451
     * @return void
452
     */
453
    private function setTokenFromCache()
454
    {
455
        if (($this->cacheMode & self::CACHE_MODE_TOKEN) === 0) {
456
            return;
457
        }
458
459
        $cachedResponse = $this->cache->get(
460
            $this->getCacheKey($this->authentication->getTokenRequest($this->baseUrl, $this->refreshToken))
461
        );
462
        if ($cachedResponse === null) {
463
            return;
464
        }
465
466
        list($this->accessToken, $this->refreshToken, ) = Authentication::parseTokenResponse($cachedResponse);
467
    }
468
469
    /**
470
     * Calls adapter->start() using caches
471
     *
472
     * @param string $url
473
     * @param string $method
474
     * @param string|null $body
475
     * @param array $headers Authorization key will be overwritten with the bearer token, and Accept-Encoding wil be
476
     *                       overwritten with gzip.
477
     *
478
     * @return string opaque handle to be given to end()
479
     */
480
    private function start(string $url, string $method, string $body = null, array $headers = [])
481
    {
482
        $headers += $this->defaultHeaders;
483
        $headers['Accept-Encoding'] = 'gzip';
484
        if ($this->accessToken === null) {
485
            $this->setTokenFromCache();
486
        }
487
488
        if ($this->accessToken === null) {
489
            $this->refreshAccessToken();
490
        }
491
492
        $headers['Authorization'] = "Bearer {$this->accessToken}";
493
494
        $request = new Request($method, $url, $headers, $body);
495
496
        if ($request->getMethod() === 'GET' && $this->cacheMode & self::CACHE_MODE_GET) {
497
            $cached = $this->cache->get($this->getCacheKey($request));
498
            if ($cached !== null) {
499
                //The response is cache. Generate a key for the handles array
500
                $key = uniqid();
501
                $this->handles[$key] = [$cached, null, $request];
502
                return $key;
503
            }
504
        }
505
506
        $key = $this->adapter->start($request);
507
        $this->handles[$key] = [null, $key, $request];
508
        return $key;
509
    }
510
511
    private function getCacheKey(RequestInterface $request) : string
512
    {
513
        return CacheHelper::getCacheKey($request);
514
    }
515
}
516