Completed
Push — master ( fb6c0e...bfbc7e )
by Dante
17s queued 17s
created

BEditaClient   F

Complexity

Total Complexity 66

Size/Duplication

Total Lines 707
Duplicated Lines 0 %

Importance

Changes 19
Bugs 6 Features 1
Metric Value
eloc 164
c 19
b 6
f 1
dl 0
loc 707
rs 3.12
wmc 66

35 Methods

Rating   Name   Duplication   Size   Complexity  
A getDefaultHeaders() 0 3 1
A getStatusCode() 0 3 2
A getTokens() 0 3 1
A getObjects() 0 3 1
A getStatusMessage() 0 3 2
A getApiBaseUrl() 0 3 1
A getObject() 0 3 1
A getResponse() 0 3 1
A __construct() 0 11 1
A authenticate() 0 5 1
A removeRelated() 0 5 1
A get() 0 5 1
A getRelated() 0 3 1
A setupTokens() 0 7 2
A addRelated() 0 5 1
A getResponseBody() 0 12 3
A mapItemsAndMeta() 0 19 3
A deleteObject() 0 3 1
A replaceRelated() 0 15 2
A sendRequest() 0 25 4
A upload() 0 14 4
A saveObject() 0 3 1
A sendRequestRetry() 0 17 5
A thumbs() 0 11 4
A save() 0 20 3
A post() 0 5 1
A patch() 0 5 1
A refreshTokens() 0 17 3
A restoreObject() 0 10 1
A createMediaFromStream() 0 15 3
A delete() 0 5 1
A remove() 0 3 1
A schema() 0 5 1
A relationData() 0 7 1
A requestUri() 0 18 5

How to fix   Complexity   

Complex Class

Complex classes like BEditaClient 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.

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 BEditaClient, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * BEdita, API-first content management framework
4
 * Copyright 2018 ChannelWeb Srl, Chialab Srl
5
 *
6
 * Licensed under The MIT License
7
 * For full copyright and license information, please see the LICENSE.txt
8
 * Redistributions of files must retain the above copyright notice.
9
 */
10
11
namespace BEdita\SDK;
12
13
use GuzzleHttp\Psr7\Request;
14
use GuzzleHttp\Psr7\Uri;
15
use Http\Adapter\Guzzle6\Client;
16
use Psr\Http\Message\ResponseInterface;
17
use WoohooLabs\Yang\JsonApi\Client\JsonApiClient;
18
19
/**
20
 * BEdita4 API Client class
21
 */
22
class BEditaClient
23
{
24
    use LogTrait;
25
26
    /**
27
     * Last response.
28
     *
29
     * @var \Psr\Http\Message\ResponseInterface
30
     */
31
    private $response = null;
32
33
    /**
34
     * BEdita4 API base URL
35
     *
36
     * @var string
37
     */
38
    private $apiBaseUrl = null;
39
40
    /**
41
     * BEdita4 API KEY
42
     *
43
     * @var string
44
     */
45
    private $apiKey = null;
46
47
    /**
48
     * Default headers in request
49
     *
50
     * @var array
51
     */
52
    private $defaultHeaders = [
53
        'Accept' => 'application/vnd.api+json',
54
    ];
55
56
    /**
57
     * Default headers in request
58
     *
59
     * @var array
60
     */
61
    private $defaultContentTypeHeader = [
62
        'Content-Type' => 'application/json',
63
    ];
64
65
    /**
66
     * JWT Auth tokens
67
     *
68
     * @var array
69
     */
70
    private $tokens = [];
71
72
    /**
73
     * JSON API BEdita4 client
74
     *
75
     * @var \WoohooLabs\Yang\JsonApi\Client\JsonApiClient
76
     */
77
    private $jsonApiClient = null;
78
79
    /**
80
     * Setup main client options:
81
     *  - API base URL
82
     *  - API KEY
83
     *  - Auth tokens 'jwt' and 'renew' (optional)
84
     *
85
     * @param string $apiUrl API base URL
86
     * @param string $apiKey API key
87
     * @param array $tokens JWT Autorization tokens as associative array ['jwt' => '###', 'renew' => '###']
88
     * @return void
89
     */
90
    public function __construct(string $apiUrl, ?string $apiKey = null, array $tokens = [])
91
    {
92
        $this->apiBaseUrl = $apiUrl;
93
        $this->apiKey = $apiKey;
94
95
        $this->defaultHeaders['X-Api-Key'] = $this->apiKey;
96
        $this->setupTokens($tokens);
97
98
        // setup an asynchronous JSON API client
99
        $guzzleClient = Client::createWithConfig([]);
100
        $this->jsonApiClient = new JsonApiClient($guzzleClient);
101
    }
102
103
    /**
104
     * Setup JWT access and refresh tokens.
105
     *
106
     * @param array $tokens JWT tokens as associative array ['jwt' => '###', 'renew' => '###']
107
     * @return void
108
     */
109
    public function setupTokens(array $tokens): void
110
    {
111
        $this->tokens = $tokens;
112
        if (!empty($tokens['jwt'])) {
113
            $this->defaultHeaders['Authorization'] = sprintf('Bearer %s', $tokens['jwt']);
114
        } else {
115
            unset($this->defaultHeaders['Authorization']);
116
        }
117
    }
118
119
    /**
120
     * Get default headers in use on every request
121
     *
122
     * @return array Default headers
123
     * @codeCoverageIgnore
124
     */
125
    public function getDefaultHeaders(): array
126
    {
127
        return $this->defaultHeaders;
128
    }
129
130
    /**
131
     * Get API base URL used tokens
132
     *
133
     * @return string API base URL
134
     * @codeCoverageIgnore
135
     */
136
    public function getApiBaseUrl(): string
137
    {
138
        return $this->apiBaseUrl;
139
    }
140
141
    /**
142
     * Get current used tokens
143
     *
144
     * @return array Current tokens
145
     * @codeCoverageIgnore
146
     */
147
    public function getTokens(): array
148
    {
149
        return $this->tokens;
150
    }
151
152
    /**
153
     * Get last HTTP response
154
     *
155
     * @return ResponseInterface|null Response PSR interface
156
     * @codeCoverageIgnore
157
     */
158
    public function getResponse(): ?ResponseInterface
159
    {
160
        return $this->response;
161
    }
162
163
    /**
164
     * Get HTTP response status code
165
     * Return null if no response is available
166
     *
167
     * @return int|null Status code.
168
     */
169
    public function getStatusCode(): ?int
170
    {
171
        return $this->response ? $this->response->getStatusCode() : null;
172
    }
173
174
    /**
175
     * Get HTTP response status message
176
     * Return null if no response is available
177
     *
178
     * @return string|null Message related to status code.
179
     */
180
    public function getStatusMessage(): ?string
181
    {
182
        return $this->response ? $this->response->getReasonPhrase() : null;
183
    }
184
185
    /**
186
     * Get response body serialized into a PHP array
187
     *
188
     * @return array|null Response body as PHP array.
189
     */
190
    public function getResponseBody(): ?array
191
    {
192
        $response = $this->getResponse();
193
        if (empty($response)) {
194
            return null;
195
        }
196
        $responseBody = json_decode((string)$response->getBody(), true);
197
        if (!is_array($responseBody)) {
198
            return null;
199
        }
200
201
        return $responseBody;
202
    }
203
204
    /**
205
     * Classic authentication via POST /auth using username and password
206
     *
207
     * @param string $username username
208
     * @param string $password password
209
     * @return array|null Response in array format
210
     */
211
    public function authenticate(string $username, string $password): ?array
212
    {
213
        $body = json_encode(compact('username', 'password'));
214
215
        return $this->post('/auth', $body, ['Content-Type' => 'application/json']);
216
    }
217
218
    /**
219
     * Send a GET request a list of resources or objects or a single resource or object
220
     *
221
     * @param string $path Endpoint URL path to invoke
222
     * @param array|null $query Optional query string
223
     * @param array|null $headers Headers
224
     * @return array|null Response in array format
225
     */
226
    public function get(string $path, ?array $query = null, ?array $headers = null): ?array
227
    {
228
        $this->sendRequestRetry('GET', $path, $query, $headers);
229
230
        return $this->getResponseBody();
231
    }
232
233
    /**
234
     * GET a list of resources or objects of a given type
235
     *
236
     * @param string $type Object type name
237
     * @param array|null $query Optional query string
238
     * @param array|null $headers Custom request headers
239
     * @return array|null Response in array format
240
     */
241
    public function getObjects(string $type = 'objects', ?array $query = null, ?array $headers = null): ?array
242
    {
243
        return $this->get(sprintf('/%s', $type), $query, $headers);
244
    }
245
246
    /**
247
     * GET a single object of a given type
248
     *
249
     * @param int|string $id Object id
250
     * @param string $type Object type name
251
     * @param array|null $query Optional query string
252
     * @param array|null $headers Custom request headers
253
     * @return array|null Response in array format
254
     */
255
    public function getObject($id, string $type = 'objects', ?array $query = null, ?array $headers = null): ?array
256
    {
257
        return $this->get(sprintf('/%s/%s', $type, $id), $query, $headers);
258
    }
259
260
    /**
261
     * Get a list of related resources or objects
262
     *
263
     * @param int|string $id Resource id or object uname/id
264
     * @param string $type Type name
265
     * @param string $relation Relation name
266
     * @param array|null $query Optional query string
267
     * @param array|null $headers Custom request headers
268
     * @return array|null Response in array format
269
     */
270
    public function getRelated($id, string $type, string $relation, ?array $query = null, ?array $headers = null): ?array
271
    {
272
        return $this->get(sprintf('/%s/%s/%s', $type, $id, $relation), $query, $headers);
273
    }
274
275
    /**
276
     * Add a list of related resources or objects
277
     *
278
     * @param int|string $id Resource id or object uname/id
279
     * @param string $type Type name
280
     * @param string $relation Relation name
281
     * @param array $data Related resources or objects to add, MUST contain id and type
282
     * @param array|null $headers Custom request headers
283
     * @return array|null Response in array format
284
     */
285
    public function addRelated($id, string $type, string $relation, array $data, ?array $headers = null): ?array
286
    {
287
        $body = compact('data');
288
289
        return $this->post(sprintf('/%s/%s/relationships/%s', $type, $id, $relation), json_encode($body), $headers);
290
    }
291
292
    /**
293
     * Remove a list of related resources or objects
294
     *
295
     * @param int|string $id Resource id or object uname/id
296
     * @param string $type Type name
297
     * @param string $relation Relation name
298
     * @param array $data Related resources or objects to remove from relation
299
     * @param array|null $headers Custom request headers
300
     * @return array|null Response in array format
301
     */
302
    public function removeRelated($id, string $type, string $relation, array $data, ?array $headers = null): ?array
303
    {
304
        $body = compact('data');
305
306
        return $this->delete(sprintf('/%s/%s/relationships/%s', $type, $id, $relation), json_encode($body), $headers);
307
    }
308
309
    /**
310
     * Create an array of objects with just id and type and filter objects with meta data.
311
     *
312
     * @param array $data Related resources or objects to insert
313
     * @return array Mapped and filtered items
314
     */
315
    private function mapItemsAndMeta(array $data): array
316
    {
317
        $items = $data;
318
        $withMeta = null;
319
        if (!empty($data[0])) {
320
            $items = array_map(function ($item) {
321
                return [
322
                    'id' => $item['id'],
323
                    'type' => $item['type'],
324
                ];
325
            }, $data);
326
            $withMeta = array_filter($data, function ($item) {
327
                return !empty($item['meta']);
328
            });
329
        } elseif (!empty($data['meta'])) {
330
            $withMeta = $data;
331
        }
332
333
        return compact('items', 'withMeta');
334
    }
335
336
    /**
337
     * Replace a list of related resources or objects: previuosly related are removed and replaced with these.
338
     *
339
     * @param int|string $id Object id
340
     * @param string $type Object type name
341
     * @param string $relation Relation name
342
     * @param array $data Related resources or objects to insert
343
     * @param array|null $headers Custom request headers
344
     * @return array|null Response in array format
345
     */
346
    public function replaceRelated($id, string $type, string $relation, array $data, ?array $headers = null): ?array
347
    {
348
        $map = $this->mapItemsAndMeta($data);
349
        $items = $map['items'];
350
        $withMeta = $map['withMeta'];
351
352
        $result = $this->patch(sprintf('/%s/%s/relationships/%s', $type, $id, $relation), json_encode(['data' => $items]), $headers);
353
354
        if (!empty($withMeta)) {
355
            $response = $this->response;
356
            $this->post(sprintf('/%s/%s/relationships/%s', $type, $id, $relation), json_encode(['data' => $withMeta]), $headers);
357
            $this->response = $response;
358
        }
359
360
        return $result;
361
    }
362
363
    /**
364
     * Create a new object or resource (POST) or modify an existing one (PATCH)
365
     *
366
     * @param string $type Object or resource type name
367
     * @param array $data Object or resource data to save
368
     * @param array|null $headers Custom request headers
369
     * @return array|null Response in array format
370
     */
371
    public function save(string $type, array $data, ?array $headers = null): ?array
372
    {
373
        $id = null;
374
        if (array_key_exists('id', $data)) {
375
            $id = $data['id'];
376
            unset($data['id']);
377
        }
378
379
        $body = [
380
            'data' => [
381
                'type' => $type,
382
                'attributes' => $data,
383
            ],
384
        ];
385
        if (!$id) {
386
            return $this->post(sprintf('/%s', $type), json_encode($body), $headers);
387
        }
388
        $body['data']['id'] = $id;
389
390
        return $this->patch(sprintf('/%s/%s', $type, $id), json_encode($body), $headers);
391
    }
392
393
    /**
394
     * [DEPRECATED] Create a new object (POST) or modify an existing one (PATCH)
395
     *
396
     * @param string $type Object type name
397
     * @param array $data Object data to save
398
     * @param array|null $headers Custom request headers
399
     * @return array|null Response in array format
400
     * @deprecated Use `save()` method instead
401
     * @codeCoverageIgnore
402
     */
403
    public function saveObject(string $type, array $data, ?array $headers = null): ?array
404
    {
405
        return $this->save($type, $data, $headers);
406
    }
407
408
    /**
409
     * Delete an object (DELETE) => move to trashcan.
410
     *
411
     * @param int|string $id Object id
412
     * @param string $type Object type name
413
     * @return array|null Response in array format
414
     */
415
    public function deleteObject($id, string $type): ?array
416
    {
417
        return $this->delete(sprintf('/%s/%s', $type, $id));
418
    }
419
420
    /**
421
     * Remove an object => permanently remove object from trashcan.
422
     *
423
     * @param int|string $id Object id
424
     * @return array|null Response in array format
425
     */
426
    public function remove($id): ?array
427
    {
428
        return $this->delete(sprintf('/trash/%s', $id));
429
    }
430
431
    /**
432
     * Upload file (POST)
433
     *
434
     * @param string $filename The file name
435
     * @param string $filepath File full path: could be on a local filesystem or a remote reachable URL
436
     * @param array|null $headers Custom request headers
437
     * @return array|null Response in array format
438
     * @throws BEditaClientException
439
     */
440
    public function upload(string $filename, string $filepath, ?array $headers = null): ?array
441
    {
442
        if (!file_exists($filepath)) {
443
            throw new BEditaClientException('File not found', 500);
444
        }
445
        $file = file_get_contents($filepath);
446
        if (!$file) {
447
            throw new BEditaClientException('File get contents failed', 500);
448
        }
449
        if (empty($headers['Content-Type'])) {
450
            $headers['Content-Type'] = mime_content_type($filepath);
451
        }
452
453
        return $this->post(sprintf('/streams/upload/%s', $filename), $file, $headers);
454
    }
455
456
    /**
457
     * Create media by type and body data and link it to a stream:
458
     *  - `POST /:type` with `$body` as payload, create media object
459
     *  - `PATCH /streams/:stream_id/relationships/object` modify stream adding relation to media
460
     *  - `GET /:type/:id` get media data
461
     *
462
     * @param string $streamId The stream identifier
463
     * @param string $type The type
464
     * @param array $body The body data
465
     * @return array|null Response in array format
466
     * @throws BEditaClientException
467
     */
468
    public function createMediaFromStream($streamId, string $type, array $body): ?array
469
    {
470
        $response = $this->post(sprintf('/%s', $type), json_encode($body));
471
        if (empty($response)) {
472
            throw new BEditaClientException('Invalid response from POST ' . sprintf('/%s', $type));
473
        }
474
        $id = $response['data']['id'];
475
        $data = compact('id', 'type');
476
        $body = compact('data');
477
        $response = $this->patch(sprintf('/streams/%s/relationships/object', $streamId), json_encode($body));
478
        if (empty($response)) {
479
            throw new BEditaClientException('Invalid response from PATCH ' . sprintf('/streams/%s/relationships/object', $id));
480
        }
481
482
        return $this->getObject($data['id'], $data['type']);
483
    }
484
485
    /**
486
     * Thumbnail request using `GET /media/thumbs` endpoint
487
     *
488
     *  Usage:
489
     *          thumbs(123) => `GET /media/thumbs/123`
490
     *          thumbs(123, ['preset' => 'glide']) => `GET /media/thumbs/123&preset=glide`
491
     *          thumbs(null, ['ids' => '123,124,125']) => `GET /media/thumbs?ids=123,124,125`
492
     *          thumbs(null, ['ids' => '123,124,125', 'preset' => 'async']) => `GET /media/thumbs?ids=123,124,125&preset=async`
493
     *          thumbs(123, ['options' => ['w' => 100, 'h' => 80, 'fm' => 'jpg']]) => `GET /media/thumbs/123/options[w]=100&options[h]=80&options[fm]=jpg` (these options could be not available... just set in preset(s))
494
     *
495
     * @param int|null $id the media Id.
496
     * @param array $query The query params for thumbs call.
497
     * @return array|null Response in array format
498
     */
499
    public function thumbs($id = null, $query = []): ?array
500
    {
501
        if (empty($id) && empty($query['ids'])) {
502
            throw new BEditaClientException('Invalid empty id|ids for thumbs');
503
        }
504
        $endpoint = '/media/thumbs';
505
        if (!empty($id)) {
506
            $endpoint .= sprintf('/%d', $id);
507
        }
508
509
        return $this->get($endpoint, $query);
510
    }
511
512
    /**
513
     * Get JSON SCHEMA of a resource or object
514
     *
515
     * @param string $type Object or resource type name
516
     * @return array|null JSON SCHEMA in array format
517
     */
518
    public function schema(string $type): ?array
519
    {
520
        $h = ['Accept' => 'application/schema+json'];
521
522
        return $this->get(sprintf('/model/schema/%s', $type), null, $h);
523
    }
524
525
    /**
526
     * Get info of a relation (data, params) and get left/right object types
527
     *
528
     * @param string $name relation name
529
     * @return array|null relation data in array format
530
     */
531
    public function relationData(string $name): ?array
532
    {
533
        $query = [
534
            'include' => 'left_object_types,right_object_types',
535
        ];
536
537
        return $this->get(sprintf('/model/relations/%s', $name), $query);
538
    }
539
540
    /**
541
     * Restore object from trash
542
     *
543
     * @param int|string $id Object id
544
     * @param string $type Object type name
545
     * @return array|null Response in array format
546
     */
547
    public function restoreObject($id, string $type): ?array
548
    {
549
        $body = [
550
            'data' => [
551
                'id' => $id,
552
                'type' => $type,
553
            ],
554
        ];
555
556
        return $this->patch(sprintf('/%s/%s', 'trash', $id), json_encode($body));
557
    }
558
559
    /**
560
     * Send a PATCH request to modify a single resource or object
561
     *
562
     * @param string $path Endpoint URL path to invoke
563
     * @param mixed $body Request body
564
     * @param array|null $headers Custom request headers
565
     * @return array|null Response in array format
566
     */
567
    public function patch(string $path, $body, ?array $headers = null): ?array
568
    {
569
        $this->sendRequestRetry('PATCH', $path, null, $headers, $body);
570
571
        return $this->getResponseBody();
572
    }
573
574
    /**
575
     * Send a POST request for creating resources or objects or other operations like /auth
576
     *
577
     * @param string $path Endpoint URL path to invoke
578
     * @param mixed $body Request body
579
     * @param array|null $headers Custom request headers
580
     * @return array|null Response in array format
581
     */
582
    public function post(string $path, $body, ?array $headers = null): ?array
583
    {
584
        $this->sendRequestRetry('POST', $path, null, $headers, $body);
585
586
        return $this->getResponseBody();
587
    }
588
589
    /**
590
     * Send a DELETE request
591
     *
592
     * @param string $path Endpoint URL path to invoke.
593
     * @param mixed $body Request body
594
     * @param array|null $headers Custom request headers
595
     * @return array|null Response in array format.
596
     */
597
    public function delete(string $path, $body = null, ?array $headers = null): ?array
598
    {
599
        $this->sendRequestRetry('DELETE', $path, null, $headers, $body);
600
601
        return $this->getResponseBody();
602
    }
603
604
    /**
605
     * Send a generic JSON API request with a basic retry policy on expired token exception.
606
     *
607
     * @param string $method HTTP Method.
608
     * @param string $path Endpoint URL path.
609
     * @param array|null $query Query string parameters.
610
     * @param string[]|null $headers Custom request headers.
611
     * @param string|resource|\Psr\Http\Message\StreamInterface|null $body Request body.
612
     * @return \Psr\Http\Message\ResponseInterface
613
     */
614
    protected function sendRequestRetry(string $method, string $path, ?array $query = null, ?array $headers = null, $body = null): ResponseInterface
615
    {
616
        try {
617
            return $this->sendRequest($method, $path, $query, $headers, $body);
618
        } catch (BEditaClientException $e) {
619
            // Handle error.
620
            $attributes = $e->getAttributes();
621
            if ($e->getCode() !== 401 || empty($attributes['code']) || $attributes['code'] !== 'be_token_expired') {
622
                // Not an expired token's fault.
623
                throw $e;
624
            }
625
626
            // Refresh and retry.
627
            $this->refreshTokens();
628
            unset($headers['Authorization']);
629
630
            return $this->sendRequest($method, $path, $query, $headers, $body);
631
        }
632
    }
633
634
    /**
635
     * Send a generic JSON API request and retrieve response $this->response
636
     *
637
     * @param string $method HTTP Method.
638
     * @param string $path Endpoint URL path (with or without starting `/`) or absolute API path
639
     * @param array|null $query Query string parameters.
640
     * @param string[]|null $headers Custom request headers.
641
     * @param string|resource|\Psr\Http\Message\StreamInterface|null $body Request body.
642
     * @return \Psr\Http\Message\ResponseInterface
643
     * @throws BEditaClientException Throws an exception if server response code is not 20x.
644
     */
645
    protected function sendRequest(string $method, string $path, ?array $query = null, ?array $headers = null, $body = null): ResponseInterface
646
    {
647
        $uri = $this->requestUri($path, $query);
648
        $headers = array_merge($this->defaultHeaders, (array)$headers);
649
650
        // set default `Content-Type` if not set and $body not empty
651
        if (!empty($body)) {
652
            $headers = array_merge($this->defaultContentTypeHeader, $headers);
653
        }
654
655
        // Send the request synchronously to retrieve the response.
656
        // Request and response log performed only if configured via `initLogger()`
657
        $request = new Request($method, $uri, $headers, $body);
658
        $this->logRequest($request);
659
        $this->response = $this->jsonApiClient->sendRequest($request);
660
        $this->logResponse($this->response);
661
        if ($this->getStatusCode() >= 400) {
662
            // Something bad just happened.
663
            $response = $this->getResponseBody();
664
            // Message will be 'error` array, if absent use status massage
665
            $message = empty($response['error']) ? $this->getStatusMessage() : $response['error'];
666
            throw new BEditaClientException($message, $this->getStatusCode());
667
        }
668
669
        return $this->response;
670
    }
671
672
    /**
673
     * Create request URI from path.
674
     * If path is absolute, i.e. it starts with 'http://' or 'https://', path is unchanged.
675
     * Otherwise `$this->apiBaseUrl` is prefixed, prepending a `/` if necessary.
676
     *
677
     * @param string $path Endpoint URL path (with or without starting `/`) or absolute API path
678
     * @param array|null $query Query string parameters.
679
     * @return Uri
680
     */
681
    protected function requestUri(string $path, ?array $query = null): Uri
682
    {
683
        if (strpos($path, 'https://') !== 0 && strpos($path, 'http://') !== 0) {
684
            if (substr($path, 0, 1) !== '/') {
685
                $path = '/' . $path;
686
            }
687
            $path = $this->apiBaseUrl . $path;
688
        }
689
        $uri = new Uri($path);
690
691
        // if path contains query strings, remove them from path and add them to query filter
692
        parse_str($uri->getQuery(), $uriQuery);
693
        if ($query) {
694
            $query = array_merge((array)$uriQuery, (array)$query);
695
            $uri = $uri->withQuery(http_build_query($query));
696
        }
697
698
        return $uri;
699
    }
700
701
    /**
702
     * Refresh JWT access token.
703
     *
704
     * On success `$this->tokens` data will be updated with new access and renew tokens.
705
     *
706
     * @throws \BadMethodCallException Throws an exception if client has no renew token available.
707
     * @throws \Cake\Network\Exception\ServiceUnavailableException Throws an exception if server response doesn't
708
     *      include the expected data.
709
     * @return void
710
     * @throws BEditaClientException Throws an exception if server response code is not 20x.
711
     */
712
    public function refreshTokens(): void
713
    {
714
        if (empty($this->tokens['renew'])) {
715
            throw new \BadMethodCallException('You must be logged in to renew token');
716
        }
717
718
        $headers = [
719
            'Authorization' => sprintf('Bearer %s', $this->tokens['renew']),
720
        ];
721
722
        $this->sendRequest('POST', '/auth', [], $headers);
723
        $body = $this->getResponseBody();
724
        if (empty($body['meta']['jwt'])) {
725
            throw new BEditaClientException('Invalid response from server');
726
        }
727
728
        $this->setupTokens($body['meta']);
729
    }
730
}
731