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

BEditaClient::authenticate()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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