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

Client   B

Complexity

Total Complexity 42

Size/Duplication

Total Lines 450
Duplicated Lines 2 %

Coupling/Cohesion

Components 1
Dependencies 9

Importance

Changes 0
Metric Value
wmc 42
lcom 1
cbo 9
dl 9
loc 450
rs 8.295
c 0
b 0
f 0

19 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 24 1
A getTokens() 0 4 1
A startIndex() 0 5 1
A index() 0 4 1
A startGet() 0 9 2
A get() 0 4 1
A startPost() 0 5 1
A post() 0 4 1
A startPut() 0 5 1
A put() 0 4 1
A startDelete() 0 10 3
A delete() 0 4 1
B end() 5 34 6
A setDefaultHeaders() 0 4 1
C isExpiredToken() 0 27 7
A refreshAccessToken() 4 12 3
A setTokenFromCache() 0 15 3
B start() 0 30 6
A getCacheKey() 0 4 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Client often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Client, and based on these observations, apply Extract Interface, too.

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