Passed
Pull Request — master (#652)
by Edoardo
03:00
created

ModulesController   B

Complexity

Total Complexity 45

Size/Duplication

Total Lines 564
Duplicated Lines 0 %

Importance

Changes 8
Bugs 6 Features 0
Metric Value
wmc 45
eloc 266
dl 0
loc 564
rs 8.8
c 8
b 6
f 0

17 Methods

Rating   Name   Duplication   Size   Complexity  
A beforeRender() 0 5 1
A index() 0 46 3
A view() 0 53 3
A uname() 0 18 3
A create() 0 36 2
B delete() 0 29 6
A related() 0 27 3
A clone() 0 31 3
A getSchemaForIndex() 0 14 4
A resources() 0 17 2
A relationships() 0 23 2
A availableRelationshipsUrl() 0 19 3
A initialize() 0 20 2
A save() 0 38 3
A saveCategory() 0 14 2
A listCategories() 0 19 1
A removeCategory() 0 13 2

How to fix   Complexity   

Complex Class

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

1
<?php
2
/**
3
 * BEdita, API-first content management framework
4
 * Copyright 2018 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;
14
15
use BEdita\SDK\BEditaClientException;
16
use Cake\Event\Event;
17
use Cake\Http\Response;
18
use Cake\Utility\Hash;
19
use Psr\Log\LogLevel;
20
21
/**
22
 * Modules controller: list, add, edit, remove objects
23
 *
24
 * @property \App\Controller\Component\CategoriesComponent $Categories
25
 * @property \App\Controller\Component\HistoryComponent $History
26
 * @property \App\Controller\Component\ObjectsEditorsComponent $ObjectsEditors
27
 * @property \App\Controller\Component\ProjectConfigurationComponent $ProjectConfiguration
28
 * @property \App\Controller\Component\PropertiesComponent $Properties
29
 * @property \App\Controller\Component\QueryComponent $Query
30
 * @property \App\Controller\Component\ThumbsComponent $Thumbs
31
 * @property \BEdita\WebTools\Controller\Component\ApiFormatterComponent $ApiFormatter
32
 */
33
class ModulesController extends AppController
34
{
35
    /**
36
     * Object type currently used
37
     *
38
     * @var string
39
     */
40
    protected $objectType = null;
41
42
    /**
43
     * {@inheritDoc}
44
     */
45
    public function initialize(): void
46
    {
47
        parent::initialize();
48
49
        $this->loadComponent('Categories');
50
        $this->loadComponent('History');
51
        $this->loadComponent('ObjectsEditors');
52
        $this->loadComponent('Properties');
53
        $this->loadComponent('ProjectConfiguration');
54
        $this->loadComponent('Query');
55
        $this->loadComponent('Thumbs');
56
        $this->loadComponent('BEdita/WebTools.ApiFormatter');
57
58
        if (!empty($this->request)) {
59
            $this->objectType = $this->request->getParam('object_type');
60
            $this->Modules->setConfig('currentModuleName', $this->objectType);
61
            $this->Schema->setConfig('type', $this->objectType);
62
        }
63
64
        $this->Security->setConfig('unlockedActions', ['save']);
65
    }
66
67
    /**
68
     * {@inheritDoc}
69
     * @codeCoverageIgnore
70
     */
71
    public function beforeRender(Event $event): ?Response
72
    {
73
        $this->set('objectType', $this->objectType);
74
75
        return parent::beforeRender($event);
0 ignored issues
show
Bug introduced by
Are you sure the usage of parent::beforeRender($event) targeting App\Controller\AppController::beforeRender() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
76
    }
77
78
    /**
79
     * Display resources list.
80
     *
81
     * @return \Cake\Http\Response|null
82
     */
83
    public function index(): ?Response
84
    {
85
        $this->request->allowMethod(['get']);
86
87
        // handle filter and query parameters using session
88
        $result = $this->applySessionFilter();
89
        if ($result != null) {
90
            return $result;
91
        }
92
93
        try {
94
            $response = $this->apiClient->getObjects($this->objectType, $this->Query->index());
95
        } catch (BEditaClientException $e) {
96
            $this->log($e, LogLevel::ERROR);
97
            $this->Flash->error($e->getMessage(), ['params' => $e]);
98
            // remove session filter to avoid error repetition
99
            $session = $this->request->getSession();
100
            $session->delete(sprintf('%s.filter', $this->Modules->getConfig('currentModuleName')));
101
102
            return $this->redirect(['_name' => 'dashboard']);
103
        }
104
105
        $this->ProjectConfiguration->read();
106
107
        $response = $this->ApiFormatter->embedIncluded((array)$response);
108
        $objects = (array)Hash::get($response, 'data');
109
        $this->set('objects', $objects);
110
        $this->set('meta', (array)Hash::get($response, 'meta'));
111
        $this->set('links', (array)Hash::get($response, 'links'));
112
        $this->set('types', ['right' => $this->Schema->descendants($this->objectType)]);
113
114
        $this->set('properties', $this->Properties->indexList($this->objectType));
115
116
        // base/custom filters for filter view
117
        $this->set('filter', $this->Properties->filterList($this->objectType));
118
119
        // base/custom bulk actions for index view
120
        $this->set('bulkActions', $this->Properties->bulkList($this->objectType));
121
122
        // objectTypes schema
123
        $this->set('schema', $this->getSchemaForIndex($this->objectType));
124
125
        // set prevNext for views navigations
126
        $this->setObjectNav($objects);
127
128
        return null;
129
    }
130
131
    /**
132
     * View single resource.
133
     *
134
     * @param string|int $id Resource ID.
135
     * @return \Cake\Http\Response|null
136
     */
137
    public function view($id): ?Response
138
    {
139
        $this->request->allowMethod(['get']);
140
141
        try {
142
            $query = ['count' => 'all'];
143
            $response = $this->apiClient->getObject($id, $this->objectType, $query);
144
        } catch (BEditaClientException $e) {
145
            // Error! Back to index.
146
            $this->log($e, LogLevel::ERROR);
147
            $this->Flash->error(__('Error retrieving the requested content'), ['params' => $e]);
148
149
            return $this->redirect(['_name' => 'modules:list', 'object_type' => $this->objectType]);
150
        }
151
        $this->ProjectConfiguration->read();
152
153
        $revision = Hash::get($response, 'meta.schema.' . $this->objectType . '.revision', null);
154
        $schema = $this->Schema->getSchema($this->objectType, $revision);
155
156
        $object = $response['data'];
157
158
        // setup `currentAttributes` and recover failure data from session.
159
        $this->Modules->setupAttributes($object);
160
161
        $included = (!empty($response['included'])) ? $response['included'] : [];
162
        $typeIncluded = (array)Hash::combine($included, '{n}.id', '{n}', '{n}.type');
163
        $streams = Hash::get($typeIncluded, 'streams');
164
        $this->History->load($id, $object);
165
        $this->set(compact('object', 'included', 'schema', 'streams'));
166
        $this->set('properties', $this->Properties->viewGroups($object, $this->objectType));
167
168
        // setup relations metadata
169
        $this->Modules->setupRelationsMeta(
170
            $this->Schema->getRelationsSchema(),
171
            $object['relationships'],
172
            $this->Properties->relationsList($this->objectType)
173
        );
174
175
        $rightTypes = \App\Utility\Schema::rightTypes($this->viewVars['relationsSchema']);
176
177
        // set schemas for relations right types
178
        $schemasByType = $this->Schema->getSchemasByType($rightTypes);
179
        $this->set('schemasByType', $schemasByType);
180
181
        $this->set('filtersByType', $this->Properties->filtersByType($rightTypes));
182
183
        // set objectNav
184
        $objectNav = $this->getObjectNav((string)$id);
185
        $this->set('objectNav', $objectNav);
186
187
        $this->ObjectsEditors->update((string)$id);
188
189
        return null;
190
    }
191
192
    /**
193
     * View single resource by id, doing a proper redirect (302) to resource module view by type.
194
     * If no resource found by ID, redirect to referer.
195
     *
196
     * @param string|int $id Resource ID.
197
     * @return \Cake\Http\Response|null
198
     */
199
    public function uname($id): ?Response
200
    {
201
        try {
202
            $response = $this->apiClient->get(sprintf('/objects/%s', $id));
203
        } catch (BEditaClientException $e) {
204
            $msg = $e->getMessage();
205
            $error = $e->getCode() === 404 ?
206
                sprintf(__('Resource "%s" not found', true), $id) :
0 ignored issues
show
Bug introduced by
It seems like __('Resource "%s" not found', true) can also be of type null; however, parameter $format of sprintf() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

206
                sprintf(/** @scrutinizer ignore-type */ __('Resource "%s" not found', true), $id) :
Loading history...
207
                sprintf(__('Resource "%s" not available. Error: %s', true), $id, $msg);
208
            $this->Flash->error($error);
209
210
            return $this->redirect($this->referer());
211
        }
212
        $_name = 'modules:view';
213
        $object_type = $response['data']['type'];
214
        $id = $response['data']['id'];
215
216
        return $this->redirect(compact('_name', 'object_type', 'id'));
217
    }
218
219
    /**
220
     * Display new resource form.
221
     *
222
     * @return \Cake\Http\Response|null
223
     */
224
    public function create(): ?Response
225
    {
226
        $this->viewBuilder()->setTemplate('view');
227
228
        // Create stub object with empty `attributes`.
229
        $schema = $this->Schema->getSchema();
230
        if (!is_array($schema)) {
231
            $this->Flash->error(__('Cannot create abstract objects or objects without schema'));
232
233
            return $this->redirect(['_name' => 'modules:list', 'object_type' => $this->objectType]);
234
        }
235
        $attributes = array_fill_keys(
236
            array_keys(
237
                array_filter(
238
                    $schema['properties'],
239
                    function ($schema) {
240
                        return empty($schema['readOnly']);
241
                    }
242
                )
243
            ),
244
            ''
245
        );
246
        $object = [
247
            'type' => $this->objectType,
248
            'attributes' => $attributes,
249
        ];
250
251
        $this->set(compact('object', 'schema'));
252
        $this->set('properties', $this->Properties->viewGroups($object, $this->objectType));
253
        $this->ProjectConfiguration->read();
254
255
        // setup relations metadata
256
        $relationships = (array)Hash::get($schema, 'relations');
257
        $this->Modules->setupRelationsMeta($this->Schema->getRelationsSchema(), $relationships);
258
259
        return null;
260
    }
261
262
    /**
263
     * Create new object from ajax request.
264
     *
265
     * @return void
266
     */
267
    public function save(): void
268
    {
269
        $this->viewBuilder()->setClassName('Json'); // force json response
270
        $this->request->allowMethod(['post']);
271
        $requestData = $this->prepareRequest($this->objectType);
272
        unset($requestData['_csrfToken']);
273
        // extract related objects data
274
        $relatedData = (array)Hash::get($requestData, '_api');
275
        unset($requestData['_api']);
276
277
        try {
278
            // upload file (if available)
279
            $this->Modules->upload($requestData);
280
281
            // save data
282
            $response = $this->apiClient->save($this->objectType, $requestData);
283
            $objectId = (string)Hash::get($response, 'data.id');
284
            $this->Modules->saveRelated($objectId, $this->objectType, $relatedData);
285
        } catch (BEditaClientException $error) {
286
            $this->log($error->getMessage(), LogLevel::ERROR);
287
            $this->Flash->error($error->getMessage(), ['params' => $error]);
288
289
            $this->set(['error' => $error->getAttributes()]);
290
            $this->set('_serialize', ['error']);
291
292
            // set session data to recover form
293
            $this->Modules->setDataFromFailedSave($this->objectType, $requestData);
294
295
            return;
296
        }
297
        if ($response['data']) {
298
            $response['data'] = [ $response['data'] ];
299
        }
300
301
        $this->Thumbs->urls($response);
302
303
        $this->set((array)$response);
304
        $this->set('_serialize', array_keys($response));
305
    }
306
307
    /**
308
     * Clone single object.
309
     *
310
     * @param string|int $id Object ID.
311
     * @return \Cake\Http\Response|null
312
     */
313
    public function clone($id): ?Response
314
    {
315
        $this->viewBuilder()->setTemplate('view');
316
317
        $schema = $this->Schema->getSchema();
318
        if (!is_array($schema)) {
319
            $this->Flash->error(__('Cannot create abstract objects or objects without schema'));
320
321
            return $this->redirect(['_name' => 'modules:list', 'object_type' => $this->objectType]);
322
        }
323
        try {
324
            $response = $this->apiClient->getObject($id, $this->objectType);
325
            $attributes = $response['data']['attributes'];
326
            $attributes['uname'] = '';
327
            unset($attributes['relationships']);
328
            $attributes['title'] = $this->request->getQuery('title');
329
        } catch (BEditaClientException $e) {
330
            $this->log($e, LogLevel::ERROR);
331
            $this->Flash->error($e->getMessage(), ['params' => $e]);
332
333
            return $this->redirect(['_name' => 'modules:view', 'object_type' => $this->objectType, 'id' => $id]);
334
        }
335
        $object = [
336
            'type' => $this->objectType,
337
            'attributes' => $attributes,
338
        ];
339
        $this->History->load($id, $object);
340
        $this->set(compact('object', 'schema'));
341
        $this->set('properties', $this->Properties->viewGroups($object, $this->objectType));
342
343
        return null;
344
    }
345
346
    /**
347
     * Delete single resource.
348
     *
349
     * @return \Cake\Http\Response|null
350
     */
351
    public function delete(): ?Response
352
    {
353
        $this->request->allowMethod(['post']);
354
        $ids = [];
355
        if (!empty($this->request->getData('ids'))) {
356
            if (is_string($this->request->getData('ids'))) {
357
                $ids = explode(',', $this->request->getData('ids'));
0 ignored issues
show
Bug introduced by
It seems like $this->request->getData('ids') can also be of type array and null; however, parameter $string of explode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

357
                $ids = explode(',', /** @scrutinizer ignore-type */ $this->request->getData('ids'));
Loading history...
358
            }
359
        } else {
360
            $ids = [$this->request->getData('id')];
361
        }
362
        foreach ($ids as $id) {
363
            try {
364
                $this->apiClient->deleteObject($id, $this->objectType);
365
            } catch (BEditaClientException $e) {
366
                $this->log($e, LogLevel::ERROR);
367
                $this->Flash->error($e->getMessage(), ['params' => $e]);
368
                if (!empty($this->request->getData('id'))) {
369
                    return $this->redirect(['_name' => 'modules:view', 'object_type' => $this->objectType, 'id' => $this->request->getData('id')]);
370
                }
371
372
                return $this->redirect(['_name' => 'modules:view', 'object_type' => $this->objectType, 'id' => $id]);
373
            }
374
        }
375
        $this->Flash->success(__('Object(s) deleted'));
376
377
        return $this->redirect([
378
            '_name' => 'modules:list',
379
            'object_type' => $this->objectType,
380
        ]);
381
    }
382
383
    /**
384
     * Relation data load via API => `GET /:object_type/:id/related/:relation`
385
     *
386
     * @param string|int $id The object ID.
387
     * @param string $relation The relation name.
388
     * @return void
389
     */
390
    public function related($id, string $relation): void
391
    {
392
        if ($id === 'new') {
393
            $this->set('data', []);
394
            $this->set('_serialize', ['data']);
395
396
            return;
397
        }
398
399
        $this->request->allowMethod(['get']);
400
        $query = $this->Query->prepare($this->request->getQueryParams());
401
        try {
402
            $response = $this->apiClient->getRelated($id, $this->objectType, $relation, $query);
403
            $response = $this->ApiFormatter->embedIncluded((array)$response);
404
        } catch (BEditaClientException $error) {
405
            $this->log($error, LogLevel::ERROR);
406
407
            $this->set(compact('error'));
408
            $this->set('_serialize', ['error']);
409
410
            return;
411
        }
412
413
        $this->Thumbs->urls($response);
414
415
        $this->set((array)$response);
416
        $this->set('_serialize', array_keys($response));
417
    }
418
419
    /**
420
     * Load resources of $type callig api `GET /:type/`
421
     * Json response
422
     *
423
     * @param string|int $id the object identifier.
424
     * @param string $type the resource type name.
425
     * @return void
426
     */
427
    public function resources($id, string $type): void
428
    {
429
        $this->request->allowMethod(['get']);
430
        $query = $this->Query->prepare($this->request->getQueryParams());
431
        try {
432
            $response = $this->apiClient->get($type, $query);
433
        } catch (BEditaClientException $error) {
434
            $this->log($error, LogLevel::ERROR);
435
436
            $this->set(compact('error'));
437
            $this->set('_serialize', ['error']);
438
439
            return;
440
        }
441
442
        $this->set((array)$response);
443
        $this->set('_serialize', array_keys($response));
0 ignored issues
show
Bug introduced by
It seems like $response can also be of type null; however, parameter $array of array_keys() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

443
        $this->set('_serialize', array_keys(/** @scrutinizer ignore-type */ $response));
Loading history...
444
    }
445
446
    /**
447
     * Relation data load callig api `GET /:object_type/:id/relationships/:relation`
448
     * Json response
449
     *
450
     * @param string|int $id The object ID.
451
     * @param string $relation The relation name.
452
     * @return void
453
     */
454
    public function relationships($id, string $relation): void
455
    {
456
        $this->request->allowMethod(['get']);
457
        $available = $this->availableRelationshipsUrl($relation);
458
459
        try {
460
            $query = $this->Query->prepare($this->request->getQueryParams());
461
            $response = $this->apiClient->get($available, $query);
462
463
            $this->Thumbs->urls($response);
464
        } catch (BEditaClientException $ex) {
465
            $this->log($ex, LogLevel::ERROR);
466
467
            $this->set([
468
                'error' => $ex->getMessage(),
469
                '_serialize' => ['error'],
470
            ]);
471
472
            return;
473
        }
474
475
        $this->set((array)$response);
476
        $this->set('_serialize', array_keys($response));
477
    }
478
479
    /**
480
     * Retrieve URL to get objects available for a relation
481
     *
482
     * @param string $relation The relation name.
483
     * @return string
484
     */
485
    protected function availableRelationshipsUrl(string $relation): string
486
    {
487
        $defaults = [
488
            'children' => '/objects',
489
            'parent' => '/folders',
490
            'parents' => '/folders',
491
        ];
492
        $defaultUrl = (string)Hash::get($defaults, $relation);
493
        if (!empty($defaultUrl)) {
494
            return $defaultUrl;
495
        }
496
497
        $relationsSchema = $this->Schema->getRelationsSchema();
498
        $types = $this->Modules->relatedTypes($relationsSchema, $relation);
499
        if (count($types) === 1) {
500
            return sprintf('/%s', $types[0]);
501
        }
502
503
        return '/objects?filter[type][]=' . implode('&filter[type][]=', $types);
504
    }
505
506
    /**
507
     * get object properties and format them for index
508
     *
509
     * @param string $objectType objecte type name
510
     *
511
     * @return array $schema
512
     */
513
    public function getSchemaForIndex($objectType): array
514
    {
515
        $schema = (array)$this->Schema->getSchema($objectType);
516
517
        // if prop is an enum then prepend an empty string for select element
518
        if (!empty($schema['properties'])) {
519
            foreach ($schema['properties'] as &$property) {
520
                if (isset($property['enum'])) {
521
                    array_unshift($property['enum'], '');
522
                }
523
            }
524
        }
525
526
        return $schema;
527
    }
528
529
    /**
530
     * List categories for the object type.
531
     *
532
     * @return \Cake\Http\Response|null
533
     */
534
    public function listCategories()
535
    {
536
        $this->viewBuilder()->setTemplate('categories');
537
538
        $this->request->allowMethod(['get']);
539
        $response = $this->Categories->index($this->objectType, $this->request->getQueryParams());
540
        $resources = $this->Categories->map($response);
541
        $roots = $this->Categories->getAvailableRoots($resources);
542
        $categoriesTree = $this->Categories->tree($resources);
543
544
        $this->set(compact('resources', 'roots', 'categoriesTree'));
545
        $this->set('meta', (array)$response['meta']);
546
        $this->set('links', (array)$response['links']);
547
        $this->set('schema', $this->Schema->getSchema());
548
        $this->set('properties', $this->Properties->indexList('categories'));
549
        $this->set('filter', $this->Properties->filterList('categories'));
550
        $this->set('object_types', [$this->objectType]);
551
552
        return null;
553
    }
554
555
    /**
556
     * Save category.
557
     *
558
     * @return \Cake\Http\Response|null
559
     */
560
    public function saveCategory(): ?Response
561
    {
562
        $this->request->allowMethod(['post']);
563
564
        try {
565
            $this->Categories->save($this->request->getData());
0 ignored issues
show
Bug introduced by
It seems like $this->request->getData() can also be of type null; however, parameter $data of App\Controller\Component...goriesComponent::save() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

565
            $this->Categories->save(/** @scrutinizer ignore-type */ $this->request->getData());
Loading history...
566
        } catch (BEditaClientException $e) {
567
            $this->log($e, 'error');
568
            $this->Flash->error($e->getMessage(), ['params' => $e]);
569
        }
570
571
        return $this->redirect([
572
            '_name' => 'modules:categories:index',
573
            'object_type' => $this->objectType,
574
        ]);
575
    }
576
577
    /**
578
     * Remove single category.
579
     *
580
     * @param string $id Category ID.
581
     *
582
     * @return \Cake\Http\Response|null
583
     */
584
    public function removeCategory(string $id): ?Response
585
    {
586
        try {
587
            $type = $this->request->getData('object_type_name');
588
            $this->Categories->delete($id, $type);
0 ignored issues
show
Bug introduced by
It seems like $type can also be of type array; however, parameter $type of App\Controller\Component...riesComponent::delete() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

588
            $this->Categories->delete($id, /** @scrutinizer ignore-type */ $type);
Loading history...
589
        } catch (BEditaClientException $e) {
590
            $this->log($e, 'error');
591
            $this->Flash->error($e->getMessage(), ['params' => $e]);
592
        }
593
594
        return $this->redirect([
595
            '_name' => 'modules:categories:index',
596
            'object_type' => $this->objectType,
597
        ]);
598
    }
599
}
600