Completed
Pull Request — master (#42)
by Chad
04:56
created

Client   B

Complexity

Total Complexity 42

Size/Duplication

Total Lines 455
Duplicated Lines 1.98 %

Coupling/Cohesion

Components 1
Dependencies 11

Importance

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

19 Methods

Rating   Name   Duplication   Size   Complexity  
A getTokens() 0 4 1
B __construct() 0 24 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 39 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 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\SimpleCache\CacheInterface;
12
13
/**
14
 * Client for apis
15
 */
16
final class Client implements ClientInterface
17
{
18
    /**
19
     * Flag to cache no requests
20
     *
21
     * @const int
22
     */
23
    const CACHE_MODE_NONE = 0;
24
25
    /**
26
     * Flag to cache only GET requests
27
     *
28
     * @const int
29
     */
30
    const CACHE_MODE_GET = 1;
31
32
    /**
33
     * Flag to cache only TOKEN requests
34
     *
35
     * @const int
36
     */
37
    const CACHE_MODE_TOKEN = 2;
38
39
    /**
40
     * Flag to cache ALL requests
41
     *
42
     * @const int
43
     */
44
    const CACHE_MODE_ALL = 3;
45
46
    /**
47
     * Flag to refresh cache on ALL requests
48
     *
49
     * @const int
50
     */
51
    const CACHE_MODE_REFRESH = 4;
52
53
    /**
54
     * @var array
55
     */
56
    const CACHE_MODES = [
57
        self::CACHE_MODE_NONE,
58
        self::CACHE_MODE_GET,
59
        self::CACHE_MODE_TOKEN,
60
        self::CACHE_MODE_ALL,
61
        self::CACHE_MODE_REFRESH,
62
    ];
63
64
    /**
65
     * Base url of the API server
66
     *
67
     * @var string
68
     */
69
    private $baseUrl;
70
71
    /**
72
     * HTTP Adapter for sending request to the api
73
     *
74
     * @var AdapterInterface
75
     */
76
    private $adapter;
77
78
    /**
79
     * Oauth authentication implementation
80
     *
81
     * @var Authentication
82
     */
83
    private $authentication;
84
85
    /**
86
     * API access token
87
     *
88
     * @var string
89
     */
90
    private $accessToken;
91
92
    /**
93
     * API refresh token
94
     *
95
     * @var string
96
     */
97
    private $refreshToken;
98
99
    /**
100
     * Storage for cached API responses
101
     *
102
     * @var CacheInterface
103
     */
104
    private $cache;
105
106
    /**
107
     * Strategy for caching
108
     *
109
     * @var int
110
     */
111
    private $cacheMode;
112
113
    /**
114
     * Handles set in start()
115
     *
116
     * @var array like [opaqueKey => [cached response (Response), adapter handle (opaque), Request]]
117
     */
118
    private $handles = [];
119
120
    /**
121
     * Array of headers that are passed on every request unless they are overridden
122
     *
123
     * @var array
124
     */
125
    private $defaultHeaders = [];
126
127
    /**
128
     * Create a new instance of Client
129
     *
130
     * @param AdapterInterface $adapter
131
     * @param Authentication $authentication
132
     * @param string $baseUrl
133
     * @param int $cacheMode
134
     * @param Cache $cache
135
     * @param string $accessToken
136
     * @param string $refreshToken
137
     *
138
     * @throws \InvalidArgumentException Thrown if $baseUrl is not a non-empty string
139
     * @throws \InvalidArgumentException Thrown if $cacheMode is not one of the cache mode constants
140
     */
141
    public function __construct(
142
        AdapterInterface $adapter,
143
        Authentication $authentication,
144
        string $baseUrl,
145
        int $cacheMode = self::CACHE_MODE_NONE,
146
        CacheInterface $cache = null,
147
        string $accessToken = null,
148
        string $refreshToken = null
149
    ) {
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();
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() : array
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 string opaque handle to be given to endIndex()
183
     */
184
    public function startIndex(string $resource, array $filters = []) : string
185
    {
186
        $url = "{$this->baseUrl}/" . urlencode($resource) . '?' . Http::buildQueryString($filters);
187
        return $this->start($url, 'GET');
188
    }
189
190
    /**
191
     * @see startIndex()
192
     */
193
    public function index(string $resource, array $filters = []) : Response
194
    {
195
        return $this->end($this->startIndex($resource, $filters));
196
    }
197
198
    /**
199
     * Get the details of an API resource based on $id
200
     *
201
     * @param string $resource
202
     * @param string $id
203
     * @param array  $parameters
204
     *
205
     * @return string opaque handle to be given to endGet()
206
     */
207
    public function startGet(string $resource, string $id, array $parameters = []) : string
208
    {
209
        $url = "{$this->baseUrl}/" . urlencode($resource) . '/' . urlencode($id);
210
        if (!empty($parameters)) {
211
            $url .= '?' . Http::buildQueryString($parameters);
212
        }
213
214
        return $this->start($url, 'GET');
215
    }
216
217
    /**
218
     * @see startGet()
219
     */
220
    public function get(string $resource, string $id, array $parameters = []) : Response
221
    {
222
        return $this->end($this->startGet($resource, $id, $parameters));
223
    }
224
225
    /**
226
     * Create a new instance of an API resource using the provided $data
227
     *
228
     * @param string $resource
229
     * @param array $data
230
     *
231
     * @return string opaque handle to be given to endPost()
232
     */
233
    public function startPost(string $resource, array $data) : string
234
    {
235
        $url = "{$this->baseUrl}/" . urlencode($resource);
236
        return $this->start($url, 'POST', json_encode($data), ['Content-Type' => 'application/json']);
237
    }
238
239
    /**
240
     * @see startPost()
241
     */
242
    public function post(string $resource, array $data) : Response
243
    {
244
        return $this->end($this->startPost($resource, $data));
245
    }
246
247
    /**
248
     * Update an existing instance of an API resource specified by $id with the provided $data
249
     *
250
     * @param string $resource
251
     * @param string $id
252
     * @param array $data
253
     *
254
     * @return string opaque handle to be given to endPut()
255
     */
256
    public function startPut(string $resource, string $id, array $data) : string
257
    {
258
        $url = "{$this->baseUrl}/" . urlencode($resource) . '/' . urlencode($id);
259
        return $this->start($url, 'PUT', json_encode($data), ['Content-Type' => 'application/json']);
260
    }
261
262
    /**
263
     * @see startPut()
264
     */
265
    public function put(string $resource, string $id, array $data) : Response
266
    {
267
        return $this->end($this->startPut($resource, $id, $data));
268
    }
269
270
    /**
271
     * Delete an existing instance of an API resource specified by $id
272
     *
273
     * @param string $resource
274
     * @param string $id
275
     * @param array $data
276
     *
277
     * @return string opaque handle to be given to endDelete()
278
     */
279
    public function startDelete(string $resource, string $id = null, array $data = null) : string
280
    {
281
        $url = "{$this->baseUrl}/" . urlencode($resource);
282
        if ($id !== null) {
283
            $url .= '/' . urlencode($id);
284
        }
285
286
        $json = $data !== null ? json_encode($data) : null;
287
        return $this->start($url, 'DELETE', $json, ['Content-Type' => 'application/json']);
288
    }
289
290
    /**
291
     * @see startDelete()
292
     */
293
    public function delete(string $resource, string $id = null, array $data = null) : Response
294
    {
295
        return $this->end($this->startDelete($resource, $id, $data));
296
    }
297
298
    /**
299
     * Get response of start*() method
300
     *
301
     * @param string $handle opaque handle from start*()
302
     *
303
     * @return Response
304
     */
305
    public function end(string $handle) : Response
306
    {
307
        Util::ensure(
308
            true,
309
            array_key_exists($handle, $this->handles),
310
            '\InvalidArgumentException',
311
            ['$handle not found']
312
        );
313
314
        list($cachedResponse, $adapterHandle, $request) = $this->handles[$handle];
315
        unset($this->handles[$handle]);
316
317
        if ($cachedResponse !== null) {
318
            return Response::fromPsr7Response($cachedResponse);
319
        }
320
321
        $response = $this->adapter->end($adapterHandle);
322
323
        if (self::isExpiredToken($response)) {
324
            $this->refreshAccessToken();
325
            $headers = $request->getHeaders();
326
            $headers['Authorization'] = "Bearer {$this->accessToken}";
327
            $request = new Request(
328
                $request->getMethod(),
329
                $request->getUri(),
330
                $headers,
331
                $request->getBody()
332
            );
333
            $response = $this->adapter->end($this->adapter->start($request));
334
        }
335
336 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...
337
                || $this->cacheMode & self::CACHE_MODE_GET)
338
                && $request->getMethod() === 'GET') {
339
            $this->cache->set($this->getCacheKey($request), $response);
340
        }
341
342
        return Response::fromPsr7Response($response);
343
    }
344
345
    /**
346
     * Set the default headers
347
     *
348
     * @param array The default headers
349
     *
350
     * @return void
351
     */
352
    public function setDefaultHeaders(array $defaultHeaders)
353
    {
354
        $this->defaultHeaders = $defaultHeaders;
355
    }
356
357
    private static function isExpiredToken(ResponseInterface $response) : bool
358
    {
359
        if ($response->getStatusCode() !== 401) {
360
            return false;
361
        }
362
363
        $parsedJson = json_decode((string)$response->getBody(), true);
364
        $error = Arrays::get($parsedJson, 'error');
365
366
        if (is_array($error)) {
367
            $error = Arrays::get($error, 'code');
368
        }
369
370
        //This detects expired access tokens on Apigee
371
        if ($error !== null) {
372
            return $error === 'invalid_grant' || $error === 'invalid_token';
373
        }
374
375
        $fault = Arrays::get($parsedJson, 'fault');
376
        if ($fault === null) {
377
            return false;
378
        }
379
380
        $error = strtolower(Arrays::get($fault, 'faultstring', ''));
381
382
        return $error === 'invalid access token' || $error === 'access token expired';
383
    }
384
385
    /**
386
     * Obtains a new access token from the API
387
     *
388
     * @return void
389
     */
390
    private function refreshAccessToken()
391
    {
392
        $request = $this->authentication->getTokenRequest($this->baseUrl, $this->refreshToken);
393
        $response = $this->adapter->end($this->adapter->start($request));
394
395
        list($this->accessToken, $this->refreshToken, $expires) = Authentication::parseTokenResponse($response);
396
397 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...
398
            $this->cache->set($this->getCacheKey($request), $response, $expires);
0 ignored issues
show
Compatibility introduced by
$request of type object<Psr\Http\Message\RequestInterface> is not a sub-type of object<GuzzleHttp\Psr7\Request>. It seems like you assume a concrete implementation of the interface Psr\Http\Message\RequestInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
399
            return;
400
        }
401
    }
402
403
    /**
404
     * Helper method to set this clients access token from cache
405
     *
406
     * @return void
407
     */
408
    private function setTokenFromCache()
409
    {
410
        if (($this->cacheMode & self::CACHE_MODE_TOKEN) === 0) {
411
            return;
412
        }
413
414
        $cachedResponse = $this->cache->get(
415
            $this->getCacheKey($this->authentication->getTokenRequest($this->baseUrl, $this->refreshToken))
0 ignored issues
show
Compatibility introduced by
$this->authentication->g...l, $this->refreshToken) of type object<Psr\Http\Message\RequestInterface> is not a sub-type of object<GuzzleHttp\Psr7\Request>. It seems like you assume a concrete implementation of the interface Psr\Http\Message\RequestInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

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