ModelTask::_interactive()   F
last analyzed

Complexity

Conditions 20
Paths 18434

Size

Total Lines 98
Code Lines 68

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 20
eloc 68
c 0
b 0
f 0
nc 18434
nop 0
dl 0
loc 98
rs 2

How to fix   Long Method    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
 * The ModelTask handles creating and updating models files.
4
 *
5
 * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
6
 * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
7
 *
8
 * Licensed under The MIT License
9
 * For full copyright and license information, please see the LICENSE.txt
10
 * Redistributions of files must retain the above copyright notice.
11
 *
12
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
13
 * @link          http://cakephp.org CakePHP(tm) Project
14
 * @since         CakePHP(tm) v 1.2
15
 * @license       http://www.opensource.org/licenses/mit-license.php MIT License
16
 */
17
18
App::uses('AppShell', 'Console/Command');
19
App::uses('BakeTask', 'Console/Command/Task');
20
App::uses('ConnectionManager', 'Model');
21
App::uses('Model', 'Model');
22
App::uses('Validation', 'Utility');
23
24
/**
25
 * Task class for creating and updating model files.
26
 *
27
 * @package	   Cake.Console.Command.Task
28
 */
29
class ModelTask extends BakeTask {
30
31
/**
32
 * path to Model directory
33
 *
34
 * @var string
35
 */
36
	public $path = null;
37
38
/**
39
 * tasks
40
 *
41
 * @var array
42
 */
43
	public $tasks = array('DbConfig', 'Fixture', 'Test', 'Template');
44
45
/**
46
 * Tables to skip when running all()
47
 *
48
 * @var array
49
 */
50
	public $skipTables = array('i18n');
51
52
/**
53
 * Holds tables found on connection.
54
 *
55
 * @var array
56
 */
57
	protected $_tables = array();
58
59
/**
60
 * Holds the model names
61
 *
62
 * @var array
63
 */
64
	protected $_modelNames = array();
65
66
/**
67
 * Holds validation method map.
68
 *
69
 * @var array
70
 */
71
	protected $_validations = array();
72
73
/**
74
 * Override initialize
75
 *
76
 * @return void
77
 */
78
	public function initialize() {
79
		$this->path = current(App::path('Model'));
80
	}
81
82
/**
83
 * Execution method always used for tasks
84
 *
85
 * @return void
86
 */
87
	public function execute() {
88
		parent::execute();
89
90
		if (empty($this->args)) {
91
			$this->_interactive();
92
		}
93
94
		if (!empty($this->args[0])) {
95
			$this->interactive = false;
96
			if (!isset($this->connection)) {
97
				$this->connection = 'default';
98
			}
99
			if (strtolower($this->args[0]) === 'all') {
100
				return $this->all();
101
			}
102
			$model = $this->_modelName($this->args[0]);
103
			$this->listAll($this->connection);
104
			$useTable = $this->getTable($model);
105
			$object = $this->_getModelObject($model, $useTable);
106
			if ($this->bake($object, false)) {
107
				if ($this->_checkUnitTest()) {
108
					$this->bakeFixture($model, $useTable);
109
					$this->bakeTest($model);
110
				}
111
			}
112
		}
113
	}
114
115
/**
116
 * Bake all models at once.
117
 *
118
 * @return void
119
 */
120
	public function all() {
121
		$this->listAll($this->connection, false);
122
		$unitTestExists = $this->_checkUnitTest();
123
		foreach ($this->_tables as $table) {
124
			if (in_array($table, $this->skipTables)) {
125
				continue;
126
			}
127
			$modelClass = Inflector::classify($table);
128
			$this->out(__d('cake_console', 'Baking %s', $modelClass));
129
			$object = $this->_getModelObject($modelClass, $table);
130
			if ($this->bake($object, false) && $unitTestExists) {
131
				$this->bakeFixture($modelClass, $table);
132
				$this->bakeTest($modelClass);
133
			}
134
		}
135
	}
136
137
/**
138
 * Get a model object for a class name.
139
 *
140
 * @param string $className Name of class you want model to be.
141
 * @param string $table Table name
142
 * @return Model Model instance
143
 */
144
	protected function _getModelObject($className, $table = null) {
145
		if (!$table) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $table of type string|null is loosely compared to false; 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...
146
			$table = Inflector::tableize($className);
147
		}
148
		$object = new Model(array('name' => $className, 'table' => $table, 'ds' => $this->connection));
149
		$fields = $object->schema(true);
150 View Code Duplication
		foreach ($fields as $name => $field) {
151
			if (isset($field['key']) && $field['key'] === 'primary') {
152
				$object->primaryKey = $name;
0 ignored issues
show
Documentation Bug introduced by
It seems like $name can also be of type integer. However, the property $primaryKey is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

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

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
153
				break;
154
			}
155
		}
156
		return $object;
157
	}
158
159
/**
160
 * Generate a key value list of options and a prompt.
161
 *
162
 * @param array $options Array of options to use for the selections. indexes must start at 0
163
 * @param string $prompt Prompt to use for options list.
164
 * @param integer $default The default option for the given prompt.
165
 * @return integer Result of user choice.
166
 */
167
	public function inOptions($options, $prompt = null, $default = null) {
168
		$valid = false;
169
		$max = count($options);
170
		while (!$valid) {
171
			$len = strlen(count($options) + 1);
172
			foreach ($options as $i => $option) {
173
				$this->out(sprintf("%${len}d. %s", $i + 1, $option));
174
			}
175
			if (empty($prompt)) {
176
				$prompt = __d('cake_console', 'Make a selection from the choices above');
177
			}
178
			$choice = $this->in($prompt, null, $default);
179 View Code Duplication
			if (intval($choice) > 0 && intval($choice) <= $max) {
180
				$valid = true;
181
			}
182
		}
183
		return $choice - 1;
0 ignored issues
show
Bug introduced by
The variable $choice does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
184
	}
185
186
/**
187
 * Handles interactive baking
188
 *
189
 * @return boolean
190
 */
191
	protected function _interactive() {
192
		$this->hr();
193
		$this->out(__d('cake_console', "Bake Model\nPath: %s", $this->getPath()));
194
		$this->hr();
195
		$this->interactive = true;
196
197
		$primaryKey = 'id';
198
		$validate = $associations = array();
199
200
		if (empty($this->connection)) {
201
			$this->connection = $this->DbConfig->getConfig();
202
		}
203
		$currentModelName = $this->getName();
204
		$useTable = $this->getTable($currentModelName);
205
		$db = ConnectionManager::getDataSource($this->connection);
206
		$fullTableName = $db->fullTableName($useTable);
207
		if (!in_array($useTable, $this->_tables)) {
208
			$prompt = __d('cake_console', "The table %s doesn't exist or could not be automatically detected\ncontinue anyway?", $useTable);
209
			$continue = $this->in($prompt, array('y', 'n'));
210
			if (strtolower($continue) === 'n') {
211
				return false;
212
			}
213
		}
214
215
		$tempModel = new Model(array('name' => $currentModelName, 'table' => $useTable, 'ds' => $this->connection));
216
217
		$knownToExist = false;
218
		try {
219
			$fields = $tempModel->schema(true);
220
			$knownToExist = true;
221
		} catch (Exception $e) {
222
			$fields = array($tempModel->primaryKey);
223
		}
224
		if (!array_key_exists('id', $fields)) {
225
			$primaryKey = $this->findPrimaryKey($fields);
226
		}
227
228
		if ($knownToExist) {
229
			$displayField = $tempModel->hasField(array('name', 'title'));
230
			if (!$displayField) {
231
				$displayField = $this->findDisplayField($tempModel->schema());
232
			}
233
234
			$prompt = __d('cake_console', "Would you like to supply validation criteria \nfor the fields in your model?");
235
			$wannaDoValidation = $this->in($prompt, array('y', 'n'), 'y');
236
			if (array_search($useTable, $this->_tables) !== false && strtolower($wannaDoValidation) === 'y') {
237
				$validate = $this->doValidation($tempModel);
238
			}
239
240
			$prompt = __d('cake_console', "Would you like to define model associations\n(hasMany, hasOne, belongsTo, etc.)?");
241
			$wannaDoAssoc = $this->in($prompt, array('y', 'n'), 'y');
242
			if (strtolower($wannaDoAssoc) === 'y') {
243
				$associations = $this->doAssociations($tempModel);
244
			}
245
		}
246
247
		$this->out();
248
		$this->hr();
249
		$this->out(__d('cake_console', 'The following Model will be created:'));
250
		$this->hr();
251
		$this->out(__d('cake_console', "Name:       %s", $currentModelName));
252
253
		if ($this->connection !== 'default') {
254
			$this->out(__d('cake_console', "DB Config:  %s", $this->connection));
255
		}
256
		if ($fullTableName !== Inflector::tableize($currentModelName)) {
257
			$this->out(__d('cake_console', 'DB Table:   %s', $fullTableName));
258
		}
259
		if ($primaryKey !== 'id') {
260
			$this->out(__d('cake_console', 'Primary Key: %s', $primaryKey));
261
		}
262
		if (!empty($validate)) {
263
			$this->out(__d('cake_console', 'Validation: %s', print_r($validate, true)));
264
		}
265
		if (!empty($associations)) {
266
			$this->out(__d('cake_console', 'Associations:'));
267
			$assocKeys = array('belongsTo', 'hasOne', 'hasMany', 'hasAndBelongsToMany');
268
			foreach ($assocKeys as $assocKey) {
269
				$this->_printAssociation($currentModelName, $assocKey, $associations);
270
			}
271
		}
272
273
		$this->hr();
274
		$looksGood = $this->in(__d('cake_console', 'Look okay?'), array('y', 'n'), 'y');
275
276
		if (strtolower($looksGood) === 'y') {
277
			$vars = compact('associations', 'validate', 'primaryKey', 'useTable', 'displayField');
278
			$vars['useDbConfig'] = $this->connection;
279
			if ($this->bake($currentModelName, $vars)) {
280
				if ($this->_checkUnitTest()) {
281
					$this->bakeFixture($currentModelName, $useTable);
282
					$this->bakeTest($currentModelName, $useTable, $associations);
283
				}
284
			}
285
		} else {
286
			return false;
287
		}
288
	}
289
290
/**
291
 * Print out all the associations of a particular type
292
 *
293
 * @param string $modelName Name of the model relations belong to.
294
 * @param string $type Name of association you want to see. i.e. 'belongsTo'
295
 * @param string $associations Collection of associations.
296
 * @return void
297
 */
298
	protected function _printAssociation($modelName, $type, $associations) {
299
		if (!empty($associations[$type])) {
300
			for ($i = 0, $len = count($associations[$type]); $i < $len; $i++) {
301
				$out = "\t" . $modelName . ' ' . $type . ' ' . $associations[$type][$i]['alias'];
302
				$this->out($out);
303
			}
304
		}
305
	}
306
307
/**
308
 * Finds a primary Key in a list of fields.
309
 *
310
 * @param array $fields Array of fields that might have a primary key.
311
 * @return string Name of field that is a primary key.
312
 */
313
	public function findPrimaryKey($fields) {
314
		$name = 'id';
315 View Code Duplication
		foreach ($fields as $name => $field) {
316
			if (isset($field['key']) && $field['key'] === 'primary') {
317
				break;
318
			}
319
		}
320
		return $this->in(__d('cake_console', 'What is the primaryKey?'), null, $name);
321
	}
322
323
/**
324
 * interact with the user to find the displayField value for a model.
325
 *
326
 * @param array $fields Array of fields to look for and choose as a displayField
327
 * @return mixed Name of field to use for displayField or false if the user declines to choose
328
 */
329
	public function findDisplayField($fields) {
330
		$fieldNames = array_keys($fields);
331
		$prompt = __d('cake_console', "A displayField could not be automatically detected\nwould you like to choose one?");
332
		$continue = $this->in($prompt, array('y', 'n'));
333
		if (strtolower($continue) === 'n') {
334
			return false;
335
		}
336
		$prompt = __d('cake_console', 'Choose a field from the options above:');
337
		$choice = $this->inOptions($fieldNames, $prompt);
338
		return $fieldNames[$choice];
339
	}
340
341
/**
342
 * Handles Generation and user interaction for creating validation.
343
 *
344
 * @param Model $model Model to have validations generated for.
345
 * @return array $validate Array of user selected validations.
346
 */
347
	public function doValidation($model) {
348
		if (!$model instanceof Model) {
349
			return false;
350
		}
351
		$fields = $model->schema();
352
353
		if (empty($fields)) {
354
			return false;
355
		}
356
		$validate = array();
357
		$this->initValidations();
358
		foreach ($fields as $fieldName => $field) {
359
			$validation = $this->fieldValidation($fieldName, $field, $model->primaryKey);
360
			if (!empty($validation)) {
361
				$validate[$fieldName] = $validation;
362
			}
363
		}
364
		return $validate;
365
	}
366
367
/**
368
 * Populate the _validations array
369
 *
370
 * @return void
371
 */
372
	public function initValidations() {
373
		$options = $choices = array();
374
		if (class_exists('Validation')) {
375
			$options = get_class_methods('Validation');
376
		}
377
		sort($options);
378
		$default = 1;
379
		foreach ($options as $option) {
380
			if ($option{0} !== '_') {
381
				$choices[$default] = $option;
382
				$default++;
383
			}
384
		}
385
		$choices[$default] = 'none'; // Needed since index starts at 1
386
		$this->_validations = $choices;
387
		return $choices;
388
	}
389
390
/**
391
 * Does individual field validation handling.
392
 *
393
 * @param string $fieldName Name of field to be validated.
394
 * @param array $metaData metadata for field
395
 * @param string $primaryKey
396
 * @return array Array of validation for the field.
397
 */
398
	public function fieldValidation($fieldName, $metaData, $primaryKey = 'id') {
399
		$defaultChoice = count($this->_validations);
400
		$validate = $alreadyChosen = array();
401
402
		$anotherValidator = 'y';
403
		while ($anotherValidator === 'y') {
404
			if ($this->interactive) {
405
				$this->out();
406
				$this->out(__d('cake_console', 'Field: <info>%s</info>', $fieldName));
407
				$this->out(__d('cake_console', 'Type: <info>%s</info>', $metaData['type']));
408
				$this->hr();
409
				$this->out(__d('cake_console', 'Please select one of the following validation options:'));
410
				$this->hr();
411
412
				$optionText = '';
413
				for ($i = 1, $m = $defaultChoice / 2; $i <= $m; $i++) {
414
					$line = sprintf("%2d. %s", $i, $this->_validations[$i]);
415
					$optionText .= $line . str_repeat(" ", 31 - strlen($line));
416
					if ($m + $i !== $defaultChoice) {
417
						$optionText .= sprintf("%2d. %s\n", $m + $i, $this->_validations[$m + $i]);
418
					}
419
				}
420
				$this->out($optionText);
421
				$this->out(__d('cake_console', "%s - Do not do any validation on this field.", $defaultChoice));
422
				$this->hr();
423
			}
424
425
			$prompt = __d('cake_console', "... or enter in a valid regex validation string.\n");
426
			$methods = array_flip($this->_validations);
427
			$guess = $defaultChoice;
428
			if ($metaData['null'] != 1 && !in_array($fieldName, array($primaryKey, 'created', 'modified', 'updated'))) {
429
				if ($fieldName === 'email') {
430
					$guess = $methods['email'];
431
				} elseif ($metaData['type'] === 'string' && $metaData['length'] == 36) {
432
					$guess = $methods['uuid'];
433
				} elseif ($metaData['type'] === 'string') {
434
					$guess = $methods['notEmpty'];
435
				} elseif ($metaData['type'] === 'text') {
436
					$guess = $methods['notEmpty'];
437
				} elseif ($metaData['type'] === 'integer') {
438
					$guess = $methods['numeric'];
439
				} elseif ($metaData['type'] === 'float') {
440
					$guess = $methods['numeric'];
441
				} elseif ($metaData['type'] === 'boolean') {
442
					$guess = $methods['boolean'];
443
				} elseif ($metaData['type'] === 'date') {
444
					$guess = $methods['date'];
445
				} elseif ($metaData['type'] === 'time') {
446
					$guess = $methods['time'];
447
				} elseif ($metaData['type'] === 'datetime') {
448
					$guess = $methods['datetime'];
449
				} elseif ($metaData['type'] === 'inet') {
450
					$guess = $methods['ip'];
451
				}
452
			}
453
454
			if ($this->interactive === true) {
455
				$choice = $this->in($prompt, null, $guess);
456
				if (in_array($choice, $alreadyChosen)) {
457
					$this->out(__d('cake_console', "You have already chosen that validation rule,\nplease choose again"));
458
					continue;
459
				}
460
				if (!isset($this->_validations[$choice]) && is_numeric($choice)) {
461
					$this->out(__d('cake_console', 'Please make a valid selection.'));
462
					continue;
463
				}
464
				$alreadyChosen[] = $choice;
465
			} else {
466
				$choice = $guess;
467
			}
468
469
			if (isset($this->_validations[$choice])) {
470
				$validatorName = $this->_validations[$choice];
471
			} else {
472
				$validatorName = Inflector::slug($choice);
473
			}
474
475
			if ($choice != $defaultChoice) {
476
				$validate[$validatorName] = $choice;
477
				if (is_numeric($choice) && isset($this->_validations[$choice])) {
478
					$validate[$validatorName] = $this->_validations[$choice];
479
				}
480
			}
481
			$anotherValidator = 'n';
482
			if ($this->interactive && $choice != $defaultChoice) {
483
				$anotherValidator = $this->in(__d('cake_console', 'Would you like to add another validation rule?'), array('y', 'n'), 'n');
484
			}
485
		}
486
		return $validate;
487
	}
488
489
/**
490
 * Handles associations
491
 *
492
 * @param Model $model
493
 * @return array Associations
494
 */
495
	public function doAssociations($model) {
496
		if (!$model instanceof Model) {
497
			return false;
498
		}
499
		if ($this->interactive === true) {
500
			$this->out(__d('cake_console', 'One moment while the associations are detected.'));
501
		}
502
503
		$fields = $model->schema(true);
504
		if (empty($fields)) {
505
			return array();
506
		}
507
508
		if (empty($this->_tables)) {
509
			$this->_tables = (array)$this->getAllTables();
510
		}
511
512
		$associations = array(
513
			'belongsTo' => array(),
514
			'hasMany' => array(),
515
			'hasOne' => array(),
516
			'hasAndBelongsToMany' => array()
517
		);
518
519
		$associations = $this->findBelongsTo($model, $associations);
520
		$associations = $this->findHasOneAndMany($model, $associations);
521
		$associations = $this->findHasAndBelongsToMany($model, $associations);
522
523
		if ($this->interactive !== true) {
524
			unset($associations['hasOne']);
525
		}
526
527
		if ($this->interactive === true) {
528
			$this->hr();
529
			if (empty($associations)) {
530
				$this->out(__d('cake_console', 'None found.'));
531
			} else {
532
				$this->out(__d('cake_console', 'Please confirm the following associations:'));
533
				$this->hr();
534
				$associations = $this->confirmAssociations($model, $associations);
535
			}
536
			$associations = $this->doMoreAssociations($model, $associations);
537
		}
538
		return $associations;
539
	}
540
541
/**
542
 * Handles behaviors
543
 *
544
 * @param Model $model
545
 * @return array Behaviors
546
 */
547
	public function doActsAs($model) {
548
		if (!$model instanceof Model) {
549
			return false;
550
		}
551
		$behaviors = array();
552
		$fields = $model->schema(true);
553
		if (empty($fields)) {
554
			return array();
555
		}
556
557
		if (isset($fields['lft']) && $fields['lft']['type'] === 'integer' &&
558
			isset($fields['rght']) && $fields['rght']['type'] === 'integer' &&
559
			isset($fields['parent_id'])) {
560
			$behaviors[] = 'Tree';
561
		}
562
		return $behaviors;
563
	}
564
565
/**
566
 * Find belongsTo relations and add them to the associations list.
567
 *
568
 * @param Model $model Model instance of model being generated.
569
 * @param array $associations Array of in progress associations
570
 * @return array Associations with belongsTo added in.
571
 */
572
	public function findBelongsTo(Model $model, $associations) {
573
		$fieldNames = array_keys($model->schema(true));
574
		foreach ($fieldNames as $fieldName) {
575
			$offset = substr($fieldName, -3) === '_id';
576
			if ($fieldName != $model->primaryKey && $fieldName !== 'parent_id' && $offset !== false) {
577
				$tmpModelName = $this->_modelNameFromKey($fieldName);
578
				$associations['belongsTo'][] = array(
579
					'alias' => $tmpModelName,
580
					'className' => $tmpModelName,
581
					'foreignKey' => $fieldName,
582
				);
583
			} elseif ($fieldName === 'parent_id') {
584
				$associations['belongsTo'][] = array(
585
					'alias' => 'Parent' . $model->name,
586
					'className' => $model->name,
587
					'foreignKey' => $fieldName,
588
				);
589
			}
590
		}
591
		return $associations;
592
	}
593
594
/**
595
 * Find the hasOne and hasMany relations and add them to associations list
596
 *
597
 * @param Model $model Model instance being generated
598
 * @param array $associations Array of in progress associations
599
 * @return array Associations with hasOne and hasMany added in.
600
 */
601
	public function findHasOneAndMany(Model $model, $associations) {
602
		$foreignKey = $this->_modelKey($model->name);
603
		foreach ($this->_tables as $otherTable) {
604
			$tempOtherModel = $this->_getModelObject($this->_modelName($otherTable), $otherTable);
605
			$tempFieldNames = array_keys($tempOtherModel->schema(true));
606
607
			$pattern = '/_' . preg_quote($model->table, '/') . '|' . preg_quote($model->table, '/') . '_/';
608
			$possibleJoinTable = preg_match($pattern, $otherTable);
609
			if ($possibleJoinTable) {
610
				continue;
611
			}
612
			foreach ($tempFieldNames as $fieldName) {
613
				$assoc = false;
614
				if ($fieldName !== $model->primaryKey && $fieldName === $foreignKey) {
615
					$assoc = array(
616
						'alias' => $tempOtherModel->name,
617
						'className' => $tempOtherModel->name,
618
						'foreignKey' => $fieldName
619
					);
620
				} elseif ($otherTable === $model->table && $fieldName === 'parent_id') {
621
					$assoc = array(
622
						'alias' => 'Child' . $model->name,
623
						'className' => $model->name,
624
						'foreignKey' => $fieldName
625
					);
626
				}
627
				if ($assoc) {
628
					$associations['hasOne'][] = $assoc;
629
					$associations['hasMany'][] = $assoc;
630
				}
631
632
			}
633
		}
634
		return $associations;
635
	}
636
637
/**
638
 * Find the hasAndBelongsToMany relations and add them to associations list
639
 *
640
 * @param Model $model Model instance being generated
641
 * @param array $associations Array of in-progress associations
642
 * @return array Associations with hasAndBelongsToMany added in.
643
 */
644
	public function findHasAndBelongsToMany(Model $model, $associations) {
645
		$foreignKey = $this->_modelKey($model->name);
646
		foreach ($this->_tables as $otherTable) {
647
			$tableName = null;
648
			$offset = strpos($otherTable, $model->table . '_');
649
			$otherOffset = strpos($otherTable, '_' . $model->table);
650
651
			if ($offset !== false) {
652
				$tableName = substr($otherTable, strlen($model->table . '_'));
653
			} elseif ($otherOffset !== false) {
654
				$tableName = substr($otherTable, 0, $otherOffset);
655
			}
656
			if ($tableName && in_array($tableName, $this->_tables)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $tableName 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...
657
				$habtmName = $this->_modelName($tableName);
658
				$associations['hasAndBelongsToMany'][] = array(
659
					'alias' => $habtmName,
660
					'className' => $habtmName,
661
					'foreignKey' => $foreignKey,
662
					'associationForeignKey' => $this->_modelKey($habtmName),
663
					'joinTable' => $otherTable
664
				);
665
			}
666
		}
667
		return $associations;
668
	}
669
670
/**
671
 * Interact with the user and confirm associations.
672
 *
673
 * @param array $model Temporary Model instance.
674
 * @param array $associations Array of associations to be confirmed.
675
 * @return array Array of confirmed associations
676
 */
677
	public function confirmAssociations(Model $model, $associations) {
678
		foreach ($associations as $type => $settings) {
679
			if (!empty($associations[$type])) {
680
				foreach ($associations[$type] as $i => $assoc) {
681
					$prompt = "{$model->name} {$type} {$assoc['alias']}?";
682
					$response = $this->in($prompt, array('y', 'n'), 'y');
683
684
					if (strtolower($response) === 'n') {
685
						unset($associations[$type][$i]);
686
					} elseif ($type === 'hasMany') {
687
						unset($associations['hasOne'][$i]);
688
					}
689
				}
690
				$associations[$type] = array_merge($associations[$type]);
691
			}
692
		}
693
		return $associations;
694
	}
695
696
/**
697
 * Interact with the user and generate additional non-conventional associations
698
 *
699
 * @param Model $model Temporary model instance
700
 * @param array $associations Array of associations.
701
 * @return array Array of associations.
702
 */
703
	public function doMoreAssociations(Model $model, $associations) {
704
		$prompt = __d('cake_console', 'Would you like to define some additional model associations?');
705
		$wannaDoMoreAssoc = $this->in($prompt, array('y', 'n'), 'n');
706
		$possibleKeys = $this->_generatePossibleKeys();
707
		while (strtolower($wannaDoMoreAssoc) === 'y') {
708
			$assocs = array('belongsTo', 'hasOne', 'hasMany', 'hasAndBelongsToMany');
709
			$this->out(__d('cake_console', 'What is the association type?'));
710
			$assocType = intval($this->inOptions($assocs, __d('cake_console', 'Enter a number')));
711
712
			$this->out(__d('cake_console', "For the following options be very careful to match your setup exactly.\n" .
713
				"Any spelling mistakes will cause errors."));
714
			$this->hr();
715
716
			$alias = $this->in(__d('cake_console', 'What is the alias for this association?'));
717
			$className = $this->in(__d('cake_console', 'What className will %s use?', $alias), null, $alias);
718
719
			if ($assocType === 0) {
720
				if (!empty($possibleKeys[$model->table])) {
721
					$showKeys = $possibleKeys[$model->table];
722
				} else {
723
					$showKeys = null;
724
				}
725
				$suggestedForeignKey = $this->_modelKey($alias);
726
			} else {
727
				$otherTable = Inflector::tableize($className);
728
				if (in_array($otherTable, $this->_tables)) {
729
					if ($assocType < 3) {
730
						if (!empty($possibleKeys[$otherTable])) {
731
							$showKeys = $possibleKeys[$otherTable];
732
						} else {
733
							$showKeys = null;
734
						}
735
					} else {
736
						$showKeys = null;
737
					}
738
				} else {
739
					$otherTable = $this->in(__d('cake_console', 'What is the table for this model?'));
740
					$showKeys = $possibleKeys[$otherTable];
741
				}
742
				$suggestedForeignKey = $this->_modelKey($model->name);
743
			}
744
			if (!empty($showKeys)) {
745
				$this->out(__d('cake_console', 'A helpful List of possible keys'));
746
				$foreignKey = $this->inOptions($showKeys, __d('cake_console', 'What is the foreignKey?'));
747
				$foreignKey = $showKeys[intval($foreignKey)];
748
			}
749
			if (!isset($foreignKey)) {
750
				$foreignKey = $this->in(__d('cake_console', 'What is the foreignKey? Specify your own.'), null, $suggestedForeignKey);
751
			}
752
			if ($assocType === 3) {
753
				$associationForeignKey = $this->in(__d('cake_console', 'What is the associationForeignKey?'), null, $this->_modelKey($model->name));
754
				$joinTable = $this->in(__d('cake_console', 'What is the joinTable?'));
755
			}
756
			$associations[$assocs[$assocType]] = array_values((array)$associations[$assocs[$assocType]]);
757
			$count = count($associations[$assocs[$assocType]]);
758
			$i = ($count > 0) ? $count : 0;
759
			$associations[$assocs[$assocType]][$i]['alias'] = $alias;
760
			$associations[$assocs[$assocType]][$i]['className'] = $className;
761
			$associations[$assocs[$assocType]][$i]['foreignKey'] = $foreignKey;
762
			if ($assocType === 3) {
763
				$associations[$assocs[$assocType]][$i]['associationForeignKey'] = $associationForeignKey;
0 ignored issues
show
Bug introduced by
The variable $associationForeignKey does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
764
				$associations[$assocs[$assocType]][$i]['joinTable'] = $joinTable;
0 ignored issues
show
Bug introduced by
The variable $joinTable does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
765
			}
766
			$wannaDoMoreAssoc = $this->in(__d('cake_console', 'Define another association?'), array('y', 'n'), 'y');
767
		}
768
		return $associations;
769
	}
770
771
/**
772
 * Finds all possible keys to use on custom associations.
773
 *
774
 * @return array Array of tables and possible keys
775
 */
776
	protected function _generatePossibleKeys() {
777
		$possible = array();
778
		foreach ($this->_tables as $otherTable) {
779
			$tempOtherModel = new Model(array('table' => $otherTable, 'ds' => $this->connection));
780
			$modelFieldsTemp = $tempOtherModel->schema(true);
781
			foreach ($modelFieldsTemp as $fieldName => $field) {
782
				if ($field['type'] === 'integer' || $field['type'] === 'string') {
783
					$possible[$otherTable][] = $fieldName;
784
				}
785
			}
786
		}
787
		return $possible;
788
	}
789
790
/**
791
 * Assembles and writes a Model file.
792
 *
793
 * @param string|object $name Model name or object
794
 * @param array|boolean $data if array and $name is not an object assume bake data, otherwise boolean.
795
 * @return string
796
 */
797
	public function bake($name, $data = array()) {
798
		if ($name instanceof Model) {
799
			if (!$data) {
800
				$data = array();
801
				$data['associations'] = $this->doAssociations($name);
802
				$data['validate'] = $this->doValidation($name);
803
				$data['actsAs'] = $this->doActsAs($name);
804
			}
805
			$data['primaryKey'] = $name->primaryKey;
806
			$data['useTable'] = $name->table;
807
			$data['useDbConfig'] = $name->useDbConfig;
808
			$data['name'] = $name = $name->name;
809
		} else {
810
			$data['name'] = $name;
811
		}
812
813
		$defaults = array(
814
			'associations' => array(),
815
			'actsAs' => array(),
816
			'validate' => array(),
817
			'primaryKey' => 'id',
818
			'useTable' => null,
819
			'useDbConfig' => 'default',
820
			'displayField' => null
821
		);
822
		$data = array_merge($defaults, $data);
823
824
		$pluginPath = '';
825
		if ($this->plugin) {
826
			$pluginPath = $this->plugin . '.';
827
		}
828
829
		$this->Template->set($data);
830
		$this->Template->set(array(
831
			'plugin' => $this->plugin,
832
			'pluginPath' => $pluginPath
833
		));
834
		$out = $this->Template->generate('classes', 'model');
835
836
		$path = $this->getPath();
837
		$filename = $path . $name . '.php';
838
		$this->out("\n" . __d('cake_console', 'Baking model class for %s...', $name), 1, Shell::QUIET);
839
		$this->createFile($filename, $out);
840
		ClassRegistry::flush();
841
		return $out;
842
	}
843
844
/**
845
 * Assembles and writes a unit test file
846
 *
847
 * @param string $className Model class name
848
 * @return string
849
 */
850 View Code Duplication
	public function bakeTest($className) {
851
		$this->Test->interactive = $this->interactive;
852
		$this->Test->plugin = $this->plugin;
853
		$this->Test->connection = $this->connection;
854
		return $this->Test->bake('Model', $className);
855
	}
856
857
/**
858
 * outputs the a list of possible models or controllers from database
859
 *
860
 * @param string $useDbConfig Database configuration name
861
 * @return array
862
 */
863
	public function listAll($useDbConfig = null) {
864
		$this->_tables = $this->getAllTables($useDbConfig);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->getAllTables($useDbConfig) 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...
865
866
		$this->_modelNames = array();
867
		$count = count($this->_tables);
868
		for ($i = 0; $i < $count; $i++) {
869
			$this->_modelNames[] = $this->_modelName($this->_tables[$i]);
870
		}
871
		if ($this->interactive === true) {
872
			$this->out(__d('cake_console', 'Possible Models based on your current database:'));
873
			$len = strlen($count + 1);
874
			for ($i = 0; $i < $count; $i++) {
875
				$this->out(sprintf("%${len}d. %s", $i + 1, $this->_modelNames[$i]));
876
			}
877
		}
878
		return $this->_tables;
879
	}
880
881
/**
882
 * Interact with the user to determine the table name of a particular model
883
 *
884
 * @param string $modelName Name of the model you want a table for.
885
 * @param string $useDbConfig Name of the database config you want to get tables from.
886
 * @return string Table name
887
 */
888
	public function getTable($modelName, $useDbConfig = null) {
889
		$useTable = Inflector::tableize($modelName);
890
		if (in_array($modelName, $this->_modelNames)) {
891
			$modelNames = array_flip($this->_modelNames);
892
			$useTable = $this->_tables[$modelNames[$modelName]];
893
		}
894
895
		if ($this->interactive === true) {
896
			if (!isset($useDbConfig)) {
897
				$useDbConfig = $this->connection;
898
			}
899
			$db = ConnectionManager::getDataSource($useDbConfig);
900
			$fullTableName = $db->fullTableName($useTable, false);
901
			$tableIsGood = false;
902
			if (array_search($useTable, $this->_tables) === false) {
903
				$this->out();
904
				$this->out(__d('cake_console', "Given your model named '%s',\nCake would expect a database table named '%s'", $modelName, $fullTableName));
905
				$tableIsGood = $this->in(__d('cake_console', 'Do you want to use this table?'), array('y', 'n'), 'y');
906
			}
907
			if (strtolower($tableIsGood) === 'n') {
908
				$useTable = $this->in(__d('cake_console', 'What is the name of the table?'));
909
			}
910
		}
911
		return $useTable;
912
	}
913
914
/**
915
 * Get an Array of all the tables in the supplied connection
916
 * will halt the script if no tables are found.
917
 *
918
 * @param string $useDbConfig Connection name to scan.
919
 * @return array Array of tables in the database.
920
 */
921
	public function getAllTables($useDbConfig = null) {
922
		if (!isset($useDbConfig)) {
923
			$useDbConfig = $this->connection;
924
		}
925
926
		$tables = array();
927
		$db = ConnectionManager::getDataSource($useDbConfig);
928
		$db->cacheSources = false;
929
		$usePrefix = empty($db->config['prefix']) ? '' : $db->config['prefix'];
930
		if ($usePrefix) {
931
			foreach ($db->listSources() as $table) {
932
				if (!strncmp($table, $usePrefix, strlen($usePrefix))) {
933
					$tables[] = substr($table, strlen($usePrefix));
934
				}
935
			}
936
		} else {
937
			$tables = $db->listSources();
938
		}
939
		if (empty($tables)) {
940
			$this->err(__d('cake_console', 'Your database does not have any tables.'));
941
			return $this->_stop();
942
		}
943
		sort($tables);
944
		return $tables;
945
	}
946
947
/**
948
 * Forces the user to specify the model he wants to bake, and returns the selected model name.
949
 *
950
 * @param string $useDbConfig Database config name
951
 * @return string The model name
952
 */
953
	public function getName($useDbConfig = null) {
954
		$this->listAll($useDbConfig);
955
956
		$enteredModel = '';
957
958
		while (!$enteredModel) {
959
			$enteredModel = $this->in(__d('cake_console', "Enter a number from the list above,\n" .
960
				"type in the name of another model, or 'q' to exit"), null, 'q');
961
962
			if ($enteredModel === 'q') {
963
				$this->out(__d('cake_console', 'Exit'));
964
				return $this->_stop();
965
			}
966
967
			if (!$enteredModel || intval($enteredModel) > count($this->_modelNames)) {
968
				$this->err(__d('cake_console', "The model name you supplied was empty,\n" .
969
					"or the number you selected was not an option. Please try again."));
970
				$enteredModel = '';
971
			}
972
		}
973
		if (intval($enteredModel) > 0 && intval($enteredModel) <= count($this->_modelNames)) {
974
			return $this->_modelNames[intval($enteredModel) - 1];
975
		}
976
977
		return $enteredModel;
978
	}
979
980
/**
981
 * get the option parser.
982
 *
983
 * @return void
984
 */
985
	public function getOptionParser() {
986
		$parser = parent::getOptionParser();
987
		return $parser->description(
988
				__d('cake_console', 'Bake models.')
989
			)->addArgument('name', array(
990
				'help' => __d('cake_console', 'Name of the model to bake. Can use Plugin.name to bake plugin models.')
991
			))->addSubcommand('all', array(
992
				'help' => __d('cake_console', 'Bake all model files with associations and validation.')
993
			))->addOption('plugin', array(
994
				'short' => 'p',
995
				'help' => __d('cake_console', 'Plugin to bake the model into.')
996
			))->addOption('theme', array(
997
				'short' => 't',
998
				'help' => __d('cake_console', 'Theme to use when baking code.')
999
			))->addOption('connection', array(
1000
				'short' => 'c',
1001
				'help' => __d('cake_console', 'The connection the model table is on.')
1002
			))->addOption('force', array(
1003
				'short' => 'f',
1004
				'help' => __d('cake_console', 'Force overwriting existing files without prompting.')
1005
			))->epilog(__d('cake_console', 'Omitting all arguments and options will enter into an interactive mode.'));
1006
	}
1007
1008
/**
1009
 * Interact with FixtureTask to automatically bake fixtures when baking models.
1010
 *
1011
 * @param string $className Name of class to bake fixture for
1012
 * @param string $useTable Optional table name for fixture to use.
1013
 * @return void
1014
 * @see FixtureTask::bake
1015
 */
1016 View Code Duplication
	public function bakeFixture($className, $useTable = null) {
1017
		$this->Fixture->interactive = $this->interactive;
1018
		$this->Fixture->connection = $this->connection;
1019
		$this->Fixture->plugin = $this->plugin;
1020
		$this->Fixture->bake($className, $useTable);
1021
	}
1022
1023
}
1024