Completed
Pull Request — master (#42)
by Chad
02:53
created

Client::start()   B

Complexity

Conditions 6
Paths 12

Size

Total Lines 30
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 30
c 0
b 0
f 0
rs 8.439
cc 6
eloc 18
nc 12
nop 4
1
<?php
2
3
namespace TraderInteractive\Api;
4
5
use DominionEnterprises\Util;
6
use DominionEnterprises\Util\Arrays;
7
use DominionEnterprises\Util\Http;
8
9
/**
10
 * Client for apis
11
 */
12
final class Client implements ClientInterface
13
{
14
    /**
15
     * Flag to cache no requests
16
     *
17
     * @const int
18
     */
19
    const CACHE_MODE_NONE = 0;
20
21
    /**
22
     * Flag to cache only GET requests
23
     *
24
     * @const int
25
     */
26
    const CACHE_MODE_GET = 1;
27
28
    /**
29
     * Flag to cache only TOKEN requests
30
     *
31
     * @const int
32
     */
33
    const CACHE_MODE_TOKEN = 2;
34
35
    /**
36
     * Flag to cache ALL requests
37
     *
38
     * @const int
39
     */
40
    const CACHE_MODE_ALL = 3;
41
42
    /**
43
     * Flag to refresh cache on ALL requests
44
     *
45
     * @const int
46
     */
47
    const CACHE_MODE_REFRESH = 4;
48
49
    /**
50
     * Base url of the API server
51
     *
52
     * @var string
53
     */
54
    private $baseUrl;
55
56
    /**
57
     * HTTP Adapter for sending request to the api
58
     *
59
     * @var Adapter
60
     */
61
    private $adapter;
62
63
    /**
64
     * Oauth authentication implementation
65
     *
66
     * @var Authentication
67
     */
68
    private $authentication;
69
70
    /**
71
     * API access token
72
     *
73
     * @var string
74
     */
75
    private $accessToken;
76
77
    /**
78
     * API refresh token
79
     *
80
     * @var string
81
     */
82
    private $refreshToken;
83
84
    /**
85
     * Storage for cached API responses
86
     *
87
     * @var Cache
88
     */
89
    private $cache;
90
91
    /**
92
     * Strategy for caching
93
     *
94
     * @var int
95
     */
96
    private $cacheMode;
97
98
    /**
99
     * Handles set in start()
100
     *
101
     * @var array like [opaqueKey => [cached response (Response), adapter handle (opaque), Request]]
102
     */
103
    private $handles = [];
104
105
    /**
106
     * Array of headers that are passed on every request unless they are overridden
107
     *
108
     * @var array
109
     */
110
    private $defaultHeaders = [];
111
112
    /**
113
     * Create a new instance of Client
114
     *
115
     * @param Adapter $adapter
116
     * @param Authentication $authentication
117
     * @param string $baseUrl
118
     * @param int $cacheMode
119
     * @param Cache $cache
120
     * @param string $accessToken
121
     * @param string $refreshToken
122
     *
123
     * @throws \InvalidArgumentException Thrown if $baseUrl is not a non-empty string
124
     * @throws \InvalidArgumentException Thrown if $cacheMode is not one of the cache mode constants
125
     */
126
    public function __construct(
127
        Adapter $adapter,
128
        Authentication $authentication,
129
        $baseUrl,
130
        $cacheMode = self::CACHE_MODE_NONE,
131
        Cache $cache = null,
132
        $accessToken = null,
133
        $refreshToken = null
134
    ) {
135
        Util::throwIfNotType(['string' => [$baseUrl]], true);
136
        Util::throwIfNotType(['string' => [$accessToken, $refreshToken]], true, true);
137
        Util::ensure(
138
            true,
139
            in_array(
140
                $cacheMode,
141
                [
142
                    self::CACHE_MODE_NONE,
143
                    self::CACHE_MODE_GET,
144
                    self::CACHE_MODE_TOKEN,
145
                    self::CACHE_MODE_ALL,
146
                    self::CACHE_MODE_REFRESH,
147
                ],
148
                true
149
            ),
150
            '\InvalidArgumentException',
151
            ['$cacheMode must be a valid cache mode constant']
152
        );
153
154
        if ($cacheMode !== self::CACHE_MODE_NONE) {
155
            Util::ensureNot(
156
                null,
157
                $cache,
158
                '\InvalidArgumentException',
159
                ['$cache must not be null if $cacheMode is not CACHE_MODE_NONE']
160
            );
161
        }
162
163
        $this->adapter = $adapter;
164
        $this->baseUrl = $baseUrl;
165
        $this->authentication = $authentication;
166
        $this->cache = $cache;
167
        $this->cacheMode = $cacheMode;
168
        $this->accessToken = $accessToken;
169
        $this->refreshToken = $refreshToken;
170
    }
171
172
    /**
173
     * Get access token and refresh token
174
     *
175
     * @return array two string values, access token and refresh token
176
     */
177
    public function getTokens()
178
    {
179
        return [$this->accessToken, $this->refreshToken];
180
    }
181
182
    /**
183
     * Search the API resource using the specified $filters
184
     *
185
     * @param string $resource
186
     * @param array $filters
187
     *
188
     * @return mixed opaque handle to be given to endIndex()
189
     */
190
    public function startIndex($resource, array $filters = [])
191
    {
192
        Util::throwIfNotType(['string' => [$resource]], true);
193
        $url = "{$this->baseUrl}/" . urlencode($resource) . '?' . Http::buildQueryString($filters);
194
        return $this->start($url, 'GET');
195
    }
196
197
    /**
198
     * @see startIndex()
199
     */
200
    public function index($resource, array $filters = [])
201
    {
202
        return $this->end($this->startIndex($resource, $filters));
203
    }
204
205
    /**
206
     * Get the details of an API resource based on $id
207
     *
208
     * @param string $resource
209
     * @param string $id
210
     * @param array  $parameters
211
     *
212
     * @return mixed opaque handle to be given to endGet()
213
     */
214
    public function startGet($resource, $id, array $parameters = [])
215
    {
216
        Util::throwIfNotType(['string' => [$resource, $id]], true);
217
        $url = "{$this->baseUrl}/" . urlencode($resource) . '/' . urlencode($id);
218
        if (!empty($parameters)) {
219
            $url .= '?' . Http::buildQueryString($parameters);
220
        }
221
222
        return $this->start($url, 'GET');
223
    }
224
225
    /**
226
     * @see startGet()
227
     */
228
    public function get($resource, $id, array $parameters = [])
229
    {
230
        return $this->end($this->startGet($resource, $id, $parameters));
231
    }
232
233
    /**
234
     * Create a new instance of an API resource using the provided $data
235
     *
236
     * @param string $resource
237
     * @param array $data
238
     *
239
     * @return mixed opaque handle to be given to endPost()
240
     */
241
    public function startPost($resource, array $data)
242
    {
243
        Util::throwIfNotType(['string' => [$resource]], true);
244
        $url = "{$this->baseUrl}/" . urlencode($resource);
245
        return $this->start($url, 'POST', json_encode($data), ['Content-Type' => 'application/json']);
246
    }
247
248
    /**
249
     * @see startPost()
250
     */
251
    public function post($resource, array $data)
252
    {
253
        return $this->end($this->startPost($resource, $data));
254
    }
255
256
    /**
257
     * Update an existing instance of an API resource specified by $id with the provided $data
258
     *
259
     * @param string $resource
260
     * @param string $id
261
     * @param array $data
262
     *
263
     * @return mixed opaque handle to be given to endPut()
264
     */
265
    public function startPut($resource, $id, array $data)
266
    {
267
        Util::throwIfNotType(['string' => [$resource, $id]], true);
268
        $url = "{$this->baseUrl}/" . urlencode($resource) . '/' . urlencode($id);
269
        return $this->start($url, 'PUT', json_encode($data), ['Content-Type' => 'application/json']);
270
    }
271
272
    /**
273
     * @see startPut()
274
     */
275
    public function put($resource, $id, array $data)
276
    {
277
        return $this->end($this->startPut($resource, $id, $data));
278
    }
279
280
    /**
281
     * Delete an existing instance of an API resource specified by $id
282
     *
283
     * @param string $resource
284
     * @param string $id
285
     * @param array $data
286
     *
287
     * @return mixed opaque handle to be given to endDelete()
288
     */
289
    public function startDelete($resource, $id = null, array $data = null)
290
    {
291
        Util::throwIfNotType(['string' => [$resource]], true);
292
        $url = "{$this->baseUrl}/" . urlencode($resource);
293
        if ($id !== null) {
294
            Util::throwIfNotType(['string' => [$id]], true);
295
            $url .= '/' . urlencode($id);
296
        }
297
298
        $json = $data !== null ? json_encode($data) : null;
299
        return $this->start($url, 'DELETE', $json, ['Content-Type' => 'application/json']);
300
    }
301
302
    /**
303
     * @see startDelete()
304
     */
305
    public function delete($resource, $id = null, array $data = null)
306
    {
307
        return $this->end($this->startDelete($resource, $id, $data));
308
    }
309
310
    /**
311
     * Get response of start*() method
312
     *
313
     * @param mixed $handle opaque handle from start*()
314
     *
315
     * @return Response
316
     */
317
    public function end($handle)
318
    {
319
        Util::ensure(
320
            true,
321
            array_key_exists($handle, $this->handles),
322
            '\InvalidArgumentException',
323
            ['$handle not found']
324
        );
325
326
        list($cachedResponse, $adapterHandle, $request) = $this->handles[$handle];
327
        unset($this->handles[$handle]);
328
329
        if ($cachedResponse !== null) {
330
            return $cachedResponse;
331
        }
332
333
        $response = $this->adapter->end($adapterHandle);
334
335
        if (self::isExpiredToken($response)) {
336
            $this->refreshAccessToken();
337
            $headers = $request->getHeaders();
338
            $headers['Authorization'] = "Bearer {$this->accessToken}";
339
            $request = new Request($request->getUrl(), $request->getMethod(), $request->getBody(), $headers);
340
            $response = $this->adapter->end($this->adapter->start($request));
341
        }
342
343 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...
344
                || $this->cacheMode & self::CACHE_MODE_GET)
345
                && $request->getMethod() === 'GET') {
346
            $this->cache->set($request, $response);
347
        }
348
349
        return $response;
350
    }
351
352
    /**
353
     * Set the default headers
354
     *
355
     * @param array The default headers
356
     *
357
     * @return void
358
     */
359
    public function setDefaultHeaders($defaultHeaders)
360
    {
361
        $this->defaultHeaders = $defaultHeaders;
362
    }
363
364
    private static function isExpiredToken(Response $response)
365
    {
366
        if ($response->getHttpCode() !== 401) {
367
            return false;
368
        }
369
370
        $parsedJson = $response->getResponse();
371
        $error = Arrays::get($parsedJson, 'error');
372
373
        if (is_array($error)) {
374
            $error = Arrays::get($error, 'code');
375
        }
376
377
        //This detects expired access tokens on Apigee
378
        if ($error !== null) {
379
            return $error === 'invalid_grant' || $error === 'invalid_token';
380
        }
381
382
        $fault = Arrays::get($parsedJson, 'fault');
383
        if ($fault === null) {
384
            return false;
385
        }
386
387
        $error = strtolower(Arrays::get($fault, 'faultstring', ''));
388
389
        return $error === 'invalid access token' || $error === 'access token expired';
390
    }
391
392
    /**
393
     * Obtains a new access token from the API
394
     *
395
     * @return void
396
     */
397
    private function refreshAccessToken()
398
    {
399
        $request = $this->authentication->getTokenRequest($this->baseUrl, $this->refreshToken);
400
        $response = $this->adapter->end($this->adapter->start($request));
401
402
        list($this->accessToken, $this->refreshToken, $expires) = Authentication::parseTokenResponse($response);
403
404 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...
405
            $this->cache->set($request, $response, $expires);
406
        }
407
    }
408
409
    /**
410
     * Helper method to set this clients access token from cache
411
     *
412
     * @return void
413
     */
414
    private function setTokenFromCache()
415
    {
416
        if (($this->cacheMode & self::CACHE_MODE_TOKEN) === 0) {
417
            return;
418
        }
419
420
        $cachedResponse = $this->cache->get(
421
            $this->authentication->getTokenRequest($this->baseUrl, $this->refreshToken)
422
        );
423
        if ($cachedResponse === null) {
424
            return;
425
        }
426
427
        list($this->accessToken, $this->refreshToken, ) = Authentication::parseTokenResponse($cachedResponse);
428
    }
429
430
    /**
431
     * Calls adapter->start() using caches
432
     *
433
     * @param string $url
434
     * @param string $method
435
     * @param string|null $body
436
     * @param array $headers Authorization key will be overwritten with the bearer token, and Accept-Encoding wil be
437
     *                       overwritten with gzip.
438
     *
439
     * @return mixed opaque handle to be given to end()
440
     */
441
    private function start($url, $method, $body = null, array $headers = [])
442
    {
443
        $headers += $this->defaultHeaders;
444
        $headers['Accept-Encoding'] = 'gzip';
445
        if ($this->accessToken === null) {
446
            $this->setTokenFromCache();
447
        }
448
449
        if ($this->accessToken === null) {
450
            $this->refreshAccessToken();
451
        }
452
453
        $headers['Authorization'] = "Bearer {$this->accessToken}";
454
455
        $request = new Request($url, $method, $body, $headers);
456
457
        if ($request->getMethod() === 'GET' && $this->cacheMode & self::CACHE_MODE_GET) {
458
            $cached = $this->cache->get($request);
459
            if ($cached !== null) {
460
                //The response is cache. Generate a key for the handles array
461
                $key = uniqid();
462
                $this->handles[$key] = [$cached, null, $request];
463
                return $key;
464
            }
465
        }
466
467
        $key = $this->adapter->start($request);
468
        $this->handles[$key] = [null, $key, $request];
469
        return $key;
470
    }
471
}
472