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; |
|
|
|
|
94
|
|
|
} |
95
|
|
|
|
96
|
|
|
$this->bake($this->_camelize($name)); |
|
|
|
|
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)) { |
|
|
|
|
259
|
|
|
$found = $this->findTableReferencedBy($schema, $fieldName); |
260
|
|
|
if ($found) { |
|
|
|
|
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) { |
|
|
|
|
323
|
|
|
$otherModel = $this->getTableObject($this->_camelize($otherTable), $otherTable); |
|
|
|
|
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) { |
|
|
|
|
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)) { |
|
|
|
|
384
|
|
|
$habtmName = $this->_camelize($assocTable); |
385
|
|
|
$assoc = [ |
386
|
|
|
'alias' => $habtmName, |
387
|
|
|
'foreignKey' => $foreignKey, |
388
|
|
|
'targetForeignKey' => $this->_modelKey($habtmName), |
|
|
|
|
389
|
|
|
'joinTable' => $otherTable |
390
|
|
|
]; |
391
|
|
View Code Duplication |
if ($assoc && $this->plugin) { |
|
|
|
|
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); |
|
|
|
|
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)); |
|
|
|
|
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(); |
|
|
|
|
860
|
|
|
foreach ($this->_tables as $table) { |
|
|
|
|
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
|
|
|
|
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:
Our function
my_function
expects aPost
object, and outputs the author of the post. The base classPost
returns a simple string and outputting a simple string will work just fine. However, the child classBlogPost
which is a sub-type ofPost
instead decided to return anobject
, and is therefore violating the SOLID principles. If aBlogPost
were passed tomy_function
, PHP would not complain, but ultimately fail when executing thestrtoupper
call in its body.