CrudAction   A
last analyzed

Complexity

Total Complexity 42

Size/Duplication

Total Lines 397
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 16

Test Coverage

Coverage 82.32%

Importance

Changes 0
Metric Value
wmc 42
lcom 1
cbo 16
dl 0
loc 397
ccs 149
cts 181
cp 0.8232
rs 9.0399
c 0
b 0
f 0

16 Methods

Rating   Name   Duplication   Size   Complexity  
D __construct() 0 36 10
A getTable() 0 4 1
A setTable() 0 6 1
A table() 0 13 2
A getService() 0 4 1
A getId() 0 4 1
A getIdName() 0 4 1
A getParentId() 0 4 1
A getParentIdName() 0 4 1
A _newEntity() 0 6 1
A _patchEntity() 0 10 2
A _getEntities() 0 16 3
A _getEntity() 0 14 3
A _buildViewCondition() 0 21 4
A _save() 0 8 2
C _describe() 0 99 8

How to fix   Complexity   

Complex Class

Complex classes like CrudAction 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 CrudAction, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Copyright 2016 - 2018, Cake Development Corporation (http://cakedc.com)
4
 *
5
 * Licensed under The MIT License
6
 * Redistributions of files must retain the above copyright notice.
7
 *
8
 * @copyright Copyright 2016 - 2018, Cake Development Corporation (http://cakedc.com)
9
 * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
10
 */
11
12
namespace CakeDC\Api\Service\Action;
13
14
use CakeDC\Api\Exception\ValidationException;
15
use CakeDC\Api\Service\CrudService;
16
use CakeDC\Api\Service\Utility\ReverseRouting;
17
use Cake\Datasource\EntityInterface;
18
use Cake\Datasource\Exception\InvalidPrimaryKeyException;
19
use Cake\ORM\Table;
20
use Cake\ORM\TableRegistry;
21
use Cake\Utility\Inflector;
22
23
/**
24
 * Class CrudAction
25
 *
26
 * @package CakeDC\Api\Service\Action
27
 */
28
abstract class CrudAction extends Action
29
{
30
    /**
31
     * @var Table
32
     */
33
    protected $_table = null;
34
35
    /**
36
     * Object Identifier
37
     *
38
     * @var string
39
     */
40
    protected $_id = null;
41
42
    protected $_idName = 'id';
43
44
    /**
45
     * Crud service.
46
     *
47
     * @var CrudService
48
     */
49
    protected $_service;
50
51
    /**
52
     * Parent Object Identifier
53
     *
54
     * Used for nested services
55
     *
56
     * @var string
57
     */
58
    protected $_parentId = null;
59
60
    protected $_parentIdName = null;
61
62
    /**
63
     * Api table finder method
64
     *
65
     * @var string
66
     */
67
    protected $_finder = null;
68
69
    /**
70
     * Action constructor.
71
     *
72
     * @param array $config Configuration options passed to the constructor
73
     */
74 113
    public function __construct(array $config = [])
75
    {
76 113
        if (!empty($config['service'])) {
77 113
            $this->setService($config['service']);
78 113
        }
79 113
        if (!empty($config['table'])) {
80
            $tableName = $config['table'];
81
        } else {
82 113
            $tableName = $this->getService()->getTable();
83
        }
84 113
        if ($tableName instanceof Table) {
85
            $this->setTable($tableName);
86
        } else {
87 113
            $table = TableRegistry::getTableLocator()->get($tableName);
88 113
            $this->setTable($table);
89
        }
90 113
        if (!empty($config['id'])) {
91 24
            $this->_id = $config['id'];
92 24
        }
93 113
        if (!empty($config['idName'])) {
94 55
            $this->_idName = $config['idName'];
95 55
        }
96 113
        if (!empty($config['finder'])) {
97
            $this->_finder = $config['finder'];
98
        }
99 113
        if (!empty($config['parentId'])) {
100 9
            $this->_parentId = $config['parentId'];
101 9
        }
102 113
        if (!empty($config['parentIdName'])) {
103 9
            $this->_parentIdName = $config['parentIdName'];
104 9
        }
105 113
        if (!empty($config['table'])) {
106
            $this->setTable($config['table']);
107
        }
108 113
        parent::__construct($config);
109 113
    }
110
111
    /**
112
     * Gets a Table instance.
113
     *
114
     * @return Table
115
     */
116 75
    public function getTable()
117
    {
118 75
        return $this->_table;
119
    }
120
121
    /**
122
     * Sets the table instance.
123
     *
124
     * @param Table $table A Table instance.
125
     * @return $this
126
     */
127 113
    public function setTable(Table $table)
128
    {
129 113
        $this->_table = $table;
130
131 113
        return $this;
132
    }
133
134
    /**
135
     * Api method for table.
136
     *
137
     * @param Table $table A Table instance.
138
     * @deprecated 3.4.0 Use setTable()/getTable() instead.
139
     * @return Table
140
     */
141
    public function table($table = null)
142
    {
143
        deprecationWarning(
144
            'Action::table() is deprecated. ' .
145
            'Use Action::setTable()/getTable() instead.'
146
        );
147
148
        if ($table !== null) {
149
            return $this->setTable($table);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->setTable($table); (CakeDC\Api\Service\Action\CrudAction) is incompatible with the return type documented by CakeDC\Api\Service\Action\CrudAction::table of type Cake\ORM\Table.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
150
        }
151
152
        return $this->getTable();
153
    }
154
155
    /**
156
     * @return CrudService
157
     */
158 113
    public function getService()
159
    {
160 113
        return $this->_service;
161
    }
162
163
    /**
164
     * Model id getter.
165
     *
166
     * @return mixed|string
167
     */
168
    public function getId()
169
    {
170
        return $this->_id;
171
    }
172
173
    /**
174
     * Model id field name getter.
175
     *
176
     * @return string
177
     */
178
    public function getIdName()
179
    {
180
        return $this->_idName;
181
    }
182
183
    /**
184
     * Parent id getter.
185
     *
186
     * @return mixed|string
187
     */
188 9
    public function getParentId()
189
    {
190 9
        return $this->_parentId;
191
    }
192
193
    /**
194
     * Parent model id field name getter.
195
     *
196
     * @return mixed|string
197
     */
198 9
    public function getParentIdName()
199
    {
200 9
        return $this->_parentIdName;
201
    }
202
203
    /**
204
     * Builds new entity instance.
205
     *
206
     * @return EntityInterface
207
     */
208 10
    protected function _newEntity()
209
    {
210 10
        $entity = $this->getTable()->newEntity();
211
212 10
        return $entity;
213
    }
214
215
    /**
216
     * Patch entity.
217
     *
218
     * @param EntityInterface $entity An Entity instance.
219
     * @param array $data Entity data.
220
     * @param array $options Patch entity options.
221
     * @return \Cake\Datasource\EntityInterface|mixed
222
     */
223 13
    protected function _patchEntity($entity, $data, $options = [])
224
    {
225 13
        $entity = $this->getTable()->patchEntity($entity, $data, $options);
226 13
        $event = $this->dispatchEvent('Action.Crud.onPatchEntity', compact('entity'));
227 13
        if ($event->result) {
228 2
            $entity = $event->result;
229 2
        }
230
231 13
        return $entity;
232
    }
233
234
    /**
235
     * Builds entities list
236
     *
237
     * @return \Cake\Collection\Collection
238
     */
239 33
    protected function _getEntities()
240
    {
241 33
        $query = $this->getTable()->find();
242 33
        if ($this->_finder !== null) {
243
            $query = $query->find($this->_finder);
244
        }
245
246 33
        $event = $this->dispatchEvent('Action.Crud.onFindEntities', compact('query'));
247 33
        if ($event->result) {
248 29
            $query = $event->result;
249 29
        }
250 33
        $records = $query->all();
251 33
        $event = $this->dispatchEvent('Action.Crud.afterFindEntities', compact('query', 'records'));
0 ignored issues
show
Unused Code introduced by
$event is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
252
253 33
        return $records;
254
    }
255
256
    /**
257
     * Returns single entity by id.
258
     *
259
     * @param mixed $primaryKey Primary key.
260
     * @return \Cake\Collection\Collection
261
     */
262 22
    protected function _getEntity($primaryKey)
263
    {
264 22
        $query = $this->getTable()->find('all')->where($this->_buildViewCondition($primaryKey));
265 22
        if ($this->_finder !== null) {
266
            $query = $query->find($this->_finder);
267
        }
268 22
        $event = $this->dispatchEvent('Action.Crud.onFindEntity', compact('query'));
269 22
        if ($event->result) {
270 10
            $query = $event->result;
271 10
        }
272 22
        $entity = $query->firstOrFail();
273
274 18
        return $entity;
275
    }
276
277
    /**
278
     * Build condition for get entity method.
279
     *
280
     * @param string $primaryKey Primary key
281
     * @return array
282
     */
283 22
    protected function _buildViewCondition($primaryKey)
284
    {
285 22
        $table = $this->getTable();
286 22
        $key = (array)$table->getPrimaryKey();
287 22
        $alias = $table->getAlias();
288 22
        foreach ($key as $index => $keyname) {
289 22
            $key[$index] = $alias . '.' . $keyname;
290 22
        }
291 22
        $primaryKey = (array)$primaryKey;
292 22
        if (count($key) !== count($primaryKey)) {
293
            $primaryKey = $primaryKey ?: [null];
294
            $primaryKey = array_map(function ($key) {
295
                return var_export($key, true);
296
            }, $primaryKey);
297
298
            throw new InvalidPrimaryKeyException(sprintf('Record not found in table "%s" with primary key [%s]', $table->getTable(), implode($primaryKey, ', ')));
299
        }
300 22
        $conditions = array_combine($key, $primaryKey);
301
302 22
        return $conditions;
303
    }
304
305
    /**
306
     * Save entity.
307
     *
308
     * @param EntityInterface $entity An Entity instance.
309
     * @return EntityInterface
310
     */
311 8
    protected function _save($entity)
312
    {
313 8
        if ($this->getTable()->save($entity)) {
314 6
            return $entity;
315
        } else {
316 2
            throw new ValidationException(__('Validation on {0} failed', $this->getTable()->getAlias()), 0, null, $entity->getErrors());
0 ignored issues
show
Bug introduced by
The method getErrors() does not exist on Cake\Datasource\EntityInterface. Did you maybe mean errors()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
317
        }
318
    }
319
320
    /**
321
     * Describe table with full details.
322
     *
323
     * @return array
324
     */
325 1
    protected function _describe()
326
    {
327 1
        $table = $this->getTable();
328 1
        $schema = $table->getSchema();
329
330 1
        $entity = $this->_newEntity();
331 1
        $reverseRouter = new ReverseRouting();
332 1
        $path = $reverseRouter->indexPath($this);
333
        $actions = [
334 1
            'index' => $reverseRouter->link('self', $path, 'GET'),
335 1
            'add' => $reverseRouter->link('add', $path, 'POST'),
336 1
            'edit' => $reverseRouter->link('edit', $path . '/{id}', 'PUT'),
337 1
            'delete' => $reverseRouter->link('delete', $path . '/{id}', 'DELETE'),
338 1
        ];
339
340 1
        $validators = [];
341 1
        foreach ($table->getValidator()
342 1
                       ->getIterator() as $name => $field) {
343 1
            $validators[$name] = [
344 1
                'validatePresence' => $field->isPresenceRequired(),
345 1
                'emptyAllowed' => $field->isEmptyAllowed(),
346 1
                'rules' => []
347 1
            ];
348 1
            foreach ($field->getIterator() as $ruleName => $rule) {
349 1
                $_rule = $rule->get('rule');
350 1
                if (is_callable($_rule)) {
351
                    continue;
352
                }
353 1
                $params = $rule->get('pass');
354 1
                if (is_callable($params)) {
355
                    $params = null;
356
                }
357 1
                if (is_array($params)) {
358 1
                    foreach ($params as &$param) {
359
                        if (is_callable($param)) {
360
                            $param = null;
361
                        }
362 1
                    }
363 1
                }
364 1
                $validators[$name]['rules'][$ruleName] = [
365 1
                    'message' => $rule->get('message'),
366 1
                    'on' => $rule->get('on'),
367 1
                    'rule' => $_rule,
368 1
                    'params' => $params,
369 1
                    'last' => $rule->get('last'),
370
                ];
371 1
            }
372 1
        }
373
374 1
        $labels = collection($schema->columns())
375
            ->map(function ($column) use ($schema) {
376
                return [
377 1
                    'name' => $column,
378 1
                    'label' => __(Inflector::humanize(preg_replace('/_id$/', '', $column)))
379 1
                ];
380 1
            })
381 1
            ->combine('name', 'label')
382 1
            ->toArray();
383
384 1
        $associationTypes = ['BelongsTo', 'HasOne', 'HasMany', 'BelongsToMany'];
385 1
        $associations = collection($associationTypes)
386
            ->map(function ($type) use ($table) {
387
                return [
388 1
                    'type' => $type,
389 1
                    'assocs' => collection($table->associations()->getByType($type))
390
                        ->map(function ($assoc) {
391 1
                            return $assoc->getTarget()->getTable();
392 1
                        })
393 1
                        ->toArray()
394 1
                ];
395 1
            })
396 1
            ->combine('type', 'assocs')
397 1
            ->toArray();
398
399 1
        $fieldTypes = collection($schema->columns())
400 1
            ->map(function ($column) use ($schema) {
401
                return [
402 1
                    'name' => $column,
403 1
                    'column' => $schema->getColumn($column)
404 1
                ];
405 1
            })
406 1
            ->combine('name', 'column')
407 1
            ->toArray();
408
409
        return [
410
            'entity' => [
411 1
                'hidden' => $entity->getHidden(),
412
413
                // ... fields with data types
414 1
            ],
415
            'schema' => [
416 1
                'columns' => $fieldTypes,
417
                'labels' => $labels
418 1
            ],
419 1
            'validators' => $validators,
420 1
            'relations' => $associations,
421
            'actions' => $actions
422 1
        ];
423
    }
424
}
425