Client::put()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 3
dl 0
loc 3
rs 10
1
<?php
2
3
namespace TraderInteractive\Api;
4
5
use SubjectivePHP\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\Http\Message\RequestInterface;
12
use Psr\SimpleCache\CacheInterface;
13
14
/**
15
 * Client for apis
16
 */
17
final class Client implements ClientInterface
18
{
19
    /**
20
     * Flag to cache no requests
21
     *
22
     * @const int
23
     */
24
    const CACHE_MODE_NONE = 0;
25
26
    /**
27
     * Flag to cache only GET requests
28
     *
29
     * @const int
30
     */
31
    const CACHE_MODE_GET = 1;
32
33
    /**
34
     * Flag to cache only TOKEN requests
35
     *
36
     * @const int
37
     */
38
    const CACHE_MODE_TOKEN = 2;
39
40
    /**
41
     * Flag to cache ALL requests
42
     *
43
     * @const int
44
     */
45
    const CACHE_MODE_ALL = 3;
46
47
    /**
48
     * Flag to refresh cache on ALL requests
49
     *
50
     * @const int
51
     */
52
    const CACHE_MODE_REFRESH = 4;
53
54
    /**
55
     * @var array
56
     */
57
    const CACHE_MODES = [
58
        self::CACHE_MODE_NONE,
59
        self::CACHE_MODE_GET,
60
        self::CACHE_MODE_TOKEN,
61
        self::CACHE_MODE_ALL,
62
        self::CACHE_MODE_REFRESH,
63
    ];
64
65
    /**
66
     * @var string
67
     */
68
    private $baseUrl;
69
70
    /**
71
     * @var AdapterInterface
72
     */
73
    private $adapter;
74
75
    /**
76
     * @var Authentication
77
     */
78
    private $authentication;
79
80
    /**
81
     * @var string
82
     */
83
    private $accessToken;
84
85
    /**
86
     * @var string
87
     */
88
    private $refreshToken;
89
90
    /**
91
     * @var CacheInterface
92
     */
93
    private $cache;
94
95
    /**
96
     * @var int
97
     */
98
    private $cacheMode;
99
100
    /**
101
     * Handles set in start()
102
     *
103
     * @var array like [opaqueKey => [cached response (Response), adapter handle (opaque), Request]]
104
     */
105
    private $handles = [];
106
107
    /**
108
     * Array of headers that are passed on every request unless they are overridden
109
     *
110
     * @var array
111
     */
112
    private $defaultHeaders = [];
113
114
    /**
115
     * Create a new instance of Client
116
     *
117
     * @param AdapterInterface $adapter        HTTP Adapter for sending request to the api
118
     * @param Authentication   $authentication Oauth authentication implementation
119
     * @param string           $baseUrl        Base url of the API server
120
     * @param int              $cacheMode      Strategy for caching
121
     * @param CacheInterface   $cache          Storage for cached API responses
122
     * @param string           $accessToken    API access token
123
     * @param string           $refreshToken   API refresh token
124
     *
125
     * @throws \InvalidArgumentException Thrown if $baseUrl is not a non-empty string
126
     * @throws \InvalidArgumentException Thrown if $cacheMode is not one of the cache mode constants
127
     */
128
    public function __construct(
129
        AdapterInterface $adapter,
130
        Authentication $authentication,
131
        string $baseUrl,
132
        int $cacheMode = self::CACHE_MODE_NONE,
133
        CacheInterface $cache = null,
134
        string $accessToken = null,
135
        string $refreshToken = null
136
    ) {
137
        Util::ensure(
138
            true,
139
            in_array($cacheMode, self::CACHE_MODES, true),
140
            '\InvalidArgumentException',
141
            ['$cacheMode must be a valid cache mode constant']
142
        );
143
144
        $this->adapter = $adapter;
145
        $this->baseUrl = $baseUrl;
146
        $this->authentication = $authentication;
147
        $this->cache = $cache ?? new NullCache();
148
        $this->cacheMode = $cacheMode;
149
        $this->accessToken = $accessToken;
150
        $this->refreshToken = $refreshToken;
151
    }
152
153
    /**
154
     * Get access token and refresh token
155
     *
156
     * @return array two string values, access token and refresh token
157
     */
158
    public function getTokens() : array
159
    {
160
        return [$this->accessToken, $this->refreshToken];
161
    }
162
163
    /**
164
     * Search the API resource using the specified $filters
165
     *
166
     * @param string $resource
167
     * @param array $filters
168
     *
169
     * @return string opaque handle to be given to endIndex()
170
     */
171
    public function startIndex(string $resource, array $filters = []) : string
172
    {
173
        $url = "{$this->baseUrl}/" . urlencode($resource) . '?' . Http::buildQueryString($filters);
174
        return $this->start($url, 'GET');
175
    }
176
177
    /**
178
     * @see startIndex()
179
     */
180
    public function index(string $resource, array $filters = []) : Response
181
    {
182
        return $this->end($this->startIndex($resource, $filters));
183
    }
184
185
    /**
186
     * Get the details of an API resource based on $id
187
     *
188
     * @param string $resource
189
     * @param string $id
190
     * @param array  $parameters
191
     *
192
     * @return string opaque handle to be given to endGet()
193
     */
194
    public function startGet(string $resource, string $id, array $parameters = []) : string
195
    {
196
        $url = "{$this->baseUrl}/" . urlencode($resource) . '/' . urlencode($id);
197
        if (!empty($parameters)) {
198
            $url .= '?' . Http::buildQueryString($parameters);
199
        }
200
201
        return $this->start($url, 'GET');
202
    }
203
204
    /**
205
     * @see startGet()
206
     */
207
    public function get(string $resource, string $id, array $parameters = []) : Response
208
    {
209
        return $this->end($this->startGet($resource, $id, $parameters));
210
    }
211
212
    /**
213
     * Create a new instance of an API resource using the provided $data
214
     *
215
     * @param string $resource
216
     * @param array $data
217
     *
218
     * @return string opaque handle to be given to endPost()
219
     */
220
    public function startPost(string $resource, array $data) : string
221
    {
222
        $url = "{$this->baseUrl}/" . urlencode($resource);
223
        return $this->start($url, 'POST', json_encode($data), ['Content-Type' => 'application/json']);
224
    }
225
226
    /**
227
     * @see startPost()
228
     */
229
    public function post(string $resource, array $data) : Response
230
    {
231
        return $this->end($this->startPost($resource, $data));
232
    }
233
234
    /**
235
     * Update an existing instance of an API resource specified by $id with the provided $data
236
     *
237
     * @param string $resource
238
     * @param string $id
239
     * @param array $data
240
     *
241
     * @return string opaque handle to be given to endPut()
242
     */
243
    public function startPut(string $resource, string $id, array $data) : string
244
    {
245
        $url = "{$this->baseUrl}/" . urlencode($resource) . '/' . urlencode($id);
246
        return $this->start($url, 'PUT', json_encode($data), ['Content-Type' => 'application/json']);
247
    }
248
249
    /**
250
     * @see startPut()
251
     */
252
    public function put(string $resource, string $id, array $data) : Response
253
    {
254
        return $this->end($this->startPut($resource, $id, $data));
255
    }
256
257
    /**
258
     * Delete an existing instance of an API resource specified by $id
259
     *
260
     * @param string $resource
261
     * @param string $id
262
     * @param array $data
263
     *
264
     * @return string opaque handle to be given to endDelete()
265
     */
266
    public function startDelete(string $resource, string $id = null, array $data = null) : string
267
    {
268
        $url = "{$this->baseUrl}/" . urlencode($resource);
269
        if ($id !== null) {
270
            $url .= '/' . urlencode($id);
271
        }
272
273
        $json = $data !== null ? json_encode($data) : null;
274
        $headers = [];
275
        if ($json !== null) {
276
            $headers['Content-Type'] = 'application/json';
277
        }
278
279
        return $this->start($url, 'DELETE', $json, $headers);
280
    }
281
282
    /**
283
     * @see startDelete()
284
     */
285
    public function delete(string $resource, string $id = null, array $data = null) : Response
286
    {
287
        return $this->end($this->startDelete($resource, $id, $data));
288
    }
289
290
    /**
291
     * Performs a request to the given URI and returns the response.
292
     *
293
     * @param string $method The HTTP method of the request to send.
294
     * @param string $uri    A relative api URI to which the POST request will be made.
295
     * @param array  $data   Array of data to be sent as the POST body.
296
     *
297
     * @return Response
298
     */
299
    public function send(string $method, string $uri, array $data = null) : Response
300
    {
301
        return $this->end($this->startSend($method, $uri, $data));
302
    }
303
304
    /**
305
     * Starts a request to the given URI.
306
     *
307
     * @param string $method The HTTP method of the request to send.
308
     * @param string $uri    A relative api URI to which the POST request will be made.
309
     * @param array  $data   Array of data to be sent as the POST body.
310
     *
311
     * @return string opaque handle to be given to endDelete()
312
     */
313
    public function startSend(string $method, string $uri, array $data = null) : string
314
    {
315
        $url = "{$this->baseUrl}/{$uri}";
316
        $json = $data !== null ? json_encode($data) : null;
317
        $headers = [];
318
        if ($json !== null) {
319
            $headers['Content-Type'] = 'application/json';
320
        }
321
322
        return $this->start($url, $method, $json, $headers);
323
    }
324
325
    /**
326
     * Get response of start*() method
327
     *
328
     * @param string $handle opaque handle from start*()
329
     *
330
     * @return Response
331
     */
332
    public function end(string $handle) : Response
333
    {
334
        Util::ensure(
335
            true,
336
            array_key_exists($handle, $this->handles),
337
            '\InvalidArgumentException',
338
            ['$handle not found']
339
        );
340
341
        list($cachedResponse, $adapterHandle, $request) = $this->handles[$handle];
342
        unset($this->handles[$handle]);
343
344
        if ($cachedResponse !== null) {
345
            return Response::fromPsr7Response($cachedResponse);
346
        }
347
348
        $response = $this->adapter->end($adapterHandle);
349
350
        if (self::isExpiredToken($response)) {
351
            $this->refreshAccessToken();
352
            $headers = $request->getHeaders();
353
            $headers['Authorization'] = "Bearer {$this->accessToken}";
354
            $request = new Request(
355
                $request->getMethod(),
356
                $request->getUri(),
357
                $headers,
358
                $request->getBody()
359
            );
360
            $response = $this->adapter->end($this->adapter->start($request));
361
        }
362
363
        if (($this->cacheMode === self::CACHE_MODE_REFRESH
364
                || $this->cacheMode & self::CACHE_MODE_GET)
365
                && $request->getMethod() === 'GET') {
366
            $this->cache->set($this->getCacheKey($request), $response);
367
        }
368
369
        return Response::fromPsr7Response($response);
370
    }
371
372
    /**
373
     * Set the default headers
374
     *
375
     * @param array The default headers
0 ignored issues
show
Bug introduced by
The type TraderInteractive\Api\The was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
376
     *
377
     * @return void
378
     */
379
    public function setDefaultHeaders(array $defaultHeaders)
380
    {
381
        $this->defaultHeaders = $defaultHeaders;
382
    }
383
384
    private static function isExpiredToken(ResponseInterface $response) : bool
385
    {
386
        if ($response->getStatusCode() !== 401) {
387
            return false;
388
        }
389
390
        $parsedJson = json_decode((string)$response->getBody(), true);
391
        $error = Arrays::get($parsedJson, 'error');
392
393
        if (is_array($error)) {
394
            $error = Arrays::get($error, 'code');
395
        }
396
397
        //This detects expired access tokens on Apigee
398
        if ($error !== null) {
399
            return $error === 'invalid_grant' || $error === 'invalid_token';
400
        }
401
402
        $fault = Arrays::get($parsedJson, 'fault');
403
        if ($fault === null) {
404
            return false;
405
        }
406
407
        $error = strtolower(Arrays::get($fault, 'faultstring', ''));
408
409
        return $error === 'invalid access token' || $error === 'access token expired';
410
    }
411
412
    /**
413
     * Obtains a new access token from the API
414
     *
415
     * @return void
416
     */
417
    private function refreshAccessToken()
418
    {
419
        $request = $this->authentication->getTokenRequest($this->baseUrl, $this->refreshToken);
420
        $response = $this->adapter->end($this->adapter->start($request));
421
422
        list($this->accessToken, $this->refreshToken, $expires) = Authentication::parseTokenResponse($response);
423
424
        if ($this->cache === self::CACHE_MODE_REFRESH || $this->cacheMode & self::CACHE_MODE_TOKEN) {
425
            $this->cache->set($this->getCacheKey($request), $response, $expires);
426
            return;
427
        }
428
    }
429
430
    /**
431
     * Helper method to set this clients access token from cache
432
     *
433
     * @return void
434
     */
435
    private function setTokenFromCache()
436
    {
437
        if (($this->cacheMode & self::CACHE_MODE_TOKEN) === 0) {
438
            return;
439
        }
440
441
        $cachedResponse = $this->cache->get(
442
            $this->getCacheKey($this->authentication->getTokenRequest($this->baseUrl, $this->refreshToken))
443
        );
444
        if ($cachedResponse === null) {
445
            return;
446
        }
447
448
        list($this->accessToken, $this->refreshToken, ) = Authentication::parseTokenResponse($cachedResponse);
449
    }
450
451
    /**
452
     * Calls adapter->start() using caches
453
     *
454
     * @param string $url
455
     * @param string $method
456
     * @param string|null $body
457
     * @param array $headers Authorization key will be overwritten with the bearer token, and Accept-Encoding wil be
458
     *                       overwritten with gzip.
459
     *
460
     * @return string opaque handle to be given to end()
461
     */
462
    private function start(string $url, string $method, string $body = null, array $headers = [])
463
    {
464
        $headers += $this->defaultHeaders;
465
        $headers['Accept-Encoding'] = 'gzip';
466
        if ($this->accessToken === null) {
467
            $this->setTokenFromCache();
468
        }
469
470
        if ($this->accessToken === null) {
471
            $this->refreshAccessToken();
472
        }
473
474
        $headers['Authorization'] = "Bearer {$this->accessToken}";
475
476
        $request = new Request($method, $url, $headers, $body);
477
478
        if ($request->getMethod() === 'GET' && $this->cacheMode & self::CACHE_MODE_GET) {
479
            $cached = $this->cache->get($this->getCacheKey($request));
480
            if ($cached !== null) {
481
                //The response is cache. Generate a key for the handles array
482
                $key = uniqid();
483
                $this->handles[$key] = [$cached, null, $request];
484
                return $key;
485
            }
486
        }
487
488
        $key = $this->adapter->start($request);
489
        $this->handles[$key] = [null, $key, $request];
490
        return $key;
491
    }
492
493
    private function getCacheKey(RequestInterface $request) : string
494
    {
495
        return CacheHelper::getCacheKey($request);
496
    }
497
}
498