Completed
Push — master ( af8278...d461f2 )
by Stefano
9s
created

BEditaClient::deleteObject()   A

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
nc 1
nop 2
dl 0
loc 3
rs 10
c 0
b 0
f 0
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
25
    /**
26
     * Last response.
27
     *
28
     * @var \Psr\Http\Message\ResponseInterface
29
     */
30
    private $response = null;
31
32
    /**
33
     * BEdita4 API base URL
34
     *
35
     * @var string
36
     */
37
    private $apiBaseUrl = null;
38
39
    /**
40
     * BEdita4 API KEY
41
     *
42
     * @var string
43
     */
44
    private $apiKey = null;
45
46
    /**
47
     * Default headers in request
48
     *
49
     * @var array
50
     */
51
    private $defaultHeaders = [
52
        'Accept' => 'application/vnd.api+json',
53
    ];
54
55
    /**
56
     * Default headers in request
57
     *
58
     * @var array
59
     */
60
    private $defaultContentTypeHeader = [
61
        'Content-Type' => 'application/json',
62
    ];
63
64
    /**
65
     * JWT Auth tokens
66
     *
67
     * @var array
68
     */
69
    private $tokens = [];
70
71
    /**
72
     * JSON API BEdita4 client
73
     *
74
     * @var \WoohooLabs\Yang\JsonApi\Client\JsonApiClient
75
     */
76
    private $jsonApiClient = null;
77
78
    /**
79
     * Setup main client options:
80
     *  - API base URL
81
     *  - API KEY
82
     *  - Auth tokens 'jwt' and 'renew' (optional)
83
     *
84
     * @param string $apiUrl API base URL
85
     * @param string $apiKey API key
86
     * @param array $tokens JWT Autorization tokens as associative array ['jwt' => '###', 'renew' => '###']
87
     * @return void
88
     */
89
    public function __construct(string $apiUrl, ?string $apiKey = null, array $tokens = [])
90
    {
91
        $this->apiBaseUrl = $apiUrl;
92
        $this->apiKey = $apiKey;
93
94
        $this->defaultHeaders['X-Api-Key'] = $this->apiKey;
95
        $this->setupTokens($tokens);
96
97
        // setup an asynchronous JSON API client
98
        $guzzleClient = Client::createWithConfig([]);
99
        $this->jsonApiClient = new JsonApiClient($guzzleClient);
100
    }
101
102
    /**
103
     * Setup JWT access and refresh tokens.
104
     *
105
     * @param array $tokens JWT tokens as associative array ['jwt' => '###', 'renew' => '###']
106
     * @return void
107
     */
108
    public function setupTokens(array $tokens) : void
109
    {
110
        $this->tokens = $tokens;
111
        if (!empty($tokens['jwt'])) {
112
            $this->defaultHeaders['Authorization'] = sprintf('Bearer %s', $tokens['jwt']);
113
        } else {
114
            unset($this->defaultHeaders['Authorization']);
115
        }
116
    }
117
118
    /**
119
     * Get default headers in use on every request
120
     *
121
     * @return array Default headers
122
     * @codeCoverageIgnore
123
     */
124
    public function getDefaultHeaders() : array
125
    {
126
        return $this->defaultHeaders;
127
    }
128
129
    /**
130
     * Get API base URL used tokens
131
     *
132
     * @return string API base URL
133
     * @codeCoverageIgnore
134
     */
135
    public function getApiBaseUrl() : string
136
    {
137
        return $this->apiBaseUrl;
138
    }
139
140
    /**
141
     * Get current used tokens
142
     *
143
     * @return array Current tokens
144
     * @codeCoverageIgnore
145
     */
146
    public function getTokens() : array
147
    {
148
        return $this->tokens;
149
    }
150
151
    /**
152
     * Get last HTTP response
153
     *
154
     * @return ResponseInterface Response PSR interface
155
     * @codeCoverageIgnore
156
     */
157
    public function getResponse() : ResponseInterface
158
    {
159
        return $this->response;
160
    }
161
162
    /**
163
     * Get HTTP response status code
164
     * Return null if no response is available
165
     *
166
     * @return int|null Status code.
167
     */
168
    public function getStatusCode() : ?int
169
    {
170
        return $this->response ? $this->response->getStatusCode() : null;
171
    }
172
173
    /**
174
     * Get HTTP response status message
175
     * Return null if no response is available
176
     *
177
     * @return string|null Message related to status code.
178
     */
179
    public function getStatusMessage() : ?string
180
    {
181
        return $this->response ? $this->response->getReasonPhrase() : null;
182
    }
183
184
    /**
185
     * Get response body serialized into a PHP array
186
     *
187
     * @return array|null Response body as PHP array.
188
     */
189
    public function getResponseBody() : ?array
190
    {
191
        if (empty($this->response)) {
192
            return null;
193
        }
194
        $responseBody = json_decode((string)$this->response->getBody(), true);
195
        if (!is_array($responseBody)) {
196
            return null;
197
        }
198
199
        return $responseBody;
200
    }
201
202
    /**
203
     * Classic authentication via POST /auth using username and password
204
     *
205
     * @param string $username username
206
     * @param string $password password
207
     * @return array|null Response in array format
208
     */
209
    public function authenticate(string $username, string $password) : ?array
210
    {
211
        $body = json_encode(compact('username', 'password'));
212
213
        return $this->post('/auth', $body, ['Content-Type' => 'application/json']);
214
    }
215
216
    /**
217
     * Send a GET request a list of resources or objects or a single resource or object
218
     *
219
     * @param string $path Endpoint URL path to invoke
220
     * @param array|null $query Optional query string
221
     * @param array|null $headers Headers
222
     * @return array|null Response in array format
223
     */
224
    public function get(string $path, ?array $query = null, ?array $headers = null) : ?array
225
    {
226
        $this->sendRequestRetry('GET', $path, $query, $headers);
227
228
        return $this->getResponseBody();
229
    }
230
231
    /**
232
     * GET a list of objects of a given type
233
     *
234
     * @param string $type Object type name
235
     * @param array|null $query Optional query string
236
     * @param array|null $headers Custom request headers
237
     * @return array|null Response in array format
238
     */
239
    public function getObjects(string $type = 'objects', ?array $query = null, ?array $headers = null) : ?array
240
    {
241
        return $this->get(sprintf('/%s', $type), $query, $headers);
242
    }
243
244
    /**
245
     * GET a single object of a given type
246
     *
247
     * @param int|string $id Object id
248
     * @param string $type Object type name
249
     * @param array|null $query Optional query string
250
     * @param array|null $headers Custom request headers
251
     * @return array|null Response in array format
252
     */
253
    public function getObject($id, string $type = 'objects', ?array $query = null, ?array $headers = null) : ?array
254
    {
255
        return $this->get(sprintf('/%s/%s', $type, $id), $query, $headers);
256
    }
257
258
    /**
259
     * GET a list of related objects
260
     *
261
     * @param int|string $id Object id
262
     * @param string $type Object type name
263
     * @param string $relation Relation name
264
     * @param array|null $query Optional query string
265
     * @param array|null $headers Custom request headers
266
     * @return array|null Response in array format
267
     */
268
    public function getRelated($id, string $type, string $relation, ?array $query = null, ?array $headers = null) : ?array
269
    {
270
        return $this->get(sprintf('/%s/%s/%s', $type, $id, $relation), $query, $headers);
271
    }
272
273
    /**
274
     * Add a list of related objects
275
     *
276
     * @param int|string $id Object id
277
     * @param string $type Object type name
278
     * @param string $relation Relation name
279
     * @param string $data Related objects to add, MUST contain id and type
280
     * @param array|null $headers Custom request headers
281
     * @return array|null Response in array format
282
     */
283
    public function addRelated($id, string $type, string $relation, array $data, ?array $headers = null) : ?array
284
    {
285
        $body = compact('data');
286
287
        return $this->post(sprintf('/%s/%s/relationships/%s', $type, $id, $relation), json_encode($body), $headers);
288
    }
289
290
    /**
291
     * DELETE a list of related objects
292
     *
293
     * @param int|string $id Object id
294
     * @param string $type Object type name
295
     * @param string $relation Relation name
296
     * @param string $data Related objects to remove from relation
297
     * @param array|null $headers Custom request headers
298
     * @return array|null Response in array format
299
     */
300
    public function removeRelated($id, string $type, string $relation, array $data, ?array $headers = null) : ?array
301
    {
302
        $body = compact('data');
303
304
        return $this->delete(sprintf('/%s/%s/relationships/%s', $type, $id, $relation), json_encode($body), $headers);
305
    }
306
307
    /**
308
     * Create a new object (POST) or modify an existing one (PATCH)
309
     *
310
     * @param string $type Object type name
311
     * @param array $data Object data to save
312
     * @param array|null $headers Custom request headers
313
     * @return array|null Response in array format
314
     */
315
    public function saveObject(string $type, array $data, ?array $headers = null) : ?array
316
    {
317
        $id = null;
318
        if (array_key_exists('id', $data)) {
319
            $id = $data['id'];
320
            unset($data['id']);
321
        }
322
323
        $body = [
324
            'data' => [
325
                'type' => $type,
326
                'attributes' => $data,
327
            ],
328
        ];
329
        if (!$id) {
330
            return $this->post(sprintf('/%s', $type), json_encode($body), $headers);
331
        }
332
        $body['data']['id'] = $id;
333
334
        return $this->patch(sprintf('/%s/%s', $type, $id), json_encode($body), $headers);
335
    }
336
337
    /**
338
     * Delete an object (DELETE) => move to trashcan.
339
     *
340
     * @param int|string $id Object id
341
     * @param string $type Object type name
342
     * @return array|null Response in array format
343
     */
344
    public function deleteObject($id, string $type) : ?array
345
    {
346
        return $this->delete(sprintf('/%s/%s', $type, $id));
347
    }
348
349
    /**
350
     * Remove an object => permanently remove object from trashcan.
351
     *
352
     * @param int|string $id Object id
353
     * @return array|null Response in array format
354
     */
355
    public function remove($id) : ?array
356
    {
357
        return $this->delete(sprintf('/trash/%s', $id));
358
    }
359
360
    /**
361
     * Upload file (POST)
362
     *
363
     * @param string $filename The file name
364
     * @param string $filepath File full path: could be on a local filesystem or a remote reachable URL
365
     * @param array|null $headers Custom request headers
366
     * @return array|null Response in array format
367
     * @throws BEditaClientException
368
     */
369
    public function upload($filename, $filepath, ?array $headers = null) : array
370
    {
371
        if (!file_exists($filepath)) {
372
            throw new BEditaClientException('File not found', 500);
373
        }
374
        $file = file_get_contents($filepath);
375
        if (!$file) {
376
            throw new BEditaClientException('File get contents failed', 500);
377
        }
378
        if (empty($headers['Content-Type'])) {
379
            $headers['Content-Type'] = mime_content_type($filepath);
380
        }
381
382
        return $this->post(sprintf('/streams/upload/%s', $filename), $file, $headers);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->post(sprin...name), $file, $headers) could return the type null which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
383
    }
384
385
    /**
386
     * Create media by type and body data and link it to a stream:
387
     *  - `POST /:type` with `$body` as payload, create media object
388
     *  - `PATCH /streams/:stream_id/relationships/object` modify stream adding relation to media
389
     *  - `GET /:type/:id` get media data
390
     *
391
     * @param string $streamId The stream identifier
392
     * @param string $type The type
393
     * @param array $body The body data
394
     * @return array|null Response in array format
395
     * @throws BEditaClientException
396
     */
397
    public function createMediaFromStream($streamId, $type, $body) : array
398
    {
399
        $response = $this->post(sprintf('/%s', $type), json_encode($body));
400
        if (empty($response)) {
401
            throw new BEditaClientException('Invalid response from POST ' . sprintf('/%s', $type));
402
        }
403
        $id = $response['data']['id'];
404
        $data = compact('id', 'type');
405
        $body = compact('data');
406
        $response = $this->patch(sprintf('/streams/%s/relationships/object', $streamId), json_encode($body));
407
        if (empty($response)) {
408
            throw new BEditaClientException('Invalid response from PATCH ' . sprintf('/streams/%s/relationships/object', $id));
409
        }
410
411
        return $this->getObject($data['id'], $data['type']);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getObject(...a['id'], $data['type']) could return the type null which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
412
    }
413
414
    /**
415
     * Get JSON SCHEMA of a resource or object
416
     *
417
     * @param string $type Object or resource type name
418
     * @return array|null JSON SCHEMA in array format
419
     */
420
    public function schema(string $type) : ?array
421
    {
422
        $h = ['Accept' => 'application/schema+json'];
423
424
        return $this->get(sprintf('/model/schema/%s', $type), null, $h);
425
    }
426
427
    /**
428
     * Restore object from trash
429
     *
430
     * @param int|string $id Object id
431
     * @param string $type Object type name
432
     * @return array|null Response in array format
433
     */
434
    public function restoreObject($id, string $type) : ?array
435
    {
436
        $body = [
437
            'data' => [
438
                'id' => $id,
439
                'type' => $type,
440
            ],
441
        ];
442
443
        return $this->patch(sprintf('/%s/%s', 'trash', $id), json_encode($body));
444
    }
445
446
    /**
447
     * Send a PATCH request to modify a single resource or object
448
     *
449
     * @param string $path Endpoint URL path to invoke
450
     * @param mixed $body Request body
451
     * @param array|null $headers Custom request headers
452
     * @return array|null Response in array format
453
     */
454
    public function patch(string $path, $body, ?array $headers = null) : ?array
455
    {
456
        $this->sendRequestRetry('PATCH', $path, null, $headers, $body);
457
458
        return $this->getResponseBody();
459
    }
460
461
    /**
462
     * Send a POST request for creating resources or objects or other operations like /auth
463
     *
464
     * @param string $path Endpoint URL path to invoke
465
     * @param mixed $body Request body
466
     * @param array|null $headers Custom request headers
467
     * @return array|null Response in array format
468
     */
469
    public function post(string $path, $body, ?array $headers = null) : ?array
470
    {
471
        $this->sendRequestRetry('POST', $path, null, $headers, $body);
472
473
        return $this->getResponseBody();
474
    }
475
476
    /**
477
     * Send a DELETE request
478
     *
479
     * @param string $path Endpoint URL path to invoke.
480
     * @param mixed $body Request body
481
     * @param array|null $headers Custom request headers
482
     * @return array|null Response in array format.
483
     */
484
    public function delete(string $path, $body = null, ?array $headers = null) : ?array
485
    {
486
        $this->sendRequestRetry('DELETE', $path, null, $headers, $body);
487
488
        return $this->getResponseBody();
489
    }
490
491
    /**
492
     * Send a generic JSON API request with a basic retry policy on expired token exception.
493
     *
494
     * @param string $method HTTP Method.
495
     * @param string $path Endpoint URL path.
496
     * @param array|null $query Query string parameters.
497
     * @param string[]|null $headers Custom request headers.
498
     * @param string|resource|\Psr\Http\Message\StreamInterface|null $body Request body.
499
     * @return \Psr\Http\Message\ResponseInterface
500
     */
501
    protected function sendRequestRetry(string $method, string $path, ?array $query = null, ?array $headers = null, $body = null) : ResponseInterface
502
    {
503
        try {
504
            return $this->sendRequest($method, $path, $query, $headers, $body);
505
        } catch (BEditaClientException $e) {
506
            // Handle error.
507
            $attributes = $e->getAttributes();
508
            if ($e->getCode() !== 401 || empty($attributes['code']) || $attributes['code'] !== 'be_token_expired') {
509
                // Not an expired token's fault.
510
                throw $e;
511
            }
512
513
            // Refresh and retry.
514
            $this->refreshTokens();
515
            unset($headers['Authorization']);
516
517
            return $this->sendRequest($method, $path, $query, $headers, $body);
518
        }
519
    }
520
521
    /**
522
     * Send a generic JSON API request and retrieve response $this->response
523
     *
524
     * @param string $method HTTP Method.
525
     * @param string $path Endpoint URL path.
526
     * @param array|null $query Query string parameters.
527
     * @param string[]|null $headers Custom request headers.
528
     * @param string|resource|\Psr\Http\Message\StreamInterface|null $body Request body.
529
     * @return \Psr\Http\Message\ResponseInterface
530
     * @throws BEditaClientException Throws an exception if server response code is not 20x.
531
     */
532
    protected function sendRequest(string $method, string $path, ?array $query = null, ?array $headers = null, $body = null) : ResponseInterface
533
    {
534
        $uri = new Uri($this->apiBaseUrl);
535
        $uri = $uri->withPath($uri->getPath() . '/' . $path);
536
        if ($query) {
537
            $uri = $uri->withQuery(http_build_query((array)$query));
538
        }
539
        $headers = array_merge($this->defaultHeaders, (array)$headers);
540
541
        // set default `Content-Type` if not set and $body not empty
542
        if (!empty($body)) {
543
            $headers = array_merge($this->defaultContentTypeHeader, $headers);
544
        }
545
546
        // Send the request synchronously to retrieve the response.
547
        $this->response = $this->jsonApiClient->sendRequest(new Request($method, $uri, $headers, $body));
548
        if ($this->getStatusCode() >= 400) {
549
            // Something bad just happened.
550
            $statusCode = $this->getStatusCode();
551
            $response = $this->getResponseBody();
552
553
            $code = (string)$statusCode;
554
            $reason = $this->getStatusMessage();
555
            if (!empty($response['error']['code'])) {
556
                $code = $response['error']['code'];
557
            }
558
            if (!empty($response['error']['title'])) {
559
                $reason = $response['error']['title'];
560
            }
561
562
            throw new BEditaClientException(compact('code', 'reason'), $statusCode);
563
        }
564
565
        return $this->response;
566
    }
567
568
    /**
569
     * Refresh JWT access token.
570
     *
571
     * On success `$this->tokens` data will be updated with new access and renew tokens.
572
     *
573
     * @throws \BadMethodCallException Throws an exception if client has no renew token available.
574
     * @throws \Cake\Network\Exception\ServiceUnavailableException Throws an exception if server response doesn't
575
     *      include the expected data.
576
     * @return void
577
     * @throws BEditaClientException Throws an exception if server response code is not 20x.
578
     */
579
    public function refreshTokens() : void
580
    {
581
        if (empty($this->tokens['renew'])) {
582
            throw new \BadMethodCallException('You must be logged in to renew token');
583
        }
584
585
        $headers = [
586
            'Authorization' => sprintf('Bearer %s', $this->tokens['renew']),
587
        ];
588
589
        $this->sendRequest('POST', '/auth', [], $headers);
590
        $body = $this->getResponseBody();
591
        if (empty($body['meta']['jwt'])) {
592
            throw new BEditaClientException('Invalid response from server');
593
        }
594
595
        $this->setupTokens($body['meta']);
596
    }
597
}
598