ModulesComponent   F
last analyzed

Complexity

Total Complexity 84

Size/Duplication

Total Lines 730
Duplicated Lines 0 %

Importance

Changes 3
Bugs 2 Features 1
Metric Value
eloc 296
dl 0
loc 730
rs 2
c 3
b 2
f 1
wmc 84

24 Methods

Rating   Name   Duplication   Size   Complexity  
A beforeFilter() 0 9 2
A getModules() 0 25 2
A startup() 0 26 6
A isAbstract() 0 3 1
B modulesByAccessControl() 0 38 11
A modulesFromMeta() 0 17 2
A oEmbedMeta() 0 3 1
A getProject() 0 18 1
A objectTypes() 0 13 5
A removeStream() 0 15 3
B checkRequestForUpload() 0 31 7
A assocStreamToMedia() 0 26 3
A skipSavePermissions() 0 18 2
A getRelated() 0 28 4
A relatedTypes() 0 5 1
A setupAttributes() 0 4 1
A saveRelated() 0 6 2
A saveRelatedObjects() 0 18 6
A relationsSchema() 0 16 5
A skipSaveRelated() 0 27 6
A setupRelationsMeta() 0 21 1
A relationLabels() 0 15 2
A skipSaveObject() 0 24 4
B upload() 0 50 6

How to fix   Complexity   

Complex Class

Complex classes like ModulesComponent 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 ModulesComponent, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * BEdita, API-first content management framework
4
 * Copyright 2022 ChannelWeb Srl, Chialab Srl
5
 *
6
 * This file is part of BEdita: you can redistribute it and/or modify
7
 * it under the terms of the GNU Lesser General Public License as published
8
 * by the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * See LICENSE.LGPL or <http://gnu.org/licenses/lgpl-3.0.html> for more details.
12
 */
13
namespace App\Controller\Component;
14
15
use App\Core\Exception\UploadException;
16
use App\Utility\DateRangesTools;
17
use App\Utility\OEmbed;
18
use App\Utility\RelationsTools;
19
use App\Utility\SchemaTrait;
20
use BEdita\WebTools\ApiClientProvider;
21
use Cake\Cache\Cache;
22
use Cake\Controller\Component;
23
use Cake\Core\Configure;
24
use Cake\Event\Event;
25
use Cake\Event\EventInterface;
26
use Cake\Http\Client\Response;
27
use Cake\Http\Exception\BadRequestException;
28
use Cake\Http\Exception\InternalErrorException;
29
use Cake\I18n\I18n;
30
use Cake\Utility\Hash;
31
32
/**
33
 * Component to load available modules.
34
 *
35
 * @property \Authentication\Controller\Component\AuthenticationComponent $Authentication
36
 * @property \App\Controller\Component\ChildrenComponent $Children
37
 * @property \App\Controller\Component\ConfigComponent $Config
38
 * @property \App\Controller\Component\ParentsComponent $Parents
39
 * @property \App\Controller\Component\SchemaComponent $Schema
40
 */
41
class ModulesComponent extends Component
42
{
43
    use SchemaTrait;
44
45
    /**
46
     * Fixed relationships to be loaded for each object
47
     *
48
     * @var array
49
     */
50
    public const FIXED_RELATIONSHIPS = [
51
        'parent',
52
        'children',
53
        'parents',
54
        'translations',
55
        'streams',
56
        'roles',
57
    ];
58
59
    /**
60
     * @inheritDoc
61
     */
62
    public $components = ['Authentication', 'Children', 'Config', 'Parents', 'Schema'];
63
64
    /**
65
     * @inheritDoc
66
     */
67
    protected $_defaultConfig = [
68
        'currentModuleName' => null,
69
        'clearHomeCache' => false,
70
    ];
71
72
    /**
73
     * Project modules for a user from `/home` endpoint
74
     *
75
     * @var array
76
     */
77
    protected $modules = [];
78
79
    /**
80
     * Other "logic" modules, non objects
81
     *
82
     * @var array
83
     */
84
    protected $otherModules = [
85
        'tags' => [
86
            'name' => 'tags',
87
            'hints' => ['allow' => ['GET', 'POST', 'PATCH', 'DELETE']],
88
        ],
89
    ];
90
91
    /**
92
     * @inheritDoc
93
     */
94
    public function beforeFilter(EventInterface $event): ?Response
95
    {
96
        /** @var \Authentication\Identity|null $user */
97
        $user = $this->Authentication->getIdentity();
98
        if (!empty($user)) {
99
            $this->getController()->set('modules', $this->getModules());
100
        }
101
102
        return null;
103
    }
104
105
    /**
106
     * Read modules and project info from `/home' endpoint.
107
     *
108
     * @return void
109
     */
110
    public function startup(): void
111
    {
112
        /** @var \Authentication\Identity|null $user */
113
        $user = $this->Authentication->getIdentity();
114
        if (empty($user) || !$user->get('id')) {
115
            $this->getController()->set(['modules' => [], 'project' => []]);
116
117
            return;
118
        }
119
120
        if ($this->getConfig('clearHomeCache')) {
121
            Cache::delete(sprintf('home_%d', $user->get('id')));
122
        }
123
124
        $project = $this->getProject();
125
        $uploadable = (array)Hash::get($this->Schema->objectTypesFeatures(), 'uploadable');
126
        $this->getController()->set(compact('project', 'uploadable'));
127
128
        $currentModuleName = $this->getConfig('currentModuleName');
129
        $modules = (array)$this->getController()->viewBuilder()->getVar('modules');
130
        if (!empty($currentModuleName)) {
131
            $currentModule = Hash::get($modules, $currentModuleName);
132
        }
133
134
        if (!empty($currentModule)) {
135
            $this->getController()->set(compact('currentModule'));
136
        }
137
    }
138
139
    /**
140
     * Create internal list of available modules in `$this->modules` as an array with `name` as key
141
     * and return it.
142
     * Modules are created from configuration and merged with information read from `/home` endpoint
143
     *
144
     * @return array
145
     */
146
    public function getModules(): array
147
    {
148
        $modules = (array)Configure::read('Modules');
149
        $pluginModules = array_filter($modules, function ($item) {
150
            return !empty($item['route']);
151
        });
152
        $metaModules = $this->modulesFromMeta() + $this->otherModules;
153
        $modules = array_intersect_key($modules, $metaModules);
154
        array_walk(
155
            $modules,
156
            function (&$data, $key) use ($metaModules) {
157
                $data = array_merge((array)Hash::get($metaModules, $key), $data);
158
            }
159
        );
160
        $this->modules = array_merge(
161
            $modules,
162
            array_diff_key($metaModules, $modules),
163
            $pluginModules
164
        );
165
        $this->modulesByAccessControl();
166
        if (!$this->Schema->tagsInUse()) {
167
            unset($this->modules['tags']);
168
        }
169
170
        return $this->modules;
171
    }
172
173
    /**
174
     * This filters modules and apply 'AccessControl' config by user role, if any.
175
     * Module can be "hidden": remove from modules.
176
     * Module can be "readonly": adjust "hints.allow" for module.
177
     *
178
     * @return void
179
     */
180
    protected function modulesByAccessControl(): void
181
    {
182
        $accessControl = (array)Configure::read('AccessControl');
183
        if (empty($accessControl)) {
184
            return;
185
        }
186
        /** @var \Authentication\Identity|null $user */
187
        $user = $this->Authentication->getIdentity();
188
        $userRoles = (array)$user->get('roles');
189
        if (empty($user) || empty($user->getOriginalData()) || in_array('admin', $userRoles)) {
190
            return;
191
        }
192
        $roles = array_intersect(array_keys($accessControl), $userRoles);
193
        $modules = (array)array_keys($this->modules);
194
        $hidden = [];
195
        $readonly = [];
196
        $write = [];
197
        foreach ($roles as $role) {
198
            $h = (array)Hash::get($accessControl, sprintf('%s.hidden', $role));
199
            $hidden = empty($hidden) ? $h : array_intersect($hidden, $h);
200
            $r = (array)Hash::get($accessControl, sprintf('%s.readonly', $role));
201
            $readonly = empty($readonly) ? $r : array_intersect($readonly, $r);
202
            $write = array_unique(array_merge($write, array_diff($modules, $hidden, $readonly)));
203
        }
204
        // Note: https://github.com/bedita/manager/issues/969 Accesses priority is "write" > "read" > "hidden"
205
        $readonly = array_diff($readonly, $write);
206
        $hidden = array_diff($hidden, $readonly, $write);
207
        if (empty($hidden) && empty($readonly)) {
208
            return;
209
        }
210
        // remove "hidden"
211
        $this->modules = array_diff_key($this->modules, array_flip($hidden));
212
        // make sure $readonly contains valid module names
213
        $readonly = array_intersect($readonly, array_keys($this->modules));
214
        foreach ($readonly as $key) {
215
            $path = sprintf('%s.hints.allow', $key);
216
            $allow = (array)Hash::get($this->modules, $path);
217
            $this->modules[$key]['hints']['allow'] = array_diff($allow, ['POST', 'PATCH', 'DELETE']);
218
        }
219
    }
220
221
    /**
222
     * Modules data from `/home` endpoint 'meta' response.
223
     * Modules are object endpoints from BE4 API
224
     *
225
     * @return array
226
     */
227
    protected function modulesFromMeta(): array
228
    {
229
        /** @var \Authentication\Identity $user */
230
        $user = $this->Authentication->getIdentity();
231
        $meta = $this->getMeta($user);
232
        $modules = collection(Hash::get($meta, 'resources', []))
233
            ->map(function (array $data, $endpoint) {
234
                $name = substr($endpoint, 1);
235
236
                return $data + compact('name');
237
            })
238
            ->reject(function (array $data) {
239
                return Hash::get($data, 'hints.object_type') !== true && !in_array(Hash::get($data, 'name'), ['trash', 'translations']);
240
            })
241
            ->toList();
242
243
        return Hash::combine($modules, '{n}.name', '{n}');
244
    }
245
246
    /**
247
     * Get information about current project.
248
     *
249
     * @return array
250
     */
251
    public function getProject(): array
252
    {
253
        /** @var \Authentication\Identity $user */
254
        $user = $this->Authentication->getIdentity();
255
        $api = $this->getMeta($user);
256
        $apiName = (string)Hash::get($api, 'project.name');
257
        $apiName = str_replace('API', '', $apiName);
258
        $api['project']['name'] = $apiName;
259
260
        return [
261
            'api' => (array)Hash::get($api, 'project'),
262
            'beditaApi' => [
263
                'name' => (string)Hash::get(
264
                    (array)Configure::read('Project'),
265
                    'name',
266
                    (string)Hash::get($api, 'project.name')
267
                ),
268
                'version' => (string)Hash::get($api, 'version'),
269
            ],
270
        ];
271
    }
272
273
    /**
274
     * Check if an object type is abstract or concrete.
275
     * This method MUST NOT be called from `beforeRender` since `$this->modules` array is still not initialized.
276
     *
277
     * @param string $name Name of object type.
278
     * @return bool True if abstract, false if concrete
279
     */
280
    public function isAbstract(string $name): bool
281
    {
282
        return (bool)Hash::get($this->modules, sprintf('%s.hints.multiple_types', $name), false);
283
    }
284
285
    /**
286
     * Get list of object types
287
     * This method MUST NOT be called from `beforeRender` since `$this->modules` array is still not initialized.
288
     *
289
     * @param bool|null $abstract Only abstract or concrete types.
290
     * @return array Type names list
291
     */
292
    public function objectTypes(?bool $abstract = null): array
293
    {
294
        $types = [];
295
        foreach ($this->modules as $name => $data) {
296
            if (empty($data['hints']['object_type'])) {
297
                continue;
298
            }
299
            if ($abstract === null || $data['hints']['multiple_types'] === $abstract) {
300
                $types[] = $name;
301
            }
302
        }
303
304
        return $types;
305
    }
306
307
    /**
308
     * Read oEmbed metadata
309
     *
310
     * @param string $url Remote URL
311
     * @return array|null
312
     * @codeCoverageIgnore
313
     */
314
    protected function oEmbedMeta(string $url): ?array
315
    {
316
        return (new OEmbed())->readMetadata($url);
317
    }
318
319
    /**
320
     * Upload a file and store it in a media stream
321
     * Or create a remote media trying to get some metadata via oEmbed
322
     *
323
     * @param array $requestData The request data from form
324
     * @return void
325
     */
326
    public function upload(array &$requestData): void
327
    {
328
        $uploadBehavior = Hash::get($requestData, 'upload_behavior', 'file');
329
330
        if ($uploadBehavior === 'embed' && !empty($requestData['remote_url'])) {
331
            $data = $this->oEmbedMeta($requestData['remote_url']);
332
            $requestData = array_filter($requestData) + $data;
333
334
            return;
335
        }
336
        if (empty($requestData['file'])) {
337
            return;
338
        }
339
340
        // verify upload form data
341
        if ($this->checkRequestForUpload($requestData)) {
342
            /** @var \Laminas\Diactoros\UploadedFile $file */
343
            $file = $requestData['file'];
344
            $filepath = $file->getStream()->getMetadata('uri');
345
            $content = file_get_contents($filepath);
346
347
            // upload file
348
            $filename = basename($file->getClientFileName());
349
            $headers = ['Content-Type' => $file->getClientMediaType()];
350
            $apiClient = ApiClientProvider::getApiClient();
351
            $type = $this->getController()->getRequest()->getParam('object_type');
352
353
            if (empty($requestData['id'])) {
354
                $response = $apiClient->post(
355
                    sprintf('/%s/upload/%s', $type, $filename),
356
                    $content,
357
                    $headers
358
                );
359
                $requestData['id'] = Hash::get($response, 'data.id');
360
                unset($requestData['file'], $requestData['remote_url']);
361
362
                return;
363
            }
364
365
            // remove stream
366
            $this->removeStream($requestData);
367
368
            // create stream
369
            $response = $apiClient->upload($filename, $filepath, $headers);
370
371
            // link stream to media
372
            $streamUuid = Hash::get($response, 'data.id');
373
            $response = $this->assocStreamToMedia($streamUuid, $requestData, $filename);
374
        }
375
        unset($requestData['file'], $requestData['remote_url']);
376
    }
377
378
    /**
379
     * Remove a stream from a media, if any
380
     *
381
     * @param array $requestData The request data from form
382
     * @return bool
383
     */
384
    public function removeStream(array $requestData): bool
385
    {
386
        if (empty($requestData['id'])) {
387
            return false;
388
        }
389
390
        $apiClient = ApiClientProvider::getApiClient();
391
        $response = $apiClient->get(sprintf('/%s/%s/streams', $requestData['model-type'], $requestData['id']));
392
        if (empty($response['data'])) { // no streams for media
393
            return false;
394
        }
395
        $streamId = Hash::get($response, 'data.0.id');
396
        $apiClient->deleteObject($streamId, 'streams');
397
398
        return true;
399
    }
400
401
    /**
402
     * Associate a stream to a media using API
403
     * If $requestData['id'] is null, create media from stream.
404
     * If $requestData['id'] is not null, replace properly related stream.
405
     *
406
     * @param string $streamId The stream ID
407
     * @param array $requestData The request data
408
     * @param string $defaultTitle The default title for media
409
     * @return string The media ID
410
     */
411
    public function assocStreamToMedia(string $streamId, array &$requestData, string $defaultTitle): string
412
    {
413
        $apiClient = ApiClientProvider::getApiClient();
414
        $type = $requestData['model-type'];
415
        if (empty($requestData['id'])) {
416
            // create media from stream
417
            // save only `title` (filename if not set) and `status` in new media object
418
            $attributes = array_filter([
419
                'title' => !empty($requestData['title']) ? $requestData['title'] : $defaultTitle,
420
                'status' => Hash::get($requestData, 'status'),
421
            ]);
422
            $data = compact('type', 'attributes');
423
            $body = compact('data');
424
            $response = $apiClient->createMediaFromStream($streamId, $type, $body);
425
            // `title` and `status` saved here, remove from next save
426
            unset($requestData['title'], $requestData['status']);
427
428
            return (string)Hash::get($response, 'data.id');
429
        }
430
431
        // assoc existing media to stream
432
        $id = (string)Hash::get($requestData, 'id');
433
        $data = compact('id', 'type');
434
        $apiClient->replaceRelated($streamId, 'streams', 'object', $data);
435
436
        return $id;
437
    }
438
439
    /**
440
     * Check request data for upload and return true if upload is boht possible and needed
441
     *
442
     * @param array $requestData The request data
443
     * @return bool true if upload is possible and needed
444
     */
445
    public function checkRequestForUpload(array $requestData): bool
446
    {
447
        /** @var \Laminas\Diactoros\UploadedFile $file */
448
        $file = $requestData['file'];
449
        $error = $file->getError();
450
        // check if change file is empty
451
        if ($error === UPLOAD_ERR_NO_FILE) {
452
            return false;
453
        }
454
455
        // if upload error, throw exception
456
        if ($error !== UPLOAD_ERR_OK) {
457
            throw new UploadException(null, $error);
458
        }
459
460
        // verify presence and value of 'name', 'tmp_name', 'type'
461
        $name = $file->getClientFileName();
462
        if (empty($name)) {
463
            throw new InternalErrorException('Invalid form data: file.name');
464
        }
465
        $uri = $file->getStream()->getMetadata('uri');
466
        if (empty($uri)) {
467
            throw new InternalErrorException('Invalid form data: file.tmp_name');
468
        }
469
470
        // verify 'model-type'
471
        if (empty($requestData['model-type']) || !is_string($requestData['model-type'])) {
472
            throw new InternalErrorException('Invalid form data: model-type');
473
        }
474
475
        return true;
476
    }
477
478
    /**
479
     * Check if save can be skipped.
480
     * This is used to avoid saving object with no changes.
481
     *
482
     * @param string $id The object ID
483
     * @param array $requestData The request data
484
     * @return bool True if save can be skipped, false otherwise
485
     */
486
    public function skipSaveObject(string $id, array &$requestData): bool
487
    {
488
        if (empty($id)) {
489
            return false;
490
        }
491
        if (isset($requestData['date_ranges'])) {
492
            // check if date_ranges has changed
493
            $type = $this->getController()->getRequest()->getParam('object_type');
494
            $response = ApiClientProvider::getApiClient()->getObject($id, $type, ['fields' => 'date_ranges']);
495
            $actualDateRanges = (array)Hash::get($response, 'data.attributes.date_ranges');
496
            $dr1 = DateRangesTools::toString($actualDateRanges);
497
            $requestDateRanges = (array)Hash::get($requestData, 'date_ranges');
498
            $dr2 = DateRangesTools::toString($requestDateRanges);
499
            if ($dr1 === $dr2) {
500
                unset($requestData['date_ranges']);
501
            } else {
502
                return false;
503
            }
504
        }
505
        $data = array_filter($requestData, function ($key) {
506
            return !in_array($key, ['id', 'date_ranges', 'permissions']);
507
        }, ARRAY_FILTER_USE_KEY);
508
509
        return empty($data);
510
    }
511
512
    /**
513
     * Check if save related can be skipped.
514
     * This is used to avoid saving object relations with no changes.
515
     *
516
     * @param string $id The object ID
517
     * @param array $relatedData The related data
518
     * @return bool True if save related can be skipped, false otherwise
519
     */
520
    public function skipSaveRelated(string $id, array &$relatedData): bool
521
    {
522
        if (empty($relatedData)) {
523
            return true;
524
        }
525
        $methods = (array)Hash::extract($relatedData, '{n}.method');
526
        if (in_array('addRelated', $methods) || in_array('removeRelated', $methods)) {
527
            return false;
528
        }
529
        // check replaceRelated
530
        $type = $this->getController()->getRequest()->getParam('object_type');
531
        $rr = $relatedData;
532
        foreach ($rr as $method => $data) {
533
            $actualRelated = (array)ApiClientProvider::getApiClient()->getRelated($id, $type, $data['relation']);
534
            $actualRelated = (array)Hash::get($actualRelated, 'data');
535
            $actualRelated = RelationsTools::toString($actualRelated);
536
            $requestRelated = (array)Hash::get($data, 'relatedIds', []);
537
            $requestRelated = RelationsTools::toString($requestRelated);
538
            if ($actualRelated === $requestRelated) {
539
                unset($relatedData[$method]);
540
                continue;
541
            }
542
543
            return false;
544
        }
545
546
        return empty($relatedData);
547
    }
548
549
    /**
550
     * Check if save permissions can be skipped.
551
     * This is used to avoid saving object permissions with no changes.
552
     *
553
     * @param string $id The object ID
554
     * @param array $requestPermissions The request permissions
555
     * @param array $schema The object type schema
556
     * @return bool True if save permissions can be skipped, false otherwise
557
     */
558
    public function skipSavePermissions(string $id, array $requestPermissions, array $schema): bool
559
    {
560
        if (!in_array('Permissions', (array)Hash::get($schema, 'associations'))) {
561
            return true;
562
        }
563
        $requestPermissions = array_map(
564
            function ($role) {
565
                return (int)$role;
566
            },
567
            $requestPermissions
568
        );
569
        sort($requestPermissions);
570
        $query = ['filter' => ['object_id' => $id], 'page_size' => 100];
571
        $objectPermissions = (array)ApiClientProvider::getApiClient()->getObjects('object_permissions', $query);
572
        $actualPermissions = (array)Hash::extract($objectPermissions, 'data.{n}.attributes.role_id');
573
        sort($actualPermissions);
574
575
        return $actualPermissions === $requestPermissions;
576
    }
577
578
    /**
579
     * Set current attributes from loaded $object data in `currentAttributes`.
580
     *
581
     * @param array $object The object.
582
     * @return void
583
     */
584
    public function setupAttributes(array &$object): void
585
    {
586
        $currentAttributes = json_encode((array)Hash::get($object, 'attributes'));
587
        $this->getController()->set(compact('currentAttributes'));
588
    }
589
590
    /**
591
     * Setup relations information metadata.
592
     *
593
     * @param array $schema Relations schema.
594
     * @param array $relationships Object relationships.
595
     * @param array $order Ordered names inside 'main' and 'aside' keys.
596
     * @param array $hidden List of hidden relations.
597
     * @param array $readonly List of readonly relations.
598
     * @return void
599
     */
600
    public function setupRelationsMeta(array $schema, array $relationships, array $order = [], array $hidden = [], array $readonly = []): void
601
    {
602
        // relations between objects
603
        $relationsSchema = $this->relationsSchema($schema, $relationships, $hidden, $readonly);
604
        // relations between objects and resources
605
        $resourceRelations = array_diff(array_keys($relationships), array_keys($relationsSchema), $hidden, self::FIXED_RELATIONSHIPS);
606
        // set objectRelations array with name as key and label as value
607
        $relationNames = array_keys($relationsSchema);
608
609
        // define 'main' and 'aside' relation groups
610
        $aside = array_intersect((array)Hash::get($order, 'aside'), $relationNames);
611
        $relationNames = array_diff($relationNames, $aside);
612
        $main = array_intersect((array)Hash::get($order, 'main'), $relationNames);
613
        $main = array_unique(array_merge($main, $relationNames));
614
615
        $objectRelations = [
616
            'main' => $this->relationLabels($relationsSchema, $main),
617
            'aside' => $this->relationLabels($relationsSchema, $aside),
618
        ];
619
620
        $this->getController()->set(compact('relationsSchema', 'resourceRelations', 'objectRelations'));
621
    }
622
623
    /**
624
     * Relations schema by schema and relationships.
625
     *
626
     * @param array $schema The schema
627
     * @param array $relationships The relationships
628
     * @param array $hidden Hidden relationships
629
     * @param array $readonly Readonly relationships
630
     * @return array
631
     */
632
    protected function relationsSchema(array $schema, array $relationships, array $hidden = [], array $readonly = []): array
633
    {
634
        $types = $this->objectTypes(false);
635
        sort($types);
636
        $relationsSchema = array_diff_key(array_intersect_key($schema, $relationships), array_flip($hidden));
637
638
        foreach ($relationsSchema as $relName => &$relSchema) {
639
            if (in_array('objects', (array)Hash::get($relSchema, 'right'))) {
640
                $relSchema['right'] = $types;
641
            }
642
            if (!empty($relationships[$relName]['readonly']) || in_array($relName, $readonly)) {
643
                $relSchema['readonly'] = true;
644
            }
645
        }
646
647
        return $relationsSchema;
648
    }
649
650
    /**
651
     * Retrieve associative array with names as keys and labels as values.
652
     *
653
     * @param array $relationsSchema Relations schema.
654
     * @param array $names Relation names.
655
     * @return array
656
     */
657
    protected function relationLabels(array &$relationsSchema, array $names): array
658
    {
659
        return (array)array_combine(
660
            $names,
661
            array_map(
662
                function ($r) use ($relationsSchema) {
663
                    // return 'label' or 'inverse_label' looking at 'name'
664
                    $attributes = $relationsSchema[$r]['attributes'];
665
                    if ($r === $attributes['name']) {
666
                        return $attributes['label'];
667
                    }
668
669
                    return $attributes['inverse_label'];
670
                },
671
                $names
672
            )
673
        );
674
    }
675
676
    /**
677
     * Get related types from relation name.
678
     *
679
     * @param array $schema Relations schema.
680
     * @param string $relation Relation name.
681
     * @return array
682
     */
683
    public function relatedTypes(array $schema, string $relation): array
684
    {
685
        $relationsSchema = (array)Hash::get($schema, $relation);
686
687
        return (array)Hash::get($relationsSchema, 'right');
688
    }
689
690
    /**
691
     * Save related objects.
692
     *
693
     * @param string $id Object ID
694
     * @param string $type Object type
695
     * @param array $relatedData Related objects data
696
     * @return void
697
     */
698
    public function saveRelated(string $id, string $type, array $relatedData): void
699
    {
700
        foreach ($relatedData as $data) {
701
            $this->saveRelatedObjects($id, $type, $data);
702
            $event = new Event('Controller.afterSaveRelated', $this, compact('id', 'type', 'data'));
703
            $this->getController()->getEventManager()->dispatch($event);
704
        }
705
    }
706
707
    /**
708
     * Save related objects per object by ID.
709
     *
710
     * @param string $id Object ID
711
     * @param string $type Object type
712
     * @param array $data Related object data
713
     * @return array|null
714
     * @throws \Cake\Http\Exception\BadRequestException
715
     */
716
    public function saveRelatedObjects(string $id, string $type, array $data): ?array
717
    {
718
        $method = (string)Hash::get($data, 'method');
719
        if (!in_array($method, ['addRelated', 'removeRelated', 'replaceRelated'])) {
720
            throw new BadRequestException(__('Bad related data method'));
721
        }
722
        $relation = (string)Hash::get($data, 'relation');
723
        $related = $this->getRelated($data);
724
        if ($relation === 'parent' && $type === 'folders') {
725
            return $this->Parents->{$method}($id, $related);
726
        }
727
        if ($relation === 'children' && $type === 'folders') {
728
            return $this->Children->{$method}($id, $related);
729
        }
730
        $lang = I18n::getLocale();
731
        $headers = ['Accept-Language' => $lang];
732
733
        return ApiClientProvider::getApiClient()->{$method}($id, $type, $relation, $related, $headers);
734
    }
735
736
    /**
737
     * Get related objects.
738
     * If related object has no ID, it will be created.
739
     *
740
     * @param array $data Related object data
741
     * @return array
742
     */
743
    public function getRelated(array $data): array
744
    {
745
        $related = (array)Hash::get($data, 'relatedIds');
746
        if (empty($related)) {
747
            return [];
748
        }
749
        $relatedObjects = [];
750
        foreach ($related as $obj) {
751
            if (!empty($obj['id'])) {
752
                $relatedObjects[] = [
753
                    'id' => $obj['id'],
754
                    'type' => $obj['type'],
755
                    'meta' => (array)Hash::get($obj, 'meta'),
756
                ];
757
                continue;
758
            }
759
            $response = ApiClientProvider::getApiClient()->save(
760
                (string)Hash::get($obj, 'type'),
761
                (array)Hash::get($obj, 'attributes')
762
            );
763
            $relatedObjects[] = [
764
                'id' => Hash::get($response, 'data.id'),
765
                'type' => Hash::get($response, 'data.type'),
766
                'meta' => (array)Hash::get($response, 'data.meta'),
767
            ];
768
        }
769
770
        return $relatedObjects;
771
    }
772
}
773