Completed
Pull Request — master (#42)
by Chad
01:29
created

Client::startPut()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
c 0
b 0
f 0
rs 9.4285
cc 1
eloc 4
nc 1
nop 3
1
<?php
2
3
namespace TraderInteractive\Api;
4
5
use Chadicus\Psr\SimpleCache\NullCache;
6
use DominionEnterprises\Util;
7
use DominionEnterprises\Util\Arrays;
8
use DominionEnterprises\Util\Http;
9
use Psr\SimpleCache\CacheInterface;
10
11
/**
12
 * Client for apis
13
 */
14
final class Client implements ClientInterface
15
{
16
    /**
17
     * Flag to cache no requests
18
     *
19
     * @const int
20
     */
21
    const CACHE_MODE_NONE = 0;
22
23
    /**
24
     * Flag to cache only GET requests
25
     *
26
     * @const int
27
     */
28
    const CACHE_MODE_GET = 1;
29
30
    /**
31
     * Flag to cache only TOKEN requests
32
     *
33
     * @const int
34
     */
35
    const CACHE_MODE_TOKEN = 2;
36
37
    /**
38
     * Flag to cache ALL requests
39
     *
40
     * @const int
41
     */
42
    const CACHE_MODE_ALL = 3;
43
44
    /**
45
     * Flag to refresh cache on ALL requests
46
     *
47
     * @const int
48
     */
49
    const CACHE_MODE_REFRESH = 4;
50
51
    /**
52
     * @var array
53
     */
54
    const CACHE_MODES = [
55
        self::CACHE_MODE_NONE,
56
        self::CACHE_MODE_GET,
57
        self::CACHE_MODE_TOKEN,
58
        self::CACHE_MODE_ALL,
59
        self::CACHE_MODE_REFRESH,
60
    ];
61
62
    /**
63
     * Base url of the API server
64
     *
65
     * @var string
66
     */
67
    private $baseUrl;
68
69
    /**
70
     * HTTP Adapter for sending request to the api
71
     *
72
     * @var Adapter
73
     */
74
    private $adapter;
75
76
    /**
77
     * Oauth authentication implementation
78
     *
79
     * @var Authentication
80
     */
81
    private $authentication;
82
83
    /**
84
     * API access token
85
     *
86
     * @var string
87
     */
88
    private $accessToken;
89
90
    /**
91
     * API refresh token
92
     *
93
     * @var string
94
     */
95
    private $refreshToken;
96
97
    /**
98
     * Storage for cached API responses
99
     *
100
     * @var Cache
101
     */
102
    private $cache;
103
104
    /**
105
     * Strategy for caching
106
     *
107
     * @var int
108
     */
109
    private $cacheMode;
110
111
    /**
112
     * Handles set in start()
113
     *
114
     * @var array like [opaqueKey => [cached response (Response), adapter handle (opaque), Request]]
115
     */
116
    private $handles = [];
117
118
    /**
119
     * Array of headers that are passed on every request unless they are overridden
120
     *
121
     * @var array
122
     */
123
    private $defaultHeaders = [];
124
125
    /**
126
     * Create a new instance of Client
127
     *
128
     * @param Adapter $adapter
129
     * @param Authentication $authentication
130
     * @param string $baseUrl
131
     * @param int $cacheMode
132
     * @param Cache $cache
133
     * @param string $accessToken
134
     * @param string $refreshToken
135
     *
136
     * @throws \InvalidArgumentException Thrown if $baseUrl is not a non-empty string
137
     * @throws \InvalidArgumentException Thrown if $cacheMode is not one of the cache mode constants
138
     */
139
    public function __construct(
140
        Adapter $adapter,
141
        Authentication $authentication,
142
        $baseUrl,
143
        $cacheMode = self::CACHE_MODE_NONE,
144
        CacheInterface $cache = null,
145
        $accessToken = null,
146
        $refreshToken = null
147
    ) {
148
        Util::throwIfNotType(['string' => [$baseUrl]], true);
149
        Util::throwIfNotType(['string' => [$accessToken, $refreshToken]], true, true);
150
        Util::ensure(
151
            true,
152
            in_array($cacheMode, self::CACHE_MODES, true),
153
            '\InvalidArgumentException',
154
            ['$cacheMode must be a valid cache mode constant']
155
        );
156
157
        $this->adapter = $adapter;
158
        $this->baseUrl = $baseUrl;
159
        $this->authentication = $authentication;
160
        $this->cache = $cache ?? new NullCache();
0 ignored issues
show
Documentation Bug introduced by
It seems like $cache ?? new \Chadicus\...SimpleCache\NullCache() of type object<Psr\SimpleCache\CacheInterface> is incompatible with the declared type object<TraderInteractive\Api\Cache> of property $cache.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
161
        $this->cacheMode = $cacheMode;
162
        $this->accessToken = $accessToken;
163
        $this->refreshToken = $refreshToken;
164
    }
165
166
    /**
167
     * Get access token and refresh token
168
     *
169
     * @return array two string values, access token and refresh token
170
     */
171
    public function getTokens()
172
    {
173
        return [$this->accessToken, $this->refreshToken];
174
    }
175
176
    /**
177
     * Search the API resource using the specified $filters
178
     *
179
     * @param string $resource
180
     * @param array $filters
181
     *
182
     * @return mixed opaque handle to be given to endIndex()
183
     */
184
    public function startIndex($resource, array $filters = [])
185
    {
186
        Util::throwIfNotType(['string' => [$resource]], true);
187
        $url = "{$this->baseUrl}/" . urlencode($resource) . '?' . Http::buildQueryString($filters);
188
        return $this->start($url, 'GET');
189
    }
190
191
    /**
192
     * @see startIndex()
193
     */
194
    public function index($resource, array $filters = [])
195
    {
196
        return $this->end($this->startIndex($resource, $filters));
197
    }
198
199
    /**
200
     * Get the details of an API resource based on $id
201
     *
202
     * @param string $resource
203
     * @param string $id
204
     * @param array  $parameters
205
     *
206
     * @return mixed opaque handle to be given to endGet()
207
     */
208
    public function startGet($resource, $id, array $parameters = [])
209
    {
210
        Util::throwIfNotType(['string' => [$resource, $id]], true);
211
        $url = "{$this->baseUrl}/" . urlencode($resource) . '/' . urlencode($id);
212
        if (!empty($parameters)) {
213
            $url .= '?' . Http::buildQueryString($parameters);
214
        }
215
216
        return $this->start($url, 'GET');
217
    }
218
219
    /**
220
     * @see startGet()
221
     */
222
    public function get($resource, $id, array $parameters = [])
223
    {
224
        return $this->end($this->startGet($resource, $id, $parameters));
225
    }
226
227
    /**
228
     * Create a new instance of an API resource using the provided $data
229
     *
230
     * @param string $resource
231
     * @param array $data
232
     *
233
     * @return mixed opaque handle to be given to endPost()
234
     */
235
    public function startPost($resource, array $data)
236
    {
237
        Util::throwIfNotType(['string' => [$resource]], true);
238
        $url = "{$this->baseUrl}/" . urlencode($resource);
239
        return $this->start($url, 'POST', json_encode($data), ['Content-Type' => 'application/json']);
240
    }
241
242
    /**
243
     * @see startPost()
244
     */
245
    public function post($resource, array $data)
246
    {
247
        return $this->end($this->startPost($resource, $data));
248
    }
249
250
    /**
251
     * Update an existing instance of an API resource specified by $id with the provided $data
252
     *
253
     * @param string $resource
254
     * @param string $id
255
     * @param array $data
256
     *
257
     * @return mixed opaque handle to be given to endPut()
258
     */
259
    public function startPut($resource, $id, array $data)
260
    {
261
        Util::throwIfNotType(['string' => [$resource, $id]], true);
262
        $url = "{$this->baseUrl}/" . urlencode($resource) . '/' . urlencode($id);
263
        return $this->start($url, 'PUT', json_encode($data), ['Content-Type' => 'application/json']);
264
    }
265
266
    /**
267
     * @see startPut()
268
     */
269
    public function put($resource, $id, array $data)
270
    {
271
        return $this->end($this->startPut($resource, $id, $data));
272
    }
273
274
    /**
275
     * Delete an existing instance of an API resource specified by $id
276
     *
277
     * @param string $resource
278
     * @param string $id
279
     * @param array $data
280
     *
281
     * @return mixed opaque handle to be given to endDelete()
282
     */
283
    public function startDelete($resource, $id = null, array $data = null)
284
    {
285
        Util::throwIfNotType(['string' => [$resource]], true);
286
        $url = "{$this->baseUrl}/" . urlencode($resource);
287
        if ($id !== null) {
288
            Util::throwIfNotType(['string' => [$id]], true);
289
            $url .= '/' . urlencode($id);
290
        }
291
292
        $json = $data !== null ? json_encode($data) : null;
293
        return $this->start($url, 'DELETE', $json, ['Content-Type' => 'application/json']);
294
    }
295
296
    /**
297
     * @see startDelete()
298
     */
299
    public function delete($resource, $id = null, array $data = null)
300
    {
301
        return $this->end($this->startDelete($resource, $id, $data));
302
    }
303
304
    /**
305
     * Get response of start*() method
306
     *
307
     * @param mixed $handle opaque handle from start*()
308
     *
309
     * @return Response
310
     */
311
    public function end($handle)
312
    {
313
        Util::ensure(
314
            true,
315
            array_key_exists($handle, $this->handles),
316
            '\InvalidArgumentException',
317
            ['$handle not found']
318
        );
319
320
        list($cachedResponse, $adapterHandle, $request) = $this->handles[$handle];
321
        unset($this->handles[$handle]);
322
323
        if ($cachedResponse !== null) {
324
            return $cachedResponse;
325
        }
326
327
        $response = $this->adapter->end($adapterHandle);
328
329
        if (self::isExpiredToken($response)) {
330
            $this->refreshAccessToken();
331
            $headers = $request->getHeaders();
332
            $headers['Authorization'] = "Bearer {$this->accessToken}";
333
            $request = new Request($request->getUrl(), $request->getMethod(), $request->getBody(), $headers);
334
            $response = $this->adapter->end($this->adapter->start($request));
335
        }
336
337 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...
338
                || $this->cacheMode & self::CACHE_MODE_GET)
339
                && $request->getMethod() === 'GET') {
340
            $this->cache->set($this->getCacheKey($request), $response);
341
        }
342
343
        return $response;
344
    }
345
346
    /**
347
     * Set the default headers
348
     *
349
     * @param array The default headers
350
     *
351
     * @return void
352
     */
353
    public function setDefaultHeaders($defaultHeaders)
354
    {
355
        $this->defaultHeaders = $defaultHeaders;
356
    }
357
358
    private static function isExpiredToken(Response $response)
359
    {
360
        if ($response->getHttpCode() !== 401) {
361
            return false;
362
        }
363
364
        $parsedJson = $response->getResponse();
365
        $error = Arrays::get($parsedJson, 'error');
366
367
        if (is_array($error)) {
368
            $error = Arrays::get($error, 'code');
369
        }
370
371
        //This detects expired access tokens on Apigee
372
        if ($error !== null) {
373
            return $error === 'invalid_grant' || $error === 'invalid_token';
374
        }
375
376
        $fault = Arrays::get($parsedJson, 'fault');
377
        if ($fault === null) {
378
            return false;
379
        }
380
381
        $error = strtolower(Arrays::get($fault, 'faultstring', ''));
382
383
        return $error === 'invalid access token' || $error === 'access token expired';
384
    }
385
386
    /**
387
     * Obtains a new access token from the API
388
     *
389
     * @return void
390
     */
391
    private function refreshAccessToken()
392
    {
393
        $request = $this->authentication->getTokenRequest($this->baseUrl, $this->refreshToken);
394
        $response = $this->adapter->end($this->adapter->start($request));
395
396
        list($this->accessToken, $this->refreshToken, $expires) = Authentication::parseTokenResponse($response);
397
398 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...
399
            $this->cache->set($this->getCacheKey($request), $response, $expires);
400
            return;
401
        }
402
    }
403
404
    /**
405
     * Helper method to set this clients access token from cache
406
     *
407
     * @return void
408
     */
409
    private function setTokenFromCache()
410
    {
411
        if (($this->cacheMode & self::CACHE_MODE_TOKEN) === 0) {
412
            return;
413
        }
414
415
        $cachedResponse = $this->cache->get(
416
            $this->getCacheKey($this->authentication->getTokenRequest($this->baseUrl, $this->refreshToken))
417
        );
418
        if ($cachedResponse === null) {
419
            return;
420
        }
421
422
        list($this->accessToken, $this->refreshToken, ) = Authentication::parseTokenResponse($cachedResponse);
423
    }
424
425
    /**
426
     * Calls adapter->start() using caches
427
     *
428
     * @param string $url
429
     * @param string $method
430
     * @param string|null $body
431
     * @param array $headers Authorization key will be overwritten with the bearer token, and Accept-Encoding wil be
432
     *                       overwritten with gzip.
433
     *
434
     * @return mixed opaque handle to be given to end()
435
     */
436
    private function start($url, $method, $body = null, array $headers = [])
437
    {
438
        $headers += $this->defaultHeaders;
439
        $headers['Accept-Encoding'] = 'gzip';
440
        if ($this->accessToken === null) {
441
            $this->setTokenFromCache();
442
        }
443
444
        if ($this->accessToken === null) {
445
            $this->refreshAccessToken();
446
        }
447
448
        $headers['Authorization'] = "Bearer {$this->accessToken}";
449
450
        $request = new Request($url, $method, $body, $headers);
451
452
        if ($request->getMethod() === 'GET' && $this->cacheMode & self::CACHE_MODE_GET) {
453
            $cached = $this->cache->get($this->getCacheKey($request));
454
            if ($cached !== null) {
455
                //The response is cache. Generate a key for the handles array
456
                $key = uniqid();
457
                $this->handles[$key] = [$cached, null, $request];
458
                return $key;
459
            }
460
        }
461
462
        $key = $this->adapter->start($request);
463
        $this->handles[$key] = [null, $key, $request];
464
        return $key;
465
    }
466
467
    private function getCacheKey(Request $request) : string
468
    {
469
        return CacheHelper::getCacheKey($request);
470
    }
471
}
472