Client::startGet()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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