| Total Complexity | 74 | 
| Total Lines | 649 | 
| Duplicated Lines | 0 % | 
| Changes | 2 | ||
| Bugs | 2 | Features | 1 | 
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  | 
            ||
| 39 | class ModulesComponent extends Component  | 
            ||
| 40 | { | 
            ||
| 41 | use SchemaTrait;  | 
            ||
| 42 | |||
| 43 | /**  | 
            ||
| 44 | * Fixed relationships to be loaded for each object  | 
            ||
| 45 | *  | 
            ||
| 46 | * @var array  | 
            ||
| 47 | */  | 
            ||
| 48 | public const FIXED_RELATIONSHIPS = [  | 
            ||
| 49 | 'parent',  | 
            ||
| 50 | 'children',  | 
            ||
| 51 | 'parents',  | 
            ||
| 52 | 'translations',  | 
            ||
| 53 | 'streams',  | 
            ||
| 54 | 'roles',  | 
            ||
| 55 | ];  | 
            ||
| 56 | |||
| 57 | /**  | 
            ||
| 58 | * @inheritDoc  | 
            ||
| 59 | */  | 
            ||
| 60 | public $components = ['Authentication', 'Children', 'Config', 'Parents', 'Schema'];  | 
            ||
| 61 | |||
| 62 | /**  | 
            ||
| 63 | * @inheritDoc  | 
            ||
| 64 | */  | 
            ||
| 65 | protected $_defaultConfig = [  | 
            ||
| 66 | 'currentModuleName' => null,  | 
            ||
| 67 | 'clearHomeCache' => false,  | 
            ||
| 68 | ];  | 
            ||
| 69 | |||
| 70 | /**  | 
            ||
| 71 | * Project modules for a user from `/home` endpoint  | 
            ||
| 72 | *  | 
            ||
| 73 | * @var array  | 
            ||
| 74 | */  | 
            ||
| 75 | protected $modules = [];  | 
            ||
| 76 | |||
| 77 | /**  | 
            ||
| 78 | * Other "logic" modules, non objects  | 
            ||
| 79 | *  | 
            ||
| 80 | * @var array  | 
            ||
| 81 | */  | 
            ||
| 82 | protected $otherModules = [  | 
            ||
| 83 | 'tags' => [  | 
            ||
| 84 | 'name' => 'tags',  | 
            ||
| 85 | 'hints' => ['allow' => ['GET', 'POST', 'PATCH', 'DELETE']],  | 
            ||
| 86 | ],  | 
            ||
| 87 | ];  | 
            ||
| 88 | |||
| 89 | /**  | 
            ||
| 90 | * @inheritDoc  | 
            ||
| 91 | */  | 
            ||
| 92 | public function beforeFilter(EventInterface $event): ?Response  | 
            ||
| 93 |     { | 
            ||
| 94 | /** @var \Authentication\Identity|null $user */  | 
            ||
| 95 | $user = $this->Authentication->getIdentity();  | 
            ||
| 96 |         if (!empty($user)) { | 
            ||
| 97 |             $this->getController()->set('modules', $this->getModules()); | 
            ||
| 98 | }  | 
            ||
| 99 | |||
| 100 | return null;  | 
            ||
| 101 | }  | 
            ||
| 102 | |||
| 103 | /**  | 
            ||
| 104 | * Read modules and project info from `/home' endpoint.  | 
            ||
| 105 | *  | 
            ||
| 106 | * @return void  | 
            ||
| 107 | */  | 
            ||
| 108 | public function startup(): void  | 
            ||
| 109 |     { | 
            ||
| 110 | /** @var \Authentication\Identity|null $user */  | 
            ||
| 111 | $user = $this->Authentication->getIdentity();  | 
            ||
| 112 |         if (empty($user) || !$user->get('id')) { | 
            ||
| 113 | $this->getController()->set(['modules' => [], 'project' => []]);  | 
            ||
| 114 | |||
| 115 | return;  | 
            ||
| 116 | }  | 
            ||
| 117 | |||
| 118 |         if ($this->getConfig('clearHomeCache')) { | 
            ||
| 119 |             Cache::delete(sprintf('home_%d', $user->get('id'))); | 
            ||
| 120 | }  | 
            ||
| 121 | |||
| 122 | $project = $this->getProject();  | 
            ||
| 123 | $uploadable = (array)Hash::get($this->Schema->objectTypesFeatures(), 'uploadable');  | 
            ||
| 124 |         $this->getController()->set(compact('project', 'uploadable')); | 
            ||
| 125 | |||
| 126 |         $currentModuleName = $this->getConfig('currentModuleName'); | 
            ||
| 127 |         $modules = (array)$this->getController()->viewBuilder()->getVar('modules'); | 
            ||
| 128 |         if (!empty($currentModuleName)) { | 
            ||
| 129 | $currentModule = Hash::get($modules, $currentModuleName);  | 
            ||
| 130 | }  | 
            ||
| 131 | |||
| 132 |         if (!empty($currentModule)) { | 
            ||
| 133 |             $this->getController()->set(compact('currentModule')); | 
            ||
| 134 | }  | 
            ||
| 135 | }  | 
            ||
| 136 | |||
| 137 | /**  | 
            ||
| 138 | * Create internal list of available modules in `$this->modules` as an array with `name` as key  | 
            ||
| 139 | * and return it.  | 
            ||
| 140 | * Modules are created from configuration and merged with information read from `/home` endpoint  | 
            ||
| 141 | *  | 
            ||
| 142 | * @return array  | 
            ||
| 143 | */  | 
            ||
| 144 | public function getModules(): array  | 
            ||
| 169 | }  | 
            ||
| 170 | |||
| 171 | /**  | 
            ||
| 172 | * This filters modules and apply 'AccessControl' config by user role, if any.  | 
            ||
| 173 | * Module can be "hidden": remove from modules.  | 
            ||
| 174 | * Module can be "readonly": adjust "hints.allow" for module.  | 
            ||
| 175 | *  | 
            ||
| 176 | * @return void  | 
            ||
| 177 | */  | 
            ||
| 178 | protected function modulesByAccessControl(): void  | 
            ||
| 179 |     { | 
            ||
| 180 |         $accessControl = (array)Configure::read('AccessControl'); | 
            ||
| 181 |         if (empty($accessControl)) { | 
            ||
| 182 | return;  | 
            ||
| 183 | }  | 
            ||
| 184 | /** @var \Authentication\Identity|null $user */  | 
            ||
| 185 | $user = $this->Authentication->getIdentity();  | 
            ||
| 186 |         if (empty($user) || empty($user->getOriginalData())) { | 
            ||
| 187 | return;  | 
            ||
| 188 | }  | 
            ||
| 189 |         $roles = array_intersect(array_keys($accessControl), (array)$user->get('roles')); | 
            ||
| 190 | $modules = (array)array_keys($this->modules);  | 
            ||
| 191 | $hidden = [];  | 
            ||
| 192 | $readonly = [];  | 
            ||
| 193 | $write = [];  | 
            ||
| 194 |         foreach ($roles as $role) { | 
            ||
| 195 |             $h = (array)Hash::get($accessControl, sprintf('%s.hidden', $role)); | 
            ||
| 196 | $hidden = empty($hidden) ? $h : array_intersect($hidden, $h);  | 
            ||
| 197 |             $r = (array)Hash::get($accessControl, sprintf('%s.readonly', $role)); | 
            ||
| 198 | $readonly = empty($readonly) ? $r : array_intersect($readonly, $r);  | 
            ||
| 199 | $write = array_unique(array_merge($write, array_diff($modules, $hidden, $readonly)));  | 
            ||
| 200 | }  | 
            ||
| 201 | // Note: https://github.com/bedita/manager/issues/969 Accesses priority is "write" > "read" > "hidden"  | 
            ||
| 202 | $readonly = array_diff($readonly, $write);  | 
            ||
| 203 | $hidden = array_diff($hidden, $readonly, $write);  | 
            ||
| 204 |         if (empty($hidden) && empty($readonly)) { | 
            ||
| 205 | return;  | 
            ||
| 206 | }  | 
            ||
| 207 | // remove "hidden"  | 
            ||
| 208 | $this->modules = array_diff_key($this->modules, array_flip($hidden));  | 
            ||
| 209 | // make sure $readonly contains valid module names  | 
            ||
| 210 | $readonly = array_intersect($readonly, array_keys($this->modules));  | 
            ||
| 211 |         foreach ($readonly as $key) { | 
            ||
| 212 |             $path = sprintf('%s.hints.allow', $key); | 
            ||
| 213 | $allow = (array)Hash::get($this->modules, $path);  | 
            ||
| 214 | $this->modules[$key]['hints']['allow'] = array_diff($allow, ['POST', 'PATCH', 'DELETE']);  | 
            ||
| 215 | }  | 
            ||
| 216 | }  | 
            ||
| 217 | |||
| 218 | /**  | 
            ||
| 219 | * Modules data from `/home` endpoint 'meta' response.  | 
            ||
| 220 | * Modules are object endpoints from BE4 API  | 
            ||
| 221 | *  | 
            ||
| 222 | * @return array  | 
            ||
| 223 | */  | 
            ||
| 224 | protected function modulesFromMeta(): array  | 
            ||
| 225 |     { | 
            ||
| 226 | /** @var \Authentication\Identity $user */  | 
            ||
| 227 | $user = $this->Authentication->getIdentity();  | 
            ||
| 228 | $meta = $this->getMeta($user);  | 
            ||
| 229 | $modules = collection(Hash::get($meta, 'resources', []))  | 
            ||
| 230 |             ->map(function (array $data, $endpoint) { | 
            ||
| 231 | $name = substr($endpoint, 1);  | 
            ||
| 232 | |||
| 233 |                 return $data + compact('name'); | 
            ||
| 234 | })  | 
            ||
| 235 |             ->reject(function (array $data) { | 
            ||
| 236 | return Hash::get($data, 'hints.object_type') !== true && !in_array(Hash::get($data, 'name'), ['trash', 'translations']);  | 
            ||
| 237 | })  | 
            ||
| 238 | ->toList();  | 
            ||
| 239 | |||
| 240 |         return Hash::combine($modules, '{n}.name', '{n}'); | 
            ||
| 241 | }  | 
            ||
| 242 | |||
| 243 | /**  | 
            ||
| 244 | * Get information about current project.  | 
            ||
| 245 | *  | 
            ||
| 246 | * @return array  | 
            ||
| 247 | */  | 
            ||
| 248 | public function getProject(): array  | 
            ||
| 249 |     { | 
            ||
| 250 | /** @var \Authentication\Identity $user */  | 
            ||
| 251 | $user = $this->Authentication->getIdentity();  | 
            ||
| 252 | $meta = $this->getMeta($user);  | 
            ||
| 253 |         $project = (array)Configure::read('Project'); | 
            ||
| 254 | $name = (string)Hash::get($project, 'name', Hash::get($meta, 'project.name'));  | 
            ||
| 255 | $version = Hash::get($meta, 'version', '');  | 
            ||
| 256 | |||
| 257 |         return compact('name', 'version'); | 
            ||
| 258 | }  | 
            ||
| 259 | |||
| 260 | /**  | 
            ||
| 261 | * Check if an object type is abstract or concrete.  | 
            ||
| 262 | * This method MUST NOT be called from `beforeRender` since `$this->modules` array is still not initialized.  | 
            ||
| 263 | *  | 
            ||
| 264 | * @param string $name Name of object type.  | 
            ||
| 265 | * @return bool True if abstract, false if concrete  | 
            ||
| 266 | */  | 
            ||
| 267 | public function isAbstract(string $name): bool  | 
            ||
| 268 |     { | 
            ||
| 269 |         return (bool)Hash::get($this->modules, sprintf('%s.hints.multiple_types', $name), false); | 
            ||
| 270 | }  | 
            ||
| 271 | |||
| 272 | /**  | 
            ||
| 273 | * Get list of object types  | 
            ||
| 274 | * This method MUST NOT be called from `beforeRender` since `$this->modules` array is still not initialized.  | 
            ||
| 275 | *  | 
            ||
| 276 | * @param bool|null $abstract Only abstract or concrete types.  | 
            ||
| 277 | * @return array Type names list  | 
            ||
| 278 | */  | 
            ||
| 279 | public function objectTypes(?bool $abstract = null): array  | 
            ||
| 280 |     { | 
            ||
| 281 | $types = [];  | 
            ||
| 282 |         foreach ($this->modules as $name => $data) { | 
            ||
| 283 |             if (empty($data['hints']['object_type'])) { | 
            ||
| 284 | continue;  | 
            ||
| 285 | }  | 
            ||
| 286 |             if ($abstract === null || $data['hints']['multiple_types'] === $abstract) { | 
            ||
| 287 | $types[] = $name;  | 
            ||
| 288 | }  | 
            ||
| 289 | }  | 
            ||
| 290 | |||
| 291 | return $types;  | 
            ||
| 292 | }  | 
            ||
| 293 | |||
| 294 | /**  | 
            ||
| 295 | * Read oEmbed metadata  | 
            ||
| 296 | *  | 
            ||
| 297 | * @param string $url Remote URL  | 
            ||
| 298 | * @return array|null  | 
            ||
| 299 | * @codeCoverageIgnore  | 
            ||
| 300 | */  | 
            ||
| 301 | protected function oEmbedMeta(string $url): ?array  | 
            ||
| 302 |     { | 
            ||
| 303 | return (new OEmbed())->readMetadata($url);  | 
            ||
| 304 | }  | 
            ||
| 305 | |||
| 306 | /**  | 
            ||
| 307 | * Upload a file and store it in a media stream  | 
            ||
| 308 | * Or create a remote media trying to get some metadata via oEmbed  | 
            ||
| 309 | *  | 
            ||
| 310 | * @param array $requestData The request data from form  | 
            ||
| 311 | * @return void  | 
            ||
| 312 | */  | 
            ||
| 313 | public function upload(array &$requestData): void  | 
            ||
| 314 |     { | 
            ||
| 315 | $uploadBehavior = Hash::get($requestData, 'upload_behavior', 'file');  | 
            ||
| 316 | |||
| 317 |         if ($uploadBehavior === 'embed' && !empty($requestData['remote_url'])) { | 
            ||
| 318 | $data = $this->oEmbedMeta($requestData['remote_url']);  | 
            ||
| 319 | $requestData = array_filter($requestData) + $data;  | 
            ||
| 320 | |||
| 321 | return;  | 
            ||
| 322 | }  | 
            ||
| 323 |         if (empty($requestData['file'])) { | 
            ||
| 324 | return;  | 
            ||
| 325 | }  | 
            ||
| 326 | |||
| 327 | // verify upload form data  | 
            ||
| 328 |         if ($this->checkRequestForUpload($requestData)) { | 
            ||
| 329 | // has another stream? drop it  | 
            ||
| 330 | $this->removeStream($requestData);  | 
            ||
| 331 | |||
| 332 | /** @var \Laminas\Diactoros\UploadedFile $file */  | 
            ||
| 333 | $file = $requestData['file'];  | 
            ||
| 334 | |||
| 335 | // upload file  | 
            ||
| 336 | $filename = basename($file->getClientFileName());  | 
            ||
| 337 |             $filepath = $file->getStream()->getMetadata('uri'); | 
            ||
| 338 | $headers = ['Content-Type' => $file->getClientMediaType()];  | 
            ||
| 339 | $apiClient = ApiClientProvider::getApiClient();  | 
            ||
| 340 | $response = $apiClient->upload($filename, $filepath, $headers);  | 
            ||
| 341 | |||
| 342 | // assoc stream to media  | 
            ||
| 343 | $streamId = $response['data']['id'];  | 
            ||
| 344 | $requestData['id'] = $this->assocStreamToMedia($streamId, $requestData, $filename);  | 
            ||
| 345 | }  | 
            ||
| 346 | unset($requestData['file'], $requestData['remote_url']);  | 
            ||
| 347 | }  | 
            ||
| 348 | |||
| 349 | /**  | 
            ||
| 350 | * Remove a stream from a media, if any  | 
            ||
| 351 | *  | 
            ||
| 352 | * @param array $requestData The request data from form  | 
            ||
| 353 | * @return bool  | 
            ||
| 354 | */  | 
            ||
| 355 | public function removeStream(array $requestData): bool  | 
            ||
| 370 | }  | 
            ||
| 371 | |||
| 372 | /**  | 
            ||
| 373 | * Associate a stream to a media using API  | 
            ||
| 374 | * If $requestData['id'] is null, create media from stream.  | 
            ||
| 375 | * If $requestData['id'] is not null, replace properly related stream.  | 
            ||
| 376 | *  | 
            ||
| 377 | * @param string $streamId The stream ID  | 
            ||
| 378 | * @param array $requestData The request data  | 
            ||
| 379 | * @param string $defaultTitle The default title for media  | 
            ||
| 380 | * @return string The media ID  | 
            ||
| 381 | */  | 
            ||
| 382 | public function assocStreamToMedia(string $streamId, array &$requestData, string $defaultTitle): string  | 
            ||
| 383 |     { | 
            ||
| 384 | $apiClient = ApiClientProvider::getApiClient();  | 
            ||
| 385 | $type = $requestData['model-type'];  | 
            ||
| 386 |         if (empty($requestData['id'])) { | 
            ||
| 387 | // create media from stream  | 
            ||
| 388 | // save only `title` (filename if not set) and `status` in new media object  | 
            ||
| 389 | $attributes = array_filter([  | 
            ||
| 390 | 'title' => !empty($requestData['title']) ? $requestData['title'] : $defaultTitle,  | 
            ||
| 391 | 'status' => Hash::get($requestData, 'status'),  | 
            ||
| 392 | ]);  | 
            ||
| 393 |             $data = compact('type', 'attributes'); | 
            ||
| 394 |             $body = compact('data'); | 
            ||
| 395 | $response = $apiClient->createMediaFromStream($streamId, $type, $body);  | 
            ||
| 396 | // `title` and `status` saved here, remove from next save  | 
            ||
| 397 | unset($requestData['title'], $requestData['status']);  | 
            ||
| 398 | |||
| 399 | return (string)Hash::get($response, 'data.id');  | 
            ||
| 400 | }  | 
            ||
| 401 | |||
| 402 | // assoc existing media to stream  | 
            ||
| 403 | $id = (string)Hash::get($requestData, 'id');  | 
            ||
| 404 |         $data = compact('id', 'type'); | 
            ||
| 405 | $apiClient->replaceRelated($streamId, 'streams', 'object', $data);  | 
            ||
| 406 | |||
| 407 | return $id;  | 
            ||
| 408 | }  | 
            ||
| 409 | |||
| 410 | /**  | 
            ||
| 411 | * Check request data for upload and return true if upload is boht possible and needed  | 
            ||
| 412 | *  | 
            ||
| 413 | * @param array $requestData The request data  | 
            ||
| 414 | * @return bool true if upload is possible and needed  | 
            ||
| 415 | */  | 
            ||
| 416 | public function checkRequestForUpload(array $requestData): bool  | 
            ||
| 447 | }  | 
            ||
| 448 | |||
| 449 | /**  | 
            ||
| 450 | * Check if save can be skipped.  | 
            ||
| 451 | * This is used to avoid saving object with no changes.  | 
            ||
| 452 | *  | 
            ||
| 453 | * @param string $id The object ID  | 
            ||
| 454 | * @param array $requestData The request data  | 
            ||
| 455 | * @param array $relatedData The related data  | 
            ||
| 456 | * @return bool True if save can be skipped, false otherwise  | 
            ||
| 457 | */  | 
            ||
| 458 | public function skipSaveObject(string $id, array $requestData, array $relatedData): bool  | 
            ||
| 459 |     { | 
            ||
| 460 |         if (empty($id) || !empty($relatedData)) { | 
            ||
| 461 | return false;  | 
            ||
| 462 | }  | 
            ||
| 463 |         $data = array_filter($requestData, function ($key) { | 
            ||
| 464 | return !in_array($key, ['id', 'permissions']);  | 
            ||
| 465 | }, ARRAY_FILTER_USE_KEY);  | 
            ||
| 466 | |||
| 467 | return empty($data);  | 
            ||
| 468 | }  | 
            ||
| 469 | |||
| 470 | /**  | 
            ||
| 471 | * Check if save permissions can be skipped.  | 
            ||
| 472 | * This is used to avoid saving object permissions with no changes.  | 
            ||
| 473 | *  | 
            ||
| 474 | * @param string $id The object ID  | 
            ||
| 475 | * @param array $requestPermissions The request permissions  | 
            ||
| 476 | * @return bool True if save permissions can be skipped, false otherwise  | 
            ||
| 477 | */  | 
            ||
| 478 | public function skipSavePermissions(string $id, array $requestPermissions): bool  | 
            ||
| 493 | }  | 
            ||
| 494 | |||
| 495 | /**  | 
            ||
| 496 | * Set current attributes from loaded $object data in `currentAttributes`.  | 
            ||
| 497 | *  | 
            ||
| 498 | * @param array $object The object.  | 
            ||
| 499 | * @return void  | 
            ||
| 500 | */  | 
            ||
| 501 | public function setupAttributes(array &$object): void  | 
            ||
| 502 |     { | 
            ||
| 503 | $currentAttributes = json_encode((array)Hash::get($object, 'attributes'));  | 
            ||
| 504 |         $this->getController()->set(compact('currentAttributes')); | 
            ||
| 505 | }  | 
            ||
| 506 | |||
| 507 | /**  | 
            ||
| 508 | * Setup relations information metadata.  | 
            ||
| 509 | *  | 
            ||
| 510 | * @param array $schema Relations schema.  | 
            ||
| 511 | * @param array $relationships Object relationships.  | 
            ||
| 512 | * @param array $order Ordered names inside 'main' and 'aside' keys.  | 
            ||
| 513 | * @param array $hidden List of hidden relations.  | 
            ||
| 514 | * @param array $readonly List of readonly relations.  | 
            ||
| 515 | * @return void  | 
            ||
| 516 | */  | 
            ||
| 517 | public function setupRelationsMeta(array $schema, array $relationships, array $order = [], array $hidden = [], array $readonly = []): void  | 
            ||
| 518 |     { | 
            ||
| 519 | // relations between objects  | 
            ||
| 520 | $relationsSchema = $this->relationsSchema($schema, $relationships, $hidden, $readonly);  | 
            ||
| 521 | // relations between objects and resources  | 
            ||
| 522 | $resourceRelations = array_diff(array_keys($relationships), array_keys($relationsSchema), $hidden, self::FIXED_RELATIONSHIPS);  | 
            ||
| 523 | // set objectRelations array with name as key and label as value  | 
            ||
| 524 | $relationNames = array_keys($relationsSchema);  | 
            ||
| 525 | |||
| 526 | // define 'main' and 'aside' relation groups  | 
            ||
| 527 | $aside = array_intersect((array)Hash::get($order, 'aside'), $relationNames);  | 
            ||
| 528 | $relationNames = array_diff($relationNames, $aside);  | 
            ||
| 529 | $main = array_intersect((array)Hash::get($order, 'main'), $relationNames);  | 
            ||
| 530 | $main = array_unique(array_merge($main, $relationNames));  | 
            ||
| 531 | |||
| 532 | $objectRelations = [  | 
            ||
| 533 | 'main' => $this->relationLabels($relationsSchema, $main),  | 
            ||
| 534 | 'aside' => $this->relationLabels($relationsSchema, $aside),  | 
            ||
| 535 | ];  | 
            ||
| 536 | |||
| 537 |         $this->getController()->set(compact('relationsSchema', 'resourceRelations', 'objectRelations')); | 
            ||
| 538 | }  | 
            ||
| 539 | |||
| 540 | /**  | 
            ||
| 541 | * Relations schema by schema and relationships.  | 
            ||
| 542 | *  | 
            ||
| 543 | * @param array $schema The schema  | 
            ||
| 544 | * @param array $relationships The relationships  | 
            ||
| 545 | * @param array $hidden Hidden relationships  | 
            ||
| 546 | * @param array $readonly Readonly relationships  | 
            ||
| 547 | * @return array  | 
            ||
| 548 | */  | 
            ||
| 549 | protected function relationsSchema(array $schema, array $relationships, array $hidden = [], array $readonly = []): array  | 
            ||
| 550 |     { | 
            ||
| 551 | $types = $this->objectTypes(false);  | 
            ||
| 552 | sort($types);  | 
            ||
| 553 | $relationsSchema = array_diff_key(array_intersect_key($schema, $relationships), array_flip($hidden));  | 
            ||
| 554 | |||
| 555 |         foreach ($relationsSchema as $relName => &$relSchema) { | 
            ||
| 556 |             if (in_array('objects', (array)Hash::get($relSchema, 'right'))) { | 
            ||
| 557 | $relSchema['right'] = $types;  | 
            ||
| 558 | }  | 
            ||
| 559 |             if (!empty($relationships[$relName]['readonly']) || in_array($relName, $readonly)) { | 
            ||
| 560 | $relSchema['readonly'] = true;  | 
            ||
| 561 | }  | 
            ||
| 562 | }  | 
            ||
| 563 | |||
| 564 | return $relationsSchema;  | 
            ||
| 565 | }  | 
            ||
| 566 | |||
| 567 | /**  | 
            ||
| 568 | * Retrieve associative array with names as keys and labels as values.  | 
            ||
| 569 | *  | 
            ||
| 570 | * @param array $relationsSchema Relations schema.  | 
            ||
| 571 | * @param array $names Relation names.  | 
            ||
| 572 | * @return array  | 
            ||
| 573 | */  | 
            ||
| 574 | protected function relationLabels(array &$relationsSchema, array $names): array  | 
            ||
| 575 |     { | 
            ||
| 576 | return (array)array_combine(  | 
            ||
| 577 | $names,  | 
            ||
| 578 | array_map(  | 
            ||
| 579 |                 function ($r) use ($relationsSchema) { | 
            ||
| 580 | // return 'label' or 'inverse_label' looking at 'name'  | 
            ||
| 581 | $attributes = $relationsSchema[$r]['attributes'];  | 
            ||
| 582 |                     if ($r === $attributes['name']) { | 
            ||
| 583 | return $attributes['label'];  | 
            ||
| 584 | }  | 
            ||
| 585 | |||
| 586 | return $attributes['inverse_label'];  | 
            ||
| 587 | },  | 
            ||
| 588 | $names  | 
            ||
| 589 | )  | 
            ||
| 590 | );  | 
            ||
| 591 | }  | 
            ||
| 592 | |||
| 593 | /**  | 
            ||
| 594 | * Get related types from relation name.  | 
            ||
| 595 | *  | 
            ||
| 596 | * @param array $schema Relations schema.  | 
            ||
| 597 | * @param string $relation Relation name.  | 
            ||
| 598 | * @return array  | 
            ||
| 599 | */  | 
            ||
| 600 | public function relatedTypes(array $schema, string $relation): array  | 
            ||
| 601 |     { | 
            ||
| 602 | $relationsSchema = (array)Hash::get($schema, $relation);  | 
            ||
| 603 | |||
| 604 | return (array)Hash::get($relationsSchema, 'right');  | 
            ||
| 605 | }  | 
            ||
| 606 | |||
| 607 | /**  | 
            ||
| 608 | * Save related objects.  | 
            ||
| 609 | *  | 
            ||
| 610 | * @param string $id Object ID  | 
            ||
| 611 | * @param string $type Object type  | 
            ||
| 612 | * @param array $relatedData Related objects data  | 
            ||
| 613 | * @return void  | 
            ||
| 614 | */  | 
            ||
| 615 | public function saveRelated(string $id, string $type, array $relatedData): void  | 
            ||
| 616 |     { | 
            ||
| 617 |         foreach ($relatedData as $data) { | 
            ||
| 618 | $this->saveRelatedObjects($id, $type, $data);  | 
            ||
| 619 |             $event = new Event('Controller.afterSaveRelated', $this, compact('id', 'type', 'data')); | 
            ||
| 620 | $this->getController()->getEventManager()->dispatch($event);  | 
            ||
| 621 | }  | 
            ||
| 622 | }  | 
            ||
| 623 | |||
| 624 | /**  | 
            ||
| 625 | * Save related objects per object by ID.  | 
            ||
| 626 | *  | 
            ||
| 627 | * @param string $id Object ID  | 
            ||
| 628 | * @param string $type Object type  | 
            ||
| 629 | * @param array $data Related object data  | 
            ||
| 630 | * @return array|null  | 
            ||
| 631 | * @throws \Cake\Http\Exception\BadRequestException  | 
            ||
| 632 | */  | 
            ||
| 633 | public function saveRelatedObjects(string $id, string $type, array $data): ?array  | 
            ||
| 634 |     { | 
            ||
| 635 | $method = (string)Hash::get($data, 'method');  | 
            ||
| 636 |         if (!in_array($method, ['addRelated', 'removeRelated', 'replaceRelated'])) { | 
            ||
| 637 |             throw new BadRequestException(__('Bad related data method')); | 
            ||
| 638 | }  | 
            ||
| 639 | $relation = (string)Hash::get($data, 'relation');  | 
            ||
| 640 | $related = $this->getRelated($data);  | 
            ||
| 641 |         if ($relation === 'parent' && $type === 'folders') { | 
            ||
| 642 |             return $this->Parents->{$method}($id, $related); | 
            ||
| 643 | }  | 
            ||
| 644 |         if ($relation === 'children' && $type === 'folders') { | 
            ||
| 645 |             return $this->Children->{$method}($id, $related); | 
            ||
| 646 | }  | 
            ||
| 647 | $lang = I18n::getLocale();  | 
            ||
| 648 | $headers = ['Accept-Language' => $lang];  | 
            ||
| 649 | |||
| 650 |         return ApiClientProvider::getApiClient()->{$method}($id, $type, $relation, $related, $headers); | 
            ||
| 651 | }  | 
            ||
| 652 | |||
| 653 | /**  | 
            ||
| 654 | * Get related objects.  | 
            ||
| 655 | * If related object has no ID, it will be created.  | 
            ||
| 656 | *  | 
            ||
| 657 | * @param array $data Related object data  | 
            ||
| 658 | * @return array  | 
            ||
| 659 | */  | 
            ||
| 660 | public function getRelated(array $data): array  | 
            ||
| 688 | }  | 
            ||
| 689 | }  | 
            ||
| 690 |