Passed
Pull Request — master (#61)
by Dante
01:21
created

BEditaClient   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 518
Duplicated Lines 0 %

Importance

Changes 31
Bugs 13 Features 2
Metric Value
eloc 129
c 31
b 13
f 2
dl 0
loc 518
rs 8.64
wmc 47

24 Methods

Rating   Name   Duplication   Size   Complexity  
A authenticate() 0 11 2
A deleteObject() 0 3 1
A replaceRelated() 0 16 1
A upload() 0 14 4
A getObjects() 0 3 1
A saveObject() 0 3 1
A thumbs() 0 8 4
A getObject() 0 7 1
A save() 0 20 3
A restoreObjects() 0 8 3
A deleteObjects() 0 13 4
A removeRelated() 0 11 1
A restoreObject() 0 8 1
A createMediaFromStream() 0 6 1
A clone() 0 13 1
A bulkEdit() 0 31 5
A createMedia() 0 8 2
A remove() 0 3 1
A addStreamToMedia() 0 14 2
A getRelated() 0 8 1
A removeObjects() 0 13 4
A schema() 0 6 1
A relationData() 0 5 1
A addRelated() 0 11 1

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
declare(strict_types=1);
3
4
/**
5
 * BEdita, API-first content management framework
6
 * Copyright 2023 Atlas Srl, ChannelWeb Srl, Chialab Srl
7
 *
8
 * Licensed under The MIT License
9
 * For full copyright and license information, please see the LICENSE.txt
10
 * Redistributions of files must retain the above copyright notice.
11
 */
12
13
namespace BEdita\SDK;
14
15
use Exception;
16
17
/**
18
 * BEdita API Client class
19
 */
20
class BEditaClient extends BaseClient
21
{
22
    /**
23
     * Classic authentication via POST /auth using username and password
24
     *
25
     * @param string $username username
26
     * @param string $password password
27
     * @return array|null Response in array format
28
     */
29
    public function authenticate(string $username, string $password): ?array
30
    {
31
        // remove `Authorization` header containing user data in JWT token when using API KEY
32
        $headers = $this->getDefaultHeaders();
33
        if (!empty($headers['X-Api-Key'])) {
34
            unset($headers['Authorization']);
35
            $this->setDefaultHeaders($headers);
36
        }
37
        $body = (string)json_encode(compact('username', 'password') + ['grant_type' => 'password']);
38
39
        return $this->post('/auth', $body, ['Content-Type' => 'application/json']);
40
    }
41
42
    /**
43
     * Bulk edit objects using `POST /bulk/edit` endpoint.
44
     * If the endpoint is not available, it fallback to edit one by one (retrocompatible way).
45
     *
46
     * $objects is an array of <type>: <array of ids>, e.g.:
47
     * [
48
     *    'articles' => [1,2,3],
49
     *    'documents' => [4,5,6],
50
     * ]
51
     *
52
     * The $attributes is an array of attributes to modify, e.g.:
53
     * [
54
     *    'title' => 'New title',
55
     *    'status' => 'off',
56
     * ]
57
     *
58
     * @param array $objects Object data to indentify objects to edit
59
     * @param array $attributes Data to modify
60
     * @return array
61
     */
62
    public function bulkEdit(array $objects, array $attributes): array
63
    {
64
        $result = ['data' => ['saved' => [], 'errors' => []]];
0 ignored issues
show
Unused Code introduced by
The assignment to $result is dead and can be removed.
Loading history...
65
        try {
66
            $result = (array)$this->post(
67
                '/bulk/edit',
68
                json_encode(['data' => compact('attributes', 'objects')]),
69
                ['Content-Type' => 'application/json'],
70
            );
71
        } catch (Exception $e) {
72
            // fallback to edit one by one, to be retrocompatible
73
            $types = array_keys($objects);
74
            foreach ($types as $type) {
75
                foreach ($objects[$type] as $id) {
76
                    try {
77
                        $response = $this->save($type, $attributes + ['id' => (string)$id]);
78
                        $result['data']['saved'][] = $response['data']['id'];
79
                    } catch (Exception $e) {
80
                        $responseBody = $this->getResponseBody();
81
                        $status = $responseBody['error']['status'];
0 ignored issues
show
Unused Code introduced by
The assignment to $status is dead and can be removed.
Loading history...
82
                        $message = $responseBody['error']['message'] ?? $e->getMessage();
83
                        $result['data']['errors'][] = [
84
                            'id' => $id,
85
                            'message' => $message,
86
                        ];
87
                    }
88
                }
89
            }
90
        }
91
92
        return $result;
93
    }
94
95
    /**
96
     * GET a list of resources or objects of a given type
97
     *
98
     * @param string $type Object type name
99
     * @param array|null $query Optional query string
100
     * @param array|null $headers Custom request headers
101
     * @return array|null Response in array format
102
     */
103
    public function getObjects(string $type = 'objects', ?array $query = null, ?array $headers = null): ?array
104
    {
105
        return $this->get(sprintf('/%s', $type), $query, $headers);
106
    }
107
108
    /**
109
     * GET a single object of a given type
110
     *
111
     * @param string|int $id Object id
112
     * @param string $type Object type name
113
     * @param array|null $query Optional query string
114
     * @param array|null $headers Custom request headers
115
     * @return array|null Response in array format
116
     */
117
    public function getObject(
118
        string|int $id,
119
        string $type = 'objects',
120
        ?array $query = null,
121
        ?array $headers = null,
122
    ): ?array {
123
        return $this->get(sprintf('/%s/%s', $type, $id), $query, $headers);
124
    }
125
126
    /**
127
     * Get a list of related resources or objects
128
     *
129
     * @param string|int $id Resource id or object uname/id
130
     * @param string $type Type name
131
     * @param string $relation Relation name
132
     * @param array|null $query Optional query string
133
     * @param array|null $headers Custom request headers
134
     * @return array|null Response in array format
135
     */
136
    public function getRelated(
137
        string|int $id,
138
        string $type,
139
        string $relation,
140
        ?array $query = null,
141
        ?array $headers = null,
142
    ): ?array {
143
        return $this->get(sprintf('/%s/%s/%s', $type, $id, $relation), $query, $headers);
144
    }
145
146
    /**
147
     * Add a list of related resources or objects
148
     *
149
     * @param string|int $id Resource id or object uname/id
150
     * @param string $type Type name
151
     * @param string $relation Relation name
152
     * @param array $data Related resources or objects to add, MUST contain id and type
153
     * @param array|null $headers Custom request headers
154
     * @return array|null Response in array format
155
     */
156
    public function addRelated(
157
        string|int $id,
158
        string $type,
159
        string $relation,
160
        array $data,
161
        ?array $headers = null,
162
    ): ?array {
163
        return $this->post(
164
            sprintf('/%s/%s/relationships/%s', $type, $id, $relation),
165
            json_encode(compact('data')),
166
            $headers,
167
        );
168
    }
169
170
    /**
171
     * Remove a list of related resources or objects
172
     *
173
     * @param string|int $id Resource id or object uname/id
174
     * @param string $type Type name
175
     * @param string $relation Relation name
176
     * @param array $data Related resources or objects to remove from relation
177
     * @param array|null $headers Custom request headers
178
     * @return array|null Response in array format
179
     */
180
    public function removeRelated(
181
        string|int $id,
182
        string $type,
183
        string $relation,
184
        array $data,
185
        ?array $headers = null,
186
    ): ?array {
187
        return $this->delete(
188
            sprintf('/%s/%s/relationships/%s', $type, $id, $relation),
189
            json_encode(compact('data')),
190
            $headers,
191
        );
192
    }
193
194
    /**
195
     * Replace a list of related resources or objects: previuosly related are removed and replaced with these.
196
     *
197
     * @param string|int $id Object id
198
     * @param string $type Object type name
199
     * @param string $relation Relation name
200
     * @param array $data Related resources or objects to insert
201
     * @param array|null $headers Custom request headers
202
     * @return array|null Response in array format
203
     */
204
    public function replaceRelated(
205
        string|int $id,
206
        string $type,
207
        string $relation,
208
        array $data,
209
        ?array $headers = null,
210
    ): ?array {
211
        return $this->patch(
212
            sprintf(
213
                '/%s/%s/relationships/%s',
214
                $type,
215
                $id,
216
                $relation,
217
            ),
218
            json_encode(compact('data')),
219
            $headers,
220
        );
221
    }
222
223
    /**
224
     * Create a new object or resource (POST) or modify an existing one (PATCH)
225
     *
226
     * @param string $type Object or resource type name
227
     * @param array $data Object or resource data to save
228
     * @param array|null $headers Custom request headers
229
     * @return array|null Response in array format
230
     */
231
    public function save(string $type, array $data, ?array $headers = null): ?array
232
    {
233
        $id = null;
234
        if (array_key_exists('id', $data)) {
235
            $id = $data['id'];
236
            unset($data['id']);
237
        }
238
239
        $body = [
240
            'data' => [
241
                'type' => $type,
242
                'attributes' => $data,
243
            ],
244
        ];
245
        if (!$id) {
246
            return $this->post(sprintf('/%s', $type), json_encode($body), $headers);
247
        }
248
        $body['data']['id'] = $id;
249
250
        return $this->patch(sprintf('/%s/%s', $type, $id), json_encode($body), $headers);
251
    }
252
253
    /**
254
     * [DEPRECATED] Create a new object (POST) or modify an existing one (PATCH)
255
     *
256
     * @param string $type Object type name
257
     * @param array $data Object data to save
258
     * @param array|null $headers Custom request headers
259
     * @return array|null Response in array format
260
     * @deprecated Use `save()` method instead
261
     * @codeCoverageIgnore
262
     */
263
    public function saveObject(string $type, array $data, ?array $headers = null): ?array
264
    {
265
        return $this->save($type, $data, $headers);
266
    }
267
268
    /**
269
     * Delete an object (DELETE) => move to trashcan.
270
     *
271
     * @param string|int $id Object id
272
     * @param string $type Object type name
273
     * @return array|null Response in array format
274
     */
275
    public function deleteObject(string|int $id, string $type): ?array
276
    {
277
        return $this->delete(sprintf('/%s/%s', $type, $id));
278
    }
279
280
    /**
281
     * Delete objects (DELETE) => move to trashcan.
282
     *
283
     * @param array $ids Object ids
284
     * @param string|null $type Object type name
285
     * @return array|null Response in array format
286
     */
287
    public function deleteObjects(array $ids, string $type = 'objects'): ?array
288
    {
289
        $response = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $response is dead and can be removed.
Loading history...
290
        try {
291
            $response = $this->delete(sprintf('/%s?ids=%s', $type, implode(',', $ids)));
292
        } catch (Exception $e) {
293
            // fallback to delete one by one, to be retrocompatible
294
            foreach ($ids as $id) {
295
                $response = !empty($response) ? $response : $this->deleteObject($id, $type);
296
            }
297
        }
298
299
        return $response;
300
    }
301
302
    /**
303
     * Remove an object => permanently remove object from trashcan.
304
     *
305
     * @param string|int $id Object id
306
     * @return array|null Response in array format
307
     */
308
    public function remove(string|int $id): ?array
309
    {
310
        return $this->delete(sprintf('/trash/%s', $id));
311
    }
312
313
    /**
314
     * Remove objects => permanently remove objects from trashcan.
315
     *
316
     * @param array $ids Object ids
317
     * @return array|null Response in array format
318
     */
319
    public function removeObjects(array $ids): ?array
320
    {
321
        $response = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $response is dead and can be removed.
Loading history...
322
        try {
323
            $response = $this->delete(sprintf('/trash?ids=%s', implode(',', $ids)));
324
        } catch (Exception $e) {
325
            // fallback to delete one by one, to be retrocompatible
326
            foreach ($ids as $id) {
327
                $response = !empty($response) ? $response : $this->remove($id);
328
            }
329
        }
330
331
        return $response;
332
    }
333
334
    /**
335
     * Upload file (POST)
336
     *
337
     * @param string $filename The file name
338
     * @param string $filepath File full path: could be on a local filesystem or a remote reachable URL
339
     * @param array|null $headers Custom request headers
340
     * @return array|null Response in array format
341
     * @throws \BEdita\SDK\BEditaClientException
342
     */
343
    public function upload(string $filename, string $filepath, ?array $headers = null): ?array
344
    {
345
        if (!file_exists($filepath)) {
346
            throw new BEditaClientException('File not found', 500);
347
        }
348
        $file = file_get_contents($filepath);
349
        if (!$file) {
350
            throw new BEditaClientException('File get contents failed', 500);
351
        }
352
        if (empty($headers['Content-Type'])) {
353
            $headers['Content-Type'] = mime_content_type($filepath);
354
        }
355
356
        return $this->post(sprintf('/streams/upload/%s', $filename), $file, $headers);
357
    }
358
359
    /**
360
     * Create media by type and body data and link it to a stream:
361
     *  - `POST /:type` with `$body` as payload, create media object
362
     *  - `PATCH /streams/:stream_id/relationships/object` modify stream adding relation to media
363
     *  - `GET /:type/:id` get media data
364
     *
365
     * @param string $streamId The stream identifier
366
     * @param string $type The type
367
     * @param array $body The body data
368
     * @return array|null Response in array format
369
     * @throws \BEdita\SDK\BEditaClientException
370
     */
371
    public function createMediaFromStream(string $streamId, string $type, array $body): ?array
372
    {
373
        $id = $this->createMedia($type, $body);
374
        $this->addStreamToMedia($streamId, $id, $type);
375
376
        return $this->getObject($id, $type);
377
    }
378
379
    /**
380
     * Create media.
381
     *
382
     * @param string $type The type
383
     * @param array $body The body
384
     * @return string
385
     * @throws \BEdita\SDK\BEditaClientException
386
     */
387
    public function createMedia(string $type, array $body): string
388
    {
389
        $response = $this->post(sprintf('/%s', $type), json_encode($body));
390
        if (empty($response)) {
391
            throw new BEditaClientException('Invalid response from POST ' . sprintf('/%s', $type));
392
        }
393
394
        return (string)$response['data']['id'];
395
    }
396
397
    /**
398
     * Add stream to media using patch /streams/%s/relationships/object.
399
     *
400
     * @param string $streamId The stream ID
401
     * @param string $id The object ID
402
     * @param string $type The type
403
     * @return void
404
     * @throws \BEdita\SDK\BEditaClientException
405
     */
406
    public function addStreamToMedia(string $streamId, string $id, string $type): void
407
    {
408
        $response = $this->patch(
409
            sprintf('/streams/%s/relationships/object', $streamId),
410
            json_encode([
411
                'data' => [
412
                    'id' => $id,
413
                    'type' => $type,
414
                ],
415
            ]),
416
        );
417
        if (empty($response)) {
418
            throw new BEditaClientException(
419
                'Invalid response from PATCH ' . sprintf('/streams/%s/relationships/object', $id),
420
            );
421
        }
422
    }
423
424
    /**
425
     * Thumbnail request using `GET /media/thumbs` endpoint
426
     *
427
     *  Usage:
428
     *          thumbs(123) => `GET /media/thumbs/123`
429
     *          thumbs(123, ['preset' => 'glide']) => `GET /media/thumbs/123&preset=glide`
430
     *          thumbs(null, ['ids' => '123,124,125']) => `GET /media/thumbs?ids=123,124,125`
431
     *          thumbs(null, ['ids' => '123,124,125', 'preset' => 'async']) => `GET /media/thumbs?ids=123,124,125&preset=async`
432
     *          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))
433
     *
434
     * @param int|null $id the media Id.
435
     * @param array $query The query params for thumbs call.
436
     * @return array|null Response in array format
437
     */
438
    public function thumbs(?int $id = null, array $query = []): ?array
439
    {
440
        if (empty($id) && empty($query['ids'])) {
441
            throw new BEditaClientException('Invalid empty id|ids for thumbs');
442
        }
443
        $endpoint = empty($id) ? '/media/thumbs' : sprintf('/media/thumbs/%d', $id);
444
445
        return $this->get($endpoint, $query);
446
    }
447
448
    /**
449
     * Get JSON SCHEMA of a resource or object
450
     *
451
     * @param string $type Object or resource type name
452
     * @return array|null JSON SCHEMA in array format
453
     */
454
    public function schema(string $type): ?array
455
    {
456
        return $this->get(
457
            sprintf('/model/schema/%s', $type),
458
            null,
459
            ['Accept' => 'application/schema+json'],
460
        );
461
    }
462
463
    /**
464
     * Get info of a relation (data, params) and get left/right object types
465
     *
466
     * @param string $name relation name
467
     * @return array|null relation data in array format
468
     */
469
    public function relationData(string $name): ?array
470
    {
471
        return $this->get(
472
            sprintf('/model/relations/%s', $name),
473
            ['include' => 'left_object_types,right_object_types'],
474
        );
475
    }
476
477
    /**
478
     * Restore object from trash
479
     *
480
     * @param string|int $id Object id
481
     * @param string $type Object type name
482
     * @return array|null Response in array format
483
     */
484
    public function restoreObject(string|int $id, string $type): ?array
485
    {
486
        return $this->patch(
487
            sprintf('/trash/%s', $id),
488
            json_encode([
489
                'data' => [
490
                    'id' => $id,
491
                    'type' => $type,
492
                ],
493
            ]),
494
        );
495
    }
496
497
    /**
498
     * Restore objects from trash
499
     *
500
     * @param array $ids Object ids
501
     * @param string|null $type Object type
502
     * @return array|null Response in array format
503
     */
504
    public function restoreObjects(array $ids, string $type = 'objects'): ?array
505
    {
506
        $res = null;
507
        foreach ($ids as $id) {
508
            $res = !empty($res) ? $res : $this->restoreObject($id, $type);
509
        }
510
511
        return $res;
512
    }
513
514
    /**
515
     * Clone an object.
516
     * This requires BEdita API >= 5.36.0
517
     *
518
     * @param string $type Object type name
519
     * @param string $id Source object id
520
     * @param array $modified Object attributes to overwrite
521
     * @param array $include Associations included: can be 'relationships' and 'translations'
522
     * @param array|null $headers Custom request headers
523
     * @return array|null Response in array format
524
     */
525
    public function clone(string $type, string $id, array $modified, array $include, ?array $headers = null): ?array
526
    {
527
        $body = json_encode([
528
            'data' => [
529
                'type' => $type,
530
                'attributes' => $modified,
531
                'meta' => [
532
                    'include' => $include,
533
                ],
534
            ],
535
        ]);
536
537
        return $this->post(sprintf('/%s/%s/actions/clone', $type, $id), $body, $headers);
538
    }
539
}
540