Completed
Push — master ( 84dee3...084275 )
by Michal
36:03
created

ModelTask::getRules()   D

Complexity

Conditions 10
Paths 38

Size

Total Lines 38
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 38
rs 4.8196
cc 10
eloc 23
nc 38
nop 2

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
4
 * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
5
 *
6
 * Licensed under The MIT License
7
 * For full copyright and license information, please see the LICENSE.txt
8
 * Redistributions of files must retain the above copyright notice.
9
 *
10
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
11
 * @link          http://cakephp.org CakePHP(tm) Project
12
 * @since         0.1.0
13
 * @license       http://www.opensource.org/licenses/mit-license.php MIT License
14
 */
15
namespace Bake\Shell\Task;
16
17
use Cake\Console\Shell;
18
use Cake\Core\Configure;
19
use Cake\Database\Schema\Table as SchemaTable;
20
use Cake\Datasource\ConnectionManager;
21
use Cake\ORM\Table;
22
use Cake\ORM\TableRegistry;
23
use Cake\Utility\Inflector;
24
25
/**
26
 * Task class for generating model files.
27
 */
28
class ModelTask extends BakeTask
29
{
30
    /**
31
     * path to Model directory
32
     *
33
     * @var string
34
     */
35
    public $pathFragment = 'Model/';
36
37
    /**
38
     * tasks
39
     *
40
     * @var array
41
     */
42
    public $tasks = [
43
        'Bake.DbConfig',
44
        'Bake.Fixture',
45
        'Bake.BakeTemplate',
46
        'Bake.Test'
47
    ];
48
49
    /**
50
     * Tables to skip when running all()
51
     *
52
     * @var array
53
     */
54
    public $skipTables = ['i18n', 'cake_sessions', 'phinxlog', 'users_phinxlog'];
55
56
    /**
57
     * Holds tables found on connection.
58
     *
59
     * @var array
60
     */
61
    protected $_tables = [];
62
63
    /**
64
     * Holds the model names
65
     *
66
     * @var array
67
     */
68
    protected $_modelNames = [];
69
70
    /**
71
     * Holds validation method map.
72
     *
73
     * @var array
74
     */
75
    protected $_validations = [];
76
77
    /**
78
     * Execution method always used for tasks
79
     *
80
     * @param string|null $name The name of the table to bake.
81
     * @return void
82
     */
83 View Code Duplication
    public function main($name = null)
84
    {
85
        parent::main();
86
        $name = $this->_getName($name);
87
88
        if (empty($name)) {
89
            $this->out('Choose a model to bake from the following:');
90
            foreach ($this->listUnskipped() as $table) {
91
                $this->out('- ' . $this->_camelize($table));
92
            }
93
            return true;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return true; (boolean) is incompatible with the return type of the parent method Bake\Shell\Task\BakeTask::main of type integer|null.

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...
94
        }
95
96
        $this->bake($this->_camelize($name));
0 ignored issues
show
Bug introduced by
It seems like $this->_camelize($name) targeting Cake\Core\ConventionsTrait::_camelize() can also be of type boolean; however, Bake\Shell\Task\ModelTask::bake() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
97
    }
98
99
    /**
100
     * Generate code for the given model name.
101
     *
102
     * @param string $name The model name to generate.
103
     * @return void
104
     */
105
    public function bake($name)
106
    {
107
        $table = $this->getTable($name);
108
        $model = $this->getTableObject($name, $table);
109
110
        $associations = $this->getAssociations($model);
111
        $this->applyAssociations($model, $associations);
112
113
        $primaryKey = $this->getPrimaryKey($model);
114
        $displayField = $this->getDisplayField($model);
115
        $propertySchema = $this->getEntityPropertySchema($model);
116
        $fields = $this->getFields();
117
        $validation = $this->getValidation($model, $associations);
118
        $rulesChecker = $this->getRules($model, $associations);
119
        $behaviors = $this->getBehaviors($model);
120
        $connection = $this->connection;
121
122
        $data = compact(
123
            'associations',
124
            'primaryKey',
125
            'displayField',
126
            'table',
127
            'propertySchema',
128
            'fields',
129
            'validation',
130
            'rulesChecker',
131
            'behaviors',
132
            'connection'
133
        );
134
        $this->bakeTable($model, $data);
135
        $this->bakeEntity($model, $data);
136
        $this->bakeFixture($model->alias(), $model->table());
137
        $this->bakeTest($model->alias());
138
    }
139
140
    /**
141
     * Bake all models at once.
142
     *
143
     * @return void
144
     */
145
    public function all()
146
    {
147
        $tables = $this->listUnskipped();
148
        foreach ($tables as $table) {
149
            TableRegistry::clear();
150
            $this->main($table);
151
        }
152
    }
153
154
    /**
155
     * Get a model object for a class name.
156
     *
157
     * @param string $className Name of class you want model to be.
158
     * @param string $table Table name
159
     * @return \Cake\ORM\Table Table instance
160
     */
161
    public function getTableObject($className, $table)
162
    {
163
        if (TableRegistry::exists($className)) {
164
            return TableRegistry::get($className);
165
        }
166
        return TableRegistry::get($className, [
167
            'name' => $className,
168
            'table' => $table,
169
            'connection' => ConnectionManager::get($this->connection)
170
        ]);
171
    }
172
173
    /**
174
     * Get the array of associations to generate.
175
     *
176
     * @param \Cake\ORM\Table $table The table to get associations for.
177
     * @return array
178
     */
179
    public function getAssociations(Table $table)
180
    {
181
        if (!empty($this->params['no-associations'])) {
182
            return [];
183
        }
184
        $this->out('One moment while associations are detected.');
185
186
        $this->listAll();
187
188
        $associations = [
189
            'belongsTo' => [],
190
            'hasMany' => [],
191
            'belongsToMany' => []
192
        ];
193
194
        $primary = $table->primaryKey();
195
        $associations = $this->findBelongsTo($table, $associations);
196
197
        if (is_array($primary) && count($primary) > 1) {
198
            $this->err(
199
                '<warning>Bake cannot generate associations for composite primary keys at this time</warning>.'
200
            );
201
            return $associations;
202
        }
203
204
        $associations = $this->findHasMany($table, $associations);
205
        $associations = $this->findBelongsToMany($table, $associations);
206
        return $associations;
207
    }
208
209
    /**
210
     * Sync the in memory table object.
211
     *
212
     * Composer's class cache prevents us from loading the
213
     * newly generated class. Applying associations if we have a
214
     * generic table object means fields will be detected correctly.
215
     *
216
     * @param \Cake\ORM\Table $model The table to apply associations to.
217
     * @param array $associations The associations to append.
218
     * @return void
219
     */
220
    public function applyAssociations($model, $associations)
221
    {
222
        if (get_class($model) !== 'Cake\ORM\Table') {
223
            return;
224
        }
225
        foreach ($associations as $type => $assocs) {
226
            foreach ($assocs as $assoc) {
227
                $alias = $assoc['alias'];
228
                unset($assoc['alias']);
229
                $model->{$type}($alias, $assoc);
230
            }
231
        }
232
    }
233
234
    /**
235
     * Find belongsTo relations and add them to the associations list.
236
     *
237
     * @param \Cake\ORM\Table $model Database\Table instance of table being generated.
238
     * @param array $associations Array of in progress associations
239
     * @return array Associations with belongsTo added in.
240
     */
241
    public function findBelongsTo($model, array $associations)
242
    {
243
        $schema = $model->schema();
244
        foreach ($schema->columns() as $fieldName) {
245
            if (!preg_match('/^.*_id$/', $fieldName)) {
246
                continue;
247
            }
248
249
            if ($fieldName === 'parent_id') {
250
                $className = ($this->plugin) ? $this->plugin . '.' . $model->alias() : $model->alias();
251
                $assoc = [
252
                    'alias' => 'Parent' . $model->alias(),
253
                    'className' => $className,
254
                    'foreignKey' => $fieldName
255
                ];
256
            } else {
257
                $tmpModelName = $this->_modelNameFromKey($fieldName);
258
                if (!in_array(Inflector::tableize($tmpModelName), $this->_tables)) {
0 ignored issues
show
Bug introduced by
It seems like $tmpModelName defined by $this->_modelNameFromKey($fieldName) on line 257 can also be of type boolean; however, Cake\Utility\Inflector::tableize() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
259
                    $found = $this->findTableReferencedBy($schema, $fieldName);
260
                    if ($found) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $found of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
261
                        $tmpModelName = Inflector::camelize($found);
262
                    }
263
                }
264
                $assoc = [
265
                    'alias' => $tmpModelName,
266
                    'foreignKey' => $fieldName
267
                ];
268
                if ($schema->column($fieldName)['null'] === false) {
269
                    $assoc['joinType'] = 'INNER';
270
                }
271
272
            }
273
274 View Code Duplication
            if ($this->plugin && empty($assoc['className'])) {
275
                $assoc['className'] = $this->plugin . '.' . $assoc['alias'];
276
            }
277
            $associations['belongsTo'][] = $assoc;
278
        }
279
        return $associations;
280
    }
281
282
    /**
283
     * find the table, if any, actually referenced by the passed key field.
284
     * Search tables in db for keyField; if found search key constraints
285
     * for the table to which it refers.
286
     *
287
     * @param \Cake\Database\Schema\Table $schema The table schema to find a constraint for.
288
     * @param string $keyField The field to check for a constraint.
289
     * @return string|null Either the referenced table or null if the field has no constraints.
290
     */
291
    public function findTableReferencedBy($schema, $keyField)
292
    {
293
        if (!$schema->column($keyField)) {
294
            return null;
295
        }
296
        foreach ($schema->constraints() as $constraint) {
297
            $constraintInfo = $schema->constraint($constraint);
298
            if (in_array($keyField, $constraintInfo['columns'])) {
299
                if (!isset($constraintInfo['references'])) {
300
                    continue;
301
                }
302
                return $constraintInfo['references'][0];
303
            }
304
        }
305
        return null;
306
    }
307
308
    /**
309
     * Find the hasMany relations and add them to associations list
310
     *
311
     * @param \Cake\ORM\Table $model Model instance being generated
312
     * @param array $associations Array of in progress associations
313
     * @return array Associations with hasMany added in.
314
     */
315
    public function findHasMany($model, array $associations)
316
    {
317
        $schema = $model->schema();
318
        $primaryKey = (array)$schema->primaryKey();
319
        $tableName = $schema->name();
320
        $foreignKey = $this->_modelKey($tableName);
321
322
        foreach ($this->listAll() as $otherTable) {
0 ignored issues
show
Bug introduced by
The expression $this->listAll() of type array|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
323
            $otherModel = $this->getTableObject($this->_camelize($otherTable), $otherTable);
0 ignored issues
show
Bug introduced by
It seems like $this->_camelize($otherTable) targeting Cake\Core\ConventionsTrait::_camelize() can also be of type boolean; however, Bake\Shell\Task\ModelTask::getTableObject() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
324
            $otherSchema = $otherModel->schema();
325
326
            // Exclude habtm join tables.
327
            $pattern = '/_' . preg_quote($tableName, '/') . '|' . preg_quote($tableName, '/') . '_/';
328
            $possibleJoinTable = preg_match($pattern, $otherTable);
329
            if ($possibleJoinTable) {
330
                continue;
331
            }
332
333
            foreach ($otherSchema->columns() as $fieldName) {
334
                $assoc = false;
335
                if (!in_array($fieldName, $primaryKey) && $fieldName === $foreignKey) {
336
                    $assoc = [
337
                        'alias' => $otherModel->alias(),
338
                        'foreignKey' => $fieldName
339
                    ];
340
                } elseif ($otherTable === $tableName && $fieldName === 'parent_id') {
341
                    $className = ($this->plugin) ? $this->plugin . '.' . $model->alias() : $model->alias();
342
                    $assoc = [
343
                        'alias' => 'Child' . $model->alias(),
344
                        'className' => $className,
345
                        'foreignKey' => $fieldName
346
                    ];
347
                }
348 View Code Duplication
                if ($assoc && $this->plugin && empty($assoc['className'])) {
349
                    $assoc['className'] = $this->plugin . '.' . $assoc['alias'];
350
                }
351
                if ($assoc) {
352
                    $associations['hasMany'][] = $assoc;
353
                }
354
            }
355
        }
356
        return $associations;
357
    }
358
359
    /**
360
     * Find the BelongsToMany relations and add them to associations list
361
     *
362
     * @param \Cake\ORM\Table $model Model instance being generated
363
     * @param array $associations Array of in-progress associations
364
     * @return array Associations with belongsToMany added in.
365
     */
366
    public function findBelongsToMany($model, array $associations)
367
    {
368
        $schema = $model->schema();
369
        $tableName = $schema->name();
370
        $foreignKey = $this->_modelKey($tableName);
371
372
        $tables = $this->listAll();
373
        foreach ($tables as $otherTable) {
0 ignored issues
show
Bug introduced by
The expression $tables of type array|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
374
            $assocTable = null;
375
            $offset = strpos($otherTable, $tableName . '_');
376
            $otherOffset = strpos($otherTable, '_' . $tableName);
377
378
            if ($offset !== false) {
379
                $assocTable = substr($otherTable, strlen($tableName . '_'));
380
            } elseif ($otherOffset !== false) {
381
                $assocTable = substr($otherTable, 0, $otherOffset);
382
            }
383
            if ($assocTable && in_array($assocTable, $tables)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $assocTable of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
384
                $habtmName = $this->_camelize($assocTable);
385
                $assoc = [
386
                    'alias' => $habtmName,
387
                    'foreignKey' => $foreignKey,
388
                    'targetForeignKey' => $this->_modelKey($habtmName),
0 ignored issues
show
Bug introduced by
It seems like $habtmName defined by $this->_camelize($assocTable) on line 384 can also be of type boolean; however, Cake\Core\ConventionsTrait::_modelKey() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
389
                    'joinTable' => $otherTable
390
                ];
391 View Code Duplication
                if ($assoc && $this->plugin) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $assoc of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
392
                    $assoc['className'] = $this->plugin . '.' . $assoc['alias'];
393
                }
394
                $associations['belongsToMany'][] = $assoc;
395
            }
396
        }
397
        return $associations;
398
    }
399
400
    /**
401
     * Get the display field from the model or parameters
402
     *
403
     * @param \Cake\ORM\Table $model The model to introspect.
404
     * @return string
405
     */
406
    public function getDisplayField($model)
407
    {
408
        if (!empty($this->params['display-field'])) {
409
            return $this->params['display-field'];
410
        }
411
        return $model->displayField();
412
    }
413
414
    /**
415
     * Get the primary key field from the model or parameters
416
     *
417
     * @param \Cake\ORM\Table $model The model to introspect.
418
     * @return array The columns in the primary key
419
     */
420
    public function getPrimaryKey($model)
421
    {
422
        if (!empty($this->params['primary-key'])) {
423
            $fields = explode(',', $this->params['primary-key']);
424
            return array_values(array_filter(array_map('trim', $fields)));
425
        }
426
        return (array)$model->primaryKey();
427
    }
428
429
    /**
430
     * Returns an entity property "schema".
431
     *
432
     * The schema is an associative array, using the property names
433
     * as keys, and information about the property as the value.
434
     *
435
     * The value part consists of at least two keys:
436
     *
437
     * - `kind`: The kind of property, either `column`, which indicates
438
     * that the property stems from a database column, or `association`,
439
     * which identifies a property that is generated for an associated
440
     * table.
441
     * - `type`: The type of the property value. For the `column` kind
442
     * this is the database type associated with the column, and for the
443
     * `association` type it's the FQN of the entity class for the
444
     * associated table.
445
     *
446
     * For `association` properties an additional key will be available
447
     *
448
     * - `association`: Holds an instance of the corresponding association
449
     * class.
450
     *
451
     * @param \Cake\ORM\Table $model The model to introspect.
452
     * @return array The property schema
453
     */
454
    public function getEntityPropertySchema(Table $model)
455
    {
456
        $properties = [];
457
458
        $schema = $model->schema();
459
        foreach ($schema->columns() as $column) {
460
            $properties[$column] = [
461
                'kind' => 'column',
462
                'type' => $schema->columnType($column)
463
            ];
464
        }
465
466
        foreach ($model->associations() as $association) {
467
            $entityClass = '\\' . ltrim($association->target()->entityClass(), '\\');
468
469
            if ($entityClass === '\Cake\ORM\Entity') {
470
                $namespace = Configure::read('App.namespace');
471
472
                list($plugin, ) = pluginSplit($association->target()->registryAlias());
473
                if ($plugin !== null) {
474
                    $namespace = $plugin;
475
                }
476
                $namespace = str_replace('/', '\\', trim($namespace, '\\'));
477
478
                $entityClass = $this->_entityName($association->target()->alias());
479
                $entityClass = '\\' . $namespace . '\Model\Entity\\' . $entityClass;
480
            }
481
482
            $properties[$association->property()] = [
483
                'kind' => 'association',
484
                'association' => $association,
485
                'type' => $entityClass
486
            ];
487
        }
488
489
        return $properties;
490
    }
491
492
    /**
493
     * Evaluates the fields and no-fields options, and
494
     * returns if, and which fields should be made accessible.
495
     *
496
     * @return array|bool|null Either an array of fields, `false` in
497
     * case the no-fields option is used, or `null` if none of the
498
     * field options is used.
499
     */
500
    public function getFields()
501
    {
502
        if (!empty($this->params['no-fields'])) {
503
            return false;
504
        }
505
        if (!empty($this->params['fields'])) {
506
            $fields = explode(',', $this->params['fields']);
507
            return array_values(array_filter(array_map('trim', $fields)));
508
        }
509
        return null;
510
    }
511
512
    /**
513
     * Get the hidden fields from a model.
514
     *
515
     * Uses the hidden and no-hidden options.
516
     *
517
     * @param \Cake\ORM\Table $model The model to introspect.
518
     * @return array The columns to make accessible
519
     */
520
    public function getHiddenFields($model)
521
    {
522
        if (!empty($this->params['no-hidden'])) {
523
            return [];
524
        }
525
        if (!empty($this->params['hidden'])) {
526
            $fields = explode(',', $this->params['hidden']);
527
            return array_values(array_filter(array_map('trim', $fields)));
528
        }
529
        $schema = $model->schema();
530
        $columns = $schema->columns();
531
        $whitelist = ['token', 'password', 'passwd'];
532
        return array_values(array_intersect($columns, $whitelist));
533
    }
534
535
    /**
536
     * Generate default validation rules.
537
     *
538
     * @param \Cake\ORM\Table $model The model to introspect.
539
     * @param array $associations The associations list.
540
     * @return array The validation rules.
541
     */
542
    public function getValidation($model, $associations = [])
543
    {
544
        if (!empty($this->params['no-validation'])) {
545
            return [];
546
        }
547
        $schema = $model->schema();
548
        $fields = $schema->columns();
549
        if (empty($fields)) {
550
            return false;
551
        }
552
553
        $validate = [];
554
        $primaryKey = (array)$schema->primaryKey();
555
        $foreignKeys = [];
556
        if (isset($associations['belongsTo'])) {
557
            foreach ($associations['belongsTo'] as $assoc) {
558
                $foreignKeys[] = $assoc['foreignKey'];
559
            }
560
        }
561
        foreach ($fields as $fieldName) {
562
            if (in_array($fieldName, $foreignKeys)) {
563
                continue;
564
            }
565
            $field = $schema->column($fieldName);
566
            $validation = $this->fieldValidation($schema, $fieldName, $field, $primaryKey);
0 ignored issues
show
Bug introduced by
It seems like $field defined by $schema->column($fieldName) on line 565 can also be of type null; however, Bake\Shell\Task\ModelTask::fieldValidation() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
567
            if (!empty($validation)) {
568
                $validate[$fieldName] = $validation;
569
            }
570
        }
571
        return $validate;
572
    }
573
574
    /**
575
     * Does individual field validation handling.
576
     *
577
     * @param \Cake\Database\Schema\Table $schema The table schema for the current field.
578
     * @param string $fieldName Name of field to be validated.
579
     * @param array $metaData metadata for field
580
     * @param string $primaryKey The primary key field
581
     * @return array Array of validation for the field.
582
     */
583
    public function fieldValidation($schema, $fieldName, array $metaData, $primaryKey)
584
    {
585
        $ignoreFields = ['created', 'modified', 'updated'];
586
        if (in_array($fieldName, $ignoreFields)) {
587
            return false;
588
        }
589
590
        $rule = false;
591
        if ($fieldName === 'email') {
592
            $rule = 'email';
593
        } elseif ($metaData['type'] === 'uuid') {
594
            $rule = 'uuid';
595
        } elseif ($metaData['type'] === 'integer') {
596
            $rule = 'numeric';
597
        } elseif ($metaData['type'] === 'float') {
598
            $rule = 'numeric';
599
        } elseif ($metaData['type'] === 'decimal') {
600
            $rule = 'decimal';
601
        } elseif ($metaData['type'] === 'boolean') {
602
            $rule = 'boolean';
603
        } elseif ($metaData['type'] === 'date') {
604
            $rule = 'date';
605
        } elseif ($metaData['type'] === 'time') {
606
            $rule = 'time';
607
        } elseif ($metaData['type'] === 'datetime') {
608
            $rule = 'datetime';
609
        } elseif ($metaData['type'] === 'inet') {
610
            $rule = 'ip';
611
        }
612
613
        $allowEmpty = false;
614
        if (in_array($fieldName, $primaryKey)) {
615
            $allowEmpty = 'create';
616
        } elseif ($metaData['null'] === true) {
617
            $allowEmpty = true;
618
        }
619
620
        $validation = [
621
            'valid' => [
622
                'rule' => $rule,
623
                'allowEmpty' => $allowEmpty,
624
            ]
625
        ];
626
627
        foreach ($schema->constraints() as $constraint) {
628
            $constraint = $schema->constraint($constraint);
629
            if (!in_array($fieldName, $constraint['columns']) || count($constraint['columns']) > 1) {
630
                continue;
631
            }
632
633
            if ($constraint['type'] === SchemaTable::CONSTRAINT_UNIQUE) {
634
                $validation['unique'] = ['rule' => 'validateUnique', 'provider' => 'table'];
635
            }
636
        }
637
638
        return $validation;
639
    }
640
641
    /**
642
     * Generate default rules checker.
643
     *
644
     * @param \Cake\ORM\Table $model The model to introspect.
645
     * @param array $associations The associations for the model.
646
     * @return array The rules to be applied.
647
     */
648
    public function getRules($model, array $associations)
649
    {
650
        if (!empty($this->params['no-rules'])) {
651
            return [];
652
        }
653
        $schema = $model->schema();
654
        $fields = $schema->columns();
655
        if (empty($fields)) {
656
            return [];
657
        }
658
659
        $rules = [];
660
        foreach ($fields as $fieldName) {
661
            if (in_array($fieldName, ['username', 'email', 'login'])) {
662
                $rules[$fieldName] = ['name' => 'isUnique'];
663
            }
664
        }
665
        foreach ($schema->constraints() as $name) {
666
            $constraint = $schema->constraint($name);
667
            if ($constraint['type'] !== SchemaTable::CONSTRAINT_UNIQUE) {
668
                continue;
669
            }
670
            if (count($constraint['columns']) > 1) {
671
                continue;
672
            }
673
            $rules[$constraint['columns'][0]] = ['name' => 'isUnique'];
674
        }
675
676
        if (empty($associations['belongsTo'])) {
677
            return $rules;
678
        }
679
680
        foreach ($associations['belongsTo'] as $assoc) {
681
            $rules[$assoc['foreignKey']] = ['name' => 'existsIn', 'extra' => $assoc['alias']];
682
        }
683
684
        return $rules;
685
    }
686
687
    /**
688
     * Get behaviors
689
     *
690
     * @param \Cake\ORM\Table $model The model to generate behaviors for.
691
     * @return array Behaviors
692
     */
693
    public function getBehaviors($model)
694
    {
695
        $behaviors = [];
696
        $schema = $model->schema();
697
        $fields = $schema->columns();
698
        if (empty($fields)) {
699
            return [];
700
        }
701
        if (in_array('created', $fields) || in_array('modified', $fields)) {
702
            $behaviors['Timestamp'] = [];
703
        }
704
705
        if (in_array('lft', $fields) && $schema->columnType('lft') === 'integer' &&
706
            in_array('rght', $fields) && $schema->columnType('rght') === 'integer' &&
707
            in_array('parent_id', $fields)
708
        ) {
709
            $behaviors['Tree'] = [];
710
        }
711
712
        $counterCache = $this->getCounterCache($model);
713
        if (!empty($counterCache)) {
714
            $behaviors['CounterCache'] = $counterCache;
715
        }
716
        return $behaviors;
717
    }
718
719
    /**
720
     * Get CounterCaches
721
     *
722
     * @param \Cake\ORM\Table $model The table to get counter cache fields for.
723
     * @return array CounterCache configurations
724
     */
725
    public function getCounterCache($model)
726
    {
727
        $belongsTo = $this->findBelongsTo($model, ['belongsTo' => []]);
728
        $counterCache = [];
729
        foreach ($belongsTo['belongsTo'] as $otherTable) {
730
            $otherAlias = $otherTable['alias'];
731
            $otherModel = $this->getTableObject($this->_camelize($otherAlias), Inflector::underscore($otherAlias));
0 ignored issues
show
Bug introduced by
It seems like $this->_camelize($otherAlias) targeting Cake\Core\ConventionsTrait::_camelize() can also be of type boolean; however, Bake\Shell\Task\ModelTask::getTableObject() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
732
733
            try {
734
                $otherSchema = $otherModel->schema();
735
            } catch (\Cake\Database\Exception $e) {
736
                continue;
737
            }
738
739
            $otherFields = $otherSchema->columns();
740
            $alias = $model->alias();
741
            $field = Inflector::singularize(Inflector::underscore($alias)) . '_count';
742
            if (in_array($field, $otherFields, true)) {
743
                $counterCache[] = "'{$otherAlias}' => ['{$field}']";
744
            }
745
        }
746
        return $counterCache;
747
    }
748
749
    /**
750
     * Bake an entity class.
751
     *
752
     * @param \Cake\ORM\Table $model Model name or object
753
     * @param array $data An array to use to generate the Table
754
     * @return string
755
     */
756
    public function bakeEntity($model, array $data = [])
757
    {
758
        if (!empty($this->params['no-entity'])) {
759
            return;
760
        }
761
        $name = $this->_entityName($model->alias());
762
763
        $namespace = Configure::read('App.namespace');
764
        $pluginPath = '';
765
        if ($this->plugin) {
766
            $namespace = $this->_pluginNamespace($this->plugin);
767
            $pluginPath = $this->plugin . '.';
768
        }
769
770
        $data += [
771
            'name' => $name,
772
            'namespace' => $namespace,
773
            'plugin' => $this->plugin,
774
            'pluginPath' => $pluginPath,
775
            'primaryKey' => [],
776
        ];
777
778
        $this->BakeTemplate->set($data);
779
        $out = $this->BakeTemplate->generate('Model/entity');
780
781
        $path = $this->getPath();
782
        $filename = $path . 'Entity' . DS . $name . '.php';
783
        $this->out("\n" . sprintf('Baking entity class for %s...', $name), 1, Shell::QUIET);
784
        $this->createFile($filename, $out);
785
        $emptyFile = $path . 'Entity' . DS . 'empty';
786
        $this->_deleteEmptyFile($emptyFile);
787
        return $out;
788
    }
789
790
    /**
791
     * Bake a table class.
792
     *
793
     * @param \Cake\ORM\Table $model Model name or object
794
     * @param array $data An array to use to generate the Table
795
     * @return string
796
     */
797
    public function bakeTable($model, array $data = [])
798
    {
799
        if (!empty($this->params['no-table'])) {
800
            return;
801
        }
802
803
        $namespace = Configure::read('App.namespace');
804
        $pluginPath = '';
805
        if ($this->plugin) {
806
            $namespace = $this->_pluginNamespace($this->plugin);
807
        }
808
809
        $name = $model->alias();
810
        $entity = $this->_entityName($model->alias());
811
        $data += [
812
            'plugin' => $this->plugin,
813
            'pluginPath' => $pluginPath,
814
            'namespace' => $namespace,
815
            'name' => $name,
816
            'entity' => $entity,
817
            'associations' => [],
818
            'primaryKey' => 'id',
819
            'displayField' => null,
820
            'table' => null,
821
            'validation' => [],
822
            'rulesChecker' => [],
823
            'behaviors' => [],
824
            'connection' => $this->connection,
825
        ];
826
827
        $this->BakeTemplate->set($data);
828
        $out = $this->BakeTemplate->generate('Model/table');
829
830
        $path = $this->getPath();
831
        $filename = $path . 'Table' . DS . $name . 'Table.php';
832
        $this->out("\n" . sprintf('Baking table class for %s...', $name), 1, Shell::QUIET);
833
        $this->createFile($filename, $out);
834
835
        // Work around composer caching that classes/files do not exist.
836
        // Check for the file as it might not exist in tests.
837
        if (file_exists($filename)) {
838
            require_once $filename;
839
        }
840
        TableRegistry::clear();
841
842
        $emptyFile = $path . 'Table' . DS . 'empty';
843
        $this->_deleteEmptyFile($emptyFile);
844
        return $out;
845
    }
846
847
    /**
848
     * Outputs the a list of possible models or controllers from database
849
     *
850
     * @return array
851
     */
852
    public function listAll()
853
    {
854
        if (!empty($this->_tables)) {
855
            return $this->_tables;
856
        }
857
858
        $this->_modelNames = [];
859
        $this->_tables = $this->_getAllTables();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->_getAllTables() can be null. However, the property $_tables is declared as array. Maybe change the type of the property to array|null or add a type check?

Our type inference engine has found an assignment of a scalar value (like a string, an integer or null) to a property which is an array.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property.

To type hint that a parameter can be either an array or null, you can set a type hint of array and a default value of null. The PHP interpreter will then accept both an array or null for that parameter.

function aContainsB(array $needle = null, array  $haystack) {
    if (!$needle) {
        return false;
    }

    return array_intersect($haystack, $needle) == $haystack;
}

The function can be called with either null or an array for the parameter $needle but will only accept an array as $haystack.

Loading history...
860
        foreach ($this->_tables as $table) {
0 ignored issues
show
Bug introduced by
The expression $this->_tables of type null|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
861
            $this->_modelNames[] = $this->_camelize($table);
862
        }
863
        return $this->_tables;
864
    }
865
866
    /**
867
     * Outputs the a list of unskipped models or controllers from database
868
     *
869
     * @return array
870
     */
871
    public function listUnskipped()
872
    {
873
        $this->listAll();
874
        return array_diff($this->_tables, $this->skipTables);
875
    }
876
877
    /**
878
     * Get an Array of all the tables in the supplied connection
879
     * will halt the script if no tables are found.
880
     *
881
     * @return array Array of tables in the database.
882
     * @throws \InvalidArgumentException When connection class
883
     *   does not have a schemaCollection method.
884
     */
885
    protected function _getAllTables()
886
    {
887
        $db = ConnectionManager::get($this->connection);
888
        if (!method_exists($db, 'schemaCollection')) {
889
            $this->err(
890
                'Connections need to implement schemaCollection() to be used with bake.'
891
            );
892
            return $this->_stop();
893
        }
894
        $schema = $db->schemaCollection();
895
        $tables = $schema->listTables();
896
        if (empty($tables)) {
897
            $this->err('Your database does not have any tables.');
898
            return $this->_stop();
899
        }
900
        sort($tables);
901
        return $tables;
902
    }
903
904
    /**
905
     * Get the table name for the model being baked.
906
     *
907
     * Uses the `table` option if it is set.
908
     *
909
     * @param string $name Table name
910
     * @return string
911
     */
912
    public function getTable($name)
913
    {
914
        if (isset($this->params['table'])) {
915
            return $this->params['table'];
916
        }
917
        return Inflector::underscore($name);
918
    }
919
920
    /**
921
     * Gets the option parser instance and configures it.
922
     *
923
     * @return \Cake\Console\ConsoleOptionParser
924
     */
925
    public function getOptionParser()
926
    {
927
        $parser = parent::getOptionParser();
928
929
        $parser->description(
930
            'Bake table and entity classes.'
931
        )->addArgument('name', [
932
            'help' => 'Name of the model to bake. Can use Plugin.name to bake plugin models.'
933
        ])->addSubcommand('all', [
934
            'help' => 'Bake all model files with associations and validation.'
935
        ])->addOption('table', [
936
            'help' => 'The table name to use if you have non-conventional table names.'
937
        ])->addOption('no-entity', [
938
            'boolean' => true,
939
            'help' => 'Disable generating an entity class.'
940
        ])->addOption('no-table', [
941
            'boolean' => true,
942
            'help' => 'Disable generating a table class.'
943
        ])->addOption('no-validation', [
944
            'boolean' => true,
945
            'help' => 'Disable generating validation rules.'
946
        ])->addOption('no-rules', [
947
            'boolean' => true,
948
            'help' => 'Disable generating a rules checker.'
949
        ])->addOption('no-associations', [
950
            'boolean' => true,
951
            'help' => 'Disable generating associations.'
952
        ])->addOption('no-fields', [
953
            'boolean' => true,
954
            'help' => 'Disable generating accessible fields in the entity.'
955
        ])->addOption('fields', [
956
            'help' => 'A comma separated list of fields to make accessible.'
957
        ])->addOption('no-hidden', [
958
            'boolean' => true,
959
            'help' => 'Disable generating hidden fields in the entity.'
960
        ])->addOption('hidden', [
961
            'help' => 'A comma separated list of fields to hide.'
962
        ])->addOption('primary-key', [
963
            'help' => 'The primary key if you would like to manually set one.' .
964
                ' Can be a comma separated list if you are using a composite primary key.'
965
        ])->addOption('display-field', [
966
            'help' => 'The displayField if you would like to choose one.'
967
        ])->addOption('no-test', [
968
            'boolean' => true,
969
            'help' => 'Do not generate a test case skeleton.'
970
        ])->addOption('no-fixture', [
971
            'boolean' => true,
972
            'help' => 'Do not generate a test fixture skeleton.'
973
        ])->epilog(
974
            'Omitting all arguments and options will list the table names you can generate models for'
975
        );
976
977
        return $parser;
978
    }
979
980
    /**
981
     * Interact with FixtureTask to automatically bake fixtures when baking models.
982
     *
983
     * @param string $className Name of class to bake fixture for
984
     * @param string|null $useTable Optional table name for fixture to use.
985
     * @return void
986
     * @see FixtureTask::bake
987
     */
988 View Code Duplication
    public function bakeFixture($className, $useTable = null)
989
    {
990
        if (!empty($this->params['no-fixture'])) {
991
            return;
992
        }
993
        $this->Fixture->connection = $this->connection;
994
        $this->Fixture->plugin = $this->plugin;
995
        $this->Fixture->bake($className, $useTable);
996
    }
997
998
    /**
999
     * Assembles and writes a unit test file
1000
     *
1001
     * @param string $className Model class name
1002
     * @return string
1003
     */
1004 View Code Duplication
    public function bakeTest($className)
1005
    {
1006
        if (!empty($this->params['no-test'])) {
1007
            return;
1008
        }
1009
        $this->Test->plugin = $this->plugin;
1010
        $this->Test->connection = $this->connection;
1011
        return $this->Test->bake('Table', $className);
1012
    }
1013
}
1014