統計
| ブランチ: | リビジョン:

pictcode / lib / Cake / Console / Command / Task / ModelTask.php @ 635eef61

履歴 | 表示 | アノテート | ダウンロード (32.191 KB)

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) {
146
                        $table = Inflector::tableize($className);
147
                }
148
                $object = new Model(array('name' => $className, 'table' => $table, 'ds' => $this->connection));
149
                $fields = $object->schema(true);
150
                foreach ($fields as $name => $field) {
151
                        if (isset($field['key']) && $field['key'] === 'primary') {
152
                                $object->primaryKey = $name;
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 int $default The default option for the given prompt.
165
 * @return int 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
                        if ((int)$choice > 0 && (int)$choice <= $max) {
180
                                $valid = true;
181
                        }
182
                }
183
                return $choice - 1;
184
        }
185

    
186
/**
187
 * Handles interactive baking
188
 *
189
 * @return bool
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
                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

    
352
                $fields = $model->schema();
353
                if (empty($fields)) {
354
                        return false;
355
                }
356

    
357
                $skipFields = false;
358
                $validate = array();
359
                $this->initValidations();
360
                foreach ($fields as $fieldName => $field) {
361
                        $validation = $this->fieldValidation($fieldName, $field, $model->primaryKey);
362
                        if (isset($validation['_skipFields'])) {
363
                                unset($validation['_skipFields']);
364
                                $skipFields = true;
365
                        }
366
                        if (!empty($validation)) {
367
                                $validate[$fieldName] = $validation;
368
                        }
369
                        if ($skipFields) {
370
                                return $validate;
371
                        }
372
                }
373
                return $validate;
374
        }
375

    
376
/**
377
 * Populate the _validations array
378
 *
379
 * @return void
380
 */
381
        public function initValidations() {
382
                $options = $choices = array();
383
                if (class_exists('Validation')) {
384
                        $options = get_class_methods('Validation');
385
                }
386
                $deprecatedOptions = array('notEmpty', 'between', 'ssn');
387
                $options = array_diff($options, $deprecatedOptions);
388
                sort($options);
389
                $default = 1;
390
                foreach ($options as $option) {
391
                        if ($option{0} !== '_') {
392
                                $choices[$default] = $option;
393
                                $default++;
394
                        }
395
                }
396
                $choices[$default] = 'none'; // Needed since index starts at 1
397
                $this->_validations = $choices;
398
                return $choices;
399
        }
400

    
401
/**
402
 * Does individual field validation handling.
403
 *
404
 * @param string $fieldName Name of field to be validated.
405
 * @param array $metaData metadata for field
406
 * @param string $primaryKey The primary key field.
407
 * @return array Array of validation for the field.
408
 */
409
        public function fieldValidation($fieldName, $metaData, $primaryKey = 'id') {
410
                $defaultChoice = count($this->_validations);
411
                $validate = $alreadyChosen = array();
412

    
413
                $prompt = __d('cake_console',
414
                        "or enter in a valid regex validation string.\nAlternatively [s] skip the rest of the fields.\n"
415
                );
416
                $methods = array_flip($this->_validations);
417

    
418
                $anotherValidator = 'y';
419
                while ($anotherValidator === 'y') {
420
                        if ($this->interactive) {
421
                                $this->out();
422
                                $this->out(__d('cake_console', 'Field: <info>%s</info>', $fieldName));
423
                                $this->out(__d('cake_console', 'Type: <info>%s</info>', $metaData['type']));
424
                                $this->hr();
425
                                $this->out(__d('cake_console', 'Please select one of the following validation options:'));
426
                                $this->hr();
427

    
428
                                $optionText = '';
429
                                for ($i = 1, $m = $defaultChoice / 2; $i <= $m; $i++) {
430
                                        $line = sprintf("%2d. %s", $i, $this->_validations[$i]);
431
                                        $optionText .= $line . str_repeat(" ", 31 - strlen($line));
432
                                        if ($m + $i !== $defaultChoice) {
433
                                                $optionText .= sprintf("%2d. %s\n", $m + $i, $this->_validations[$m + $i]);
434
                                        }
435
                                }
436
                                $this->out($optionText);
437
                                $this->out(__d('cake_console', "%s - Do not do any validation on this field.", $defaultChoice));
438
                                $this->hr();
439
                        }
440

    
441
                        $guess = $defaultChoice;
442
                        if ($metaData['null'] != 1 && !in_array($fieldName, array($primaryKey, 'created', 'modified', 'updated'))) {
443
                                if ($fieldName === 'email') {
444
                                        $guess = $methods['email'];
445
                                } elseif ($metaData['type'] === 'string' && $metaData['length'] == 36) {
446
                                        $guess = $methods['uuid'];
447
                                } elseif ($metaData['type'] === 'string') {
448
                                        $guess = $methods['notBlank'];
449
                                } elseif ($metaData['type'] === 'text') {
450
                                        $guess = $methods['notBlank'];
451
                                } elseif ($metaData['type'] === 'integer') {
452
                                        $guess = $methods['numeric'];
453
                                } elseif ($metaData['type'] === 'float') {
454
                                        $guess = $methods['numeric'];
455
                                } elseif ($metaData['type'] === 'boolean') {
456
                                        $guess = $methods['boolean'];
457
                                } elseif ($metaData['type'] === 'date') {
458
                                        $guess = $methods['date'];
459
                                } elseif ($metaData['type'] === 'time') {
460
                                        $guess = $methods['time'];
461
                                } elseif ($metaData['type'] === 'datetime') {
462
                                        $guess = $methods['datetime'];
463
                                } elseif ($metaData['type'] === 'inet') {
464
                                        $guess = $methods['ip'];
465
                                } elseif ($metaData['type'] === 'decimal') {
466
                                        $guess = $methods['decimal'];
467
                                }
468
                        }
469

    
470
                        if ($this->interactive === true) {
471
                                $choice = $this->in($prompt, null, $guess);
472
                                if ($choice === 's') {
473
                                        $validate['_skipFields'] = true;
474
                                        return $validate;
475
                                }
476
                                if (in_array($choice, $alreadyChosen)) {
477
                                        $this->out(__d('cake_console', "You have already chosen that validation rule,\nplease choose again"));
478
                                        continue;
479
                                }
480
                                if (!isset($this->_validations[$choice]) && is_numeric($choice)) {
481
                                        $this->out(__d('cake_console', 'Please make a valid selection.'));
482
                                        continue;
483
                                }
484
                                $alreadyChosen[] = $choice;
485
                        } else {
486
                                $choice = $guess;
487
                        }
488

    
489
                        if (isset($this->_validations[$choice])) {
490
                                $validatorName = $this->_validations[$choice];
491
                        } else {
492
                                $validatorName = Inflector::slug($choice);
493
                        }
494

    
495
                        if ($choice != $defaultChoice) {
496
                                $validate[$validatorName] = $choice;
497
                                if (is_numeric($choice) && isset($this->_validations[$choice])) {
498
                                        $validate[$validatorName] = $this->_validations[$choice];
499
                                }
500
                        }
501
                        $anotherValidator = 'n';
502
                        if ($this->interactive && $choice != $defaultChoice) {
503
                                $anotherValidator = $this->in(__d('cake_console', "Would you like to add another validation rule\n" .
504
                                        "or skip the rest of the fields?"), array('y', 'n', 's'), 'n');
505
                                if ($anotherValidator === 's') {
506
                                        $validate['_skipFields'] = true;
507
                                        return $validate;
508
                                }
509
                        }
510
                }
511
                return $validate;
512
        }
513

    
514
/**
515
 * Handles associations
516
 *
517
 * @param Model $model The model object
518
 * @return array Associations
519
 */
520
        public function doAssociations($model) {
521
                if (!$model instanceof Model) {
522
                        return false;
523
                }
524
                if ($this->interactive === true) {
525
                        $this->out(__d('cake_console', 'One moment while the associations are detected.'));
526
                }
527

    
528
                $fields = $model->schema(true);
529
                if (empty($fields)) {
530
                        return array();
531
                }
532

    
533
                if (empty($this->_tables)) {
534
                        $this->_tables = (array)$this->getAllTables();
535
                }
536

    
537
                $associations = array(
538
                        'belongsTo' => array(),
539
                        'hasMany' => array(),
540
                        'hasOne' => array(),
541
                        'hasAndBelongsToMany' => array()
542
                );
543

    
544
                $associations = $this->findBelongsTo($model, $associations);
545
                $associations = $this->findHasOneAndMany($model, $associations);
546
                $associations = $this->findHasAndBelongsToMany($model, $associations);
547

    
548
                if ($this->interactive !== true) {
549
                        unset($associations['hasOne']);
550
                }
551

    
552
                if ($this->interactive === true) {
553
                        $this->hr();
554
                        if (empty($associations)) {
555
                                $this->out(__d('cake_console', 'None found.'));
556
                        } else {
557
                                $this->out(__d('cake_console', 'Please confirm the following associations:'));
558
                                $this->hr();
559
                                $associations = $this->confirmAssociations($model, $associations);
560
                        }
561
                        $associations = $this->doMoreAssociations($model, $associations);
562
                }
563
                return $associations;
564
        }
565

    
566
/**
567
 * Handles behaviors
568
 *
569
 * @param Model $model The model object.
570
 * @return array Behaviors
571
 */
572
        public function doActsAs($model) {
573
                if (!$model instanceof Model) {
574
                        return false;
575
                }
576
                $behaviors = array();
577
                $fields = $model->schema(true);
578
                if (empty($fields)) {
579
                        return array();
580
                }
581

    
582
                if (isset($fields['lft']) && $fields['lft']['type'] === 'integer' &&
583
                        isset($fields['rght']) && $fields['rght']['type'] === 'integer' &&
584
                        isset($fields['parent_id'])) {
585
                        $behaviors[] = 'Tree';
586
                }
587
                return $behaviors;
588
        }
589

    
590
/**
591
 * Find belongsTo relations and add them to the associations list.
592
 *
593
 * @param Model $model Model instance of model being generated.
594
 * @param array $associations Array of in progress associations
595
 * @return array Associations with belongsTo added in.
596
 */
597
        public function findBelongsTo(Model $model, $associations) {
598
                $fieldNames = array_keys($model->schema(true));
599
                foreach ($fieldNames as $fieldName) {
600
                        $offset = substr($fieldName, -3) === '_id';
601
                        if ($fieldName != $model->primaryKey && $fieldName !== 'parent_id' && $offset !== false) {
602
                                $tmpModelName = $this->_modelNameFromKey($fieldName);
603
                                $associations['belongsTo'][] = array(
604
                                        'alias' => $tmpModelName,
605
                                        'className' => $tmpModelName,
606
                                        'foreignKey' => $fieldName,
607
                                );
608
                        } elseif ($fieldName === 'parent_id') {
609
                                $associations['belongsTo'][] = array(
610
                                        'alias' => 'Parent' . $model->name,
611
                                        'className' => $model->name,
612
                                        'foreignKey' => $fieldName,
613
                                );
614
                        }
615
                }
616
                return $associations;
617
        }
618

    
619
/**
620
 * Find the hasOne and hasMany relations and add them to associations list
621
 *
622
 * @param Model $model Model instance being generated
623
 * @param array $associations Array of in progress associations
624
 * @return array Associations with hasOne and hasMany added in.
625
 */
626
        public function findHasOneAndMany(Model $model, $associations) {
627
                $foreignKey = $this->_modelKey($model->name);
628
                foreach ($this->_tables as $otherTable) {
629
                        $tempOtherModel = $this->_getModelObject($this->_modelName($otherTable), $otherTable);
630
                        $tempFieldNames = array_keys($tempOtherModel->schema(true));
631

    
632
                        $pattern = '/_' . preg_quote($model->table, '/') . '|' . preg_quote($model->table, '/') . '_/';
633
                        $possibleJoinTable = preg_match($pattern, $otherTable);
634
                        if ($possibleJoinTable) {
635
                                continue;
636
                        }
637
                        foreach ($tempFieldNames as $fieldName) {
638
                                $assoc = false;
639
                                if ($fieldName !== $model->primaryKey && $fieldName === $foreignKey) {
640
                                        $assoc = array(
641
                                                'alias' => $tempOtherModel->name,
642
                                                'className' => $tempOtherModel->name,
643
                                                'foreignKey' => $fieldName
644
                                        );
645
                                } elseif ($otherTable === $model->table && $fieldName === 'parent_id') {
646
                                        $assoc = array(
647
                                                'alias' => 'Child' . $model->name,
648
                                                'className' => $model->name,
649
                                                'foreignKey' => $fieldName
650
                                        );
651
                                }
652
                                if ($assoc) {
653
                                        $associations['hasOne'][] = $assoc;
654
                                        $associations['hasMany'][] = $assoc;
655
                                }
656

    
657
                        }
658
                }
659
                return $associations;
660
        }
661

    
662
/**
663
 * Find the hasAndBelongsToMany relations and add them to associations list
664
 *
665
 * @param Model $model Model instance being generated
666
 * @param array $associations Array of in-progress associations
667
 * @return array Associations with hasAndBelongsToMany added in.
668
 */
669
        public function findHasAndBelongsToMany(Model $model, $associations) {
670
                $foreignKey = $this->_modelKey($model->name);
671
                foreach ($this->_tables as $otherTable) {
672
                        $tableName = null;
673
                        $offset = strpos($otherTable, $model->table . '_');
674
                        $otherOffset = strpos($otherTable, '_' . $model->table);
675

    
676
                        if ($offset !== false) {
677
                                $tableName = substr($otherTable, strlen($model->table . '_'));
678
                        } elseif ($otherOffset !== false) {
679
                                $tableName = substr($otherTable, 0, $otherOffset);
680
                        }
681
                        if ($tableName && in_array($tableName, $this->_tables)) {
682
                                $habtmName = $this->_modelName($tableName);
683
                                $associations['hasAndBelongsToMany'][] = array(
684
                                        'alias' => $habtmName,
685
                                        'className' => $habtmName,
686
                                        'foreignKey' => $foreignKey,
687
                                        'associationForeignKey' => $this->_modelKey($habtmName),
688
                                        'joinTable' => $otherTable
689
                                );
690
                        }
691
                }
692
                return $associations;
693
        }
694

    
695
/**
696
 * Interact with the user and confirm associations.
697
 *
698
 * @param array $model Temporary Model instance.
699
 * @param array $associations Array of associations to be confirmed.
700
 * @return array Array of confirmed associations
701
 */
702
        public function confirmAssociations(Model $model, $associations) {
703
                foreach ($associations as $type => $settings) {
704
                        if (!empty($associations[$type])) {
705
                                foreach ($associations[$type] as $i => $assoc) {
706
                                        $prompt = "{$model->name} {$type} {$assoc['alias']}?";
707
                                        $response = $this->in($prompt, array('y', 'n'), 'y');
708

    
709
                                        if (strtolower($response) === 'n') {
710
                                                unset($associations[$type][$i]);
711
                                        } elseif ($type === 'hasMany') {
712
                                                unset($associations['hasOne'][$i]);
713
                                        }
714
                                }
715
                                $associations[$type] = array_merge($associations[$type]);
716
                        }
717
                }
718
                return $associations;
719
        }
720

    
721
/**
722
 * Interact with the user and generate additional non-conventional associations
723
 *
724
 * @param Model $model Temporary model instance
725
 * @param array $associations Array of associations.
726
 * @return array Array of associations.
727
 */
728
        public function doMoreAssociations(Model $model, $associations) {
729
                $prompt = __d('cake_console', 'Would you like to define some additional model associations?');
730
                $wannaDoMoreAssoc = $this->in($prompt, array('y', 'n'), 'n');
731
                $possibleKeys = $this->_generatePossibleKeys();
732
                while (strtolower($wannaDoMoreAssoc) === 'y') {
733
                        $assocs = array('belongsTo', 'hasOne', 'hasMany', 'hasAndBelongsToMany');
734
                        $this->out(__d('cake_console', 'What is the association type?'));
735
                        $assocType = (int)$this->inOptions($assocs, __d('cake_console', 'Enter a number'));
736

    
737
                        $this->out(__d('cake_console', "For the following options be very careful to match your setup exactly.\n" .
738
                                "Any spelling mistakes will cause errors."));
739
                        $this->hr();
740

    
741
                        $alias = $this->in(__d('cake_console', 'What is the alias for this association?'));
742
                        $className = $this->in(__d('cake_console', 'What className will %s use?', $alias), null, $alias);
743

    
744
                        if ($assocType === 0) {
745
                                if (!empty($possibleKeys[$model->table])) {
746
                                        $showKeys = $possibleKeys[$model->table];
747
                                } else {
748
                                        $showKeys = null;
749
                                }
750
                                $suggestedForeignKey = $this->_modelKey($alias);
751
                        } else {
752
                                $otherTable = Inflector::tableize($className);
753
                                if (in_array($otherTable, $this->_tables)) {
754
                                        if ($assocType < 3) {
755
                                                if (!empty($possibleKeys[$otherTable])) {
756
                                                        $showKeys = $possibleKeys[$otherTable];
757
                                                } else {
758
                                                        $showKeys = null;
759
                                                }
760
                                        } else {
761
                                                $showKeys = null;
762
                                        }
763
                                } else {
764
                                        $otherTable = $this->in(__d('cake_console', 'What is the table for this model?'));
765
                                        $showKeys = $possibleKeys[$otherTable];
766
                                }
767
                                $suggestedForeignKey = $this->_modelKey($model->name);
768
                        }
769
                        if (!empty($showKeys)) {
770
                                $this->out(__d('cake_console', 'A helpful List of possible keys'));
771
                                $foreignKey = $this->inOptions($showKeys, __d('cake_console', 'What is the foreignKey?'));
772
                                $foreignKey = $showKeys[(int)$foreignKey];
773
                        }
774
                        if (!isset($foreignKey)) {
775
                                $foreignKey = $this->in(__d('cake_console', 'What is the foreignKey? Specify your own.'), null, $suggestedForeignKey);
776
                        }
777
                        if ($assocType === 3) {
778
                                $associationForeignKey = $this->in(__d('cake_console', 'What is the associationForeignKey?'), null, $this->_modelKey($model->name));
779
                                $joinTable = $this->in(__d('cake_console', 'What is the joinTable?'));
780
                        }
781
                        $associations[$assocs[$assocType]] = array_values((array)$associations[$assocs[$assocType]]);
782
                        $count = count($associations[$assocs[$assocType]]);
783
                        $i = ($count > 0) ? $count : 0;
784
                        $associations[$assocs[$assocType]][$i]['alias'] = $alias;
785
                        $associations[$assocs[$assocType]][$i]['className'] = $className;
786
                        $associations[$assocs[$assocType]][$i]['foreignKey'] = $foreignKey;
787
                        if ($assocType === 3) {
788
                                $associations[$assocs[$assocType]][$i]['associationForeignKey'] = $associationForeignKey;
789
                                $associations[$assocs[$assocType]][$i]['joinTable'] = $joinTable;
790
                        }
791
                        $wannaDoMoreAssoc = $this->in(__d('cake_console', 'Define another association?'), array('y', 'n'), 'y');
792
                }
793
                return $associations;
794
        }
795

    
796
/**
797
 * Finds all possible keys to use on custom associations.
798
 *
799
 * @return array Array of tables and possible keys
800
 */
801
        protected function _generatePossibleKeys() {
802
                $possible = array();
803
                foreach ($this->_tables as $otherTable) {
804
                        $tempOtherModel = new Model(array('table' => $otherTable, 'ds' => $this->connection));
805
                        $modelFieldsTemp = $tempOtherModel->schema(true);
806
                        foreach ($modelFieldsTemp as $fieldName => $field) {
807
                                if ($field['type'] === 'integer' || $field['type'] === 'string') {
808
                                        $possible[$otherTable][] = $fieldName;
809
                                }
810
                        }
811
                }
812
                return $possible;
813
        }
814

    
815
/**
816
 * Assembles and writes a Model file.
817
 *
818
 * @param string|object $name Model name or object
819
 * @param array|bool $data if array and $name is not an object assume bake data, otherwise boolean.
820
 * @return string
821
 */
822
        public function bake($name, $data = array()) {
823
                if ($name instanceof Model) {
824
                        if (!$data) {
825
                                $data = array();
826
                                $data['associations'] = $this->doAssociations($name);
827
                                $data['validate'] = $this->doValidation($name);
828
                                $data['actsAs'] = $this->doActsAs($name);
829
                        }
830
                        $data['primaryKey'] = $name->primaryKey;
831
                        $data['useTable'] = $name->table;
832
                        $data['useDbConfig'] = $name->useDbConfig;
833
                        $data['name'] = $name = $name->name;
834
                } else {
835
                        $data['name'] = $name;
836
                }
837

    
838
                $defaults = array(
839
                        'associations' => array(),
840
                        'actsAs' => array(),
841
                        'validate' => array(),
842
                        'primaryKey' => 'id',
843
                        'useTable' => null,
844
                        'useDbConfig' => 'default',
845
                        'displayField' => null
846
                );
847
                $data = array_merge($defaults, $data);
848

    
849
                $pluginPath = '';
850
                if ($this->plugin) {
851
                        $pluginPath = $this->plugin . '.';
852
                }
853

    
854
                $this->Template->set($data);
855
                $this->Template->set(array(
856
                        'plugin' => $this->plugin,
857
                        'pluginPath' => $pluginPath
858
                ));
859
                $out = $this->Template->generate('classes', 'model');
860

    
861
                $path = $this->getPath();
862
                $filename = $path . $name . '.php';
863
                $this->out("\n" . __d('cake_console', 'Baking model class for %s...', $name), 1, Shell::QUIET);
864
                $this->createFile($filename, $out);
865
                ClassRegistry::flush();
866
                return $out;
867
        }
868

    
869
/**
870
 * Assembles and writes a unit test file
871
 *
872
 * @param string $className Model class name
873
 * @return string
874
 */
875
        public function bakeTest($className) {
876
                $this->Test->interactive = $this->interactive;
877
                $this->Test->plugin = $this->plugin;
878
                $this->Test->connection = $this->connection;
879
                return $this->Test->bake('Model', $className);
880
        }
881

    
882
/**
883
 * outputs the a list of possible models or controllers from database
884
 *
885
 * @param string $useDbConfig Database configuration name
886
 * @return array
887
 */
888
        public function listAll($useDbConfig = null) {
889
                $this->_tables = $this->getAllTables($useDbConfig);
890

    
891
                $this->_modelNames = array();
892
                $count = count($this->_tables);
893
                for ($i = 0; $i < $count; $i++) {
894
                        $this->_modelNames[] = $this->_modelName($this->_tables[$i]);
895
                }
896
                if ($this->interactive === true) {
897
                        $this->out(__d('cake_console', 'Possible Models based on your current database:'));
898
                        $len = strlen($count + 1);
899
                        for ($i = 0; $i < $count; $i++) {
900
                                $this->out(sprintf("%${len}d. %s", $i + 1, $this->_modelNames[$i]));
901
                        }
902
                }
903
                return $this->_tables;
904
        }
905

    
906
/**
907
 * Interact with the user to determine the table name of a particular model
908
 *
909
 * @param string $modelName Name of the model you want a table for.
910
 * @param string $useDbConfig Name of the database config you want to get tables from.
911
 * @return string Table name
912
 */
913
        public function getTable($modelName, $useDbConfig = null) {
914
                $useTable = Inflector::tableize($modelName);
915
                if (in_array($modelName, $this->_modelNames)) {
916
                        $modelNames = array_flip($this->_modelNames);
917
                        $useTable = $this->_tables[$modelNames[$modelName]];
918
                }
919

    
920
                if ($this->interactive === true) {
921
                        if (!isset($useDbConfig)) {
922
                                $useDbConfig = $this->connection;
923
                        }
924
                        $db = ConnectionManager::getDataSource($useDbConfig);
925
                        $fullTableName = $db->fullTableName($useTable, false);
926
                        $tableIsGood = false;
927
                        if (array_search($useTable, $this->_tables) === false) {
928
                                $this->out();
929
                                $this->out(__d('cake_console', "Given your model named '%s',\nCake would expect a database table named '%s'", $modelName, $fullTableName));
930
                                $tableIsGood = $this->in(__d('cake_console', 'Do you want to use this table?'), array('y', 'n'), 'y');
931
                        }
932
                        if (strtolower($tableIsGood) === 'n') {
933
                                $useTable = $this->in(__d('cake_console', 'What is the name of the table (without prefix)?'));
934
                        }
935
                }
936
                return $useTable;
937
        }
938

    
939
/**
940
 * Get an Array of all the tables in the supplied connection
941
 * will halt the script if no tables are found.
942
 *
943
 * @param string $useDbConfig Connection name to scan.
944
 * @return array Array of tables in the database.
945
 */
946
        public function getAllTables($useDbConfig = null) {
947
                if (!isset($useDbConfig)) {
948
                        $useDbConfig = $this->connection;
949
                }
950

    
951
                $tables = array();
952
                $db = ConnectionManager::getDataSource($useDbConfig);
953
                $db->cacheSources = false;
954
                $usePrefix = empty($db->config['prefix']) ? '' : $db->config['prefix'];
955
                if ($usePrefix) {
956
                        foreach ($db->listSources() as $table) {
957
                                if (!strncmp($table, $usePrefix, strlen($usePrefix))) {
958
                                        $tables[] = substr($table, strlen($usePrefix));
959
                                }
960
                        }
961
                } else {
962
                        $tables = $db->listSources();
963
                }
964
                if (empty($tables)) {
965
                        $this->err(__d('cake_console', 'Your database does not have any tables.'));
966
                        return $this->_stop();
967
                }
968
                sort($tables);
969
                return $tables;
970
        }
971

    
972
/**
973
 * Forces the user to specify the model he wants to bake, and returns the selected model name.
974
 *
975
 * @param string $useDbConfig Database config name
976
 * @return string The model name
977
 */
978
        public function getName($useDbConfig = null) {
979
                $this->listAll($useDbConfig);
980

    
981
                $enteredModel = '';
982

    
983
                while (!$enteredModel) {
984
                        $enteredModel = $this->in(__d('cake_console', "Enter a number from the list above,\n" .
985
                                "type in the name of another model, or 'q' to exit"), null, 'q');
986

    
987
                        if ($enteredModel === 'q') {
988
                                $this->out(__d('cake_console', 'Exit'));
989
                                return $this->_stop();
990
                        }
991

    
992
                        if (!$enteredModel || (int)$enteredModel > count($this->_modelNames)) {
993
                                $this->err(__d('cake_console', "The model name you supplied was empty,\n" .
994
                                        "or the number you selected was not an option. Please try again."));
995
                                $enteredModel = '';
996
                        }
997
                }
998
                if ((int)$enteredModel > 0 && (int)$enteredModel <= count($this->_modelNames)) {
999
                        return $this->_modelNames[(int)$enteredModel - 1];
1000
                }
1001

    
1002
                return $enteredModel;
1003
        }
1004

    
1005
/**
1006
 * Gets the option parser instance and configures it.
1007
 *
1008
 * @return ConsoleOptionParser
1009
 */
1010
        public function getOptionParser() {
1011
                $parser = parent::getOptionParser();
1012

    
1013
                $parser->description(
1014
                        __d('cake_console', 'Bake models.')
1015
                )->addArgument('name', array(
1016
                        'help' => __d('cake_console', 'Name of the model to bake. Can use Plugin.name to bake plugin models.')
1017
                ))->addSubcommand('all', array(
1018
                        'help' => __d('cake_console', 'Bake all model files with associations and validation.')
1019
                ))->addOption('plugin', array(
1020
                        'short' => 'p',
1021
                        'help' => __d('cake_console', 'Plugin to bake the model into.')
1022
                ))->addOption('theme', array(
1023
                        'short' => 't',
1024
                        'help' => __d('cake_console', 'Theme to use when baking code.')
1025
                ))->addOption('connection', array(
1026
                        'short' => 'c',
1027
                        'help' => __d('cake_console', 'The connection the model table is on.')
1028
                ))->addOption('force', array(
1029
                        'short' => 'f',
1030
                        'help' => __d('cake_console', 'Force overwriting existing files without prompting.')
1031
                ))->epilog(
1032
                        __d('cake_console', 'Omitting all arguments and options will enter into an interactive mode.')
1033
                );
1034

    
1035
                return $parser;
1036
        }
1037

    
1038
/**
1039
 * Interact with FixtureTask to automatically bake fixtures when baking models.
1040
 *
1041
 * @param string $className Name of class to bake fixture for
1042
 * @param string $useTable Optional table name for fixture to use.
1043
 * @return void
1044
 * @see FixtureTask::bake
1045
 */
1046
        public function bakeFixture($className, $useTable = null) {
1047
                $this->Fixture->interactive = $this->interactive;
1048
                $this->Fixture->connection = $this->connection;
1049
                $this->Fixture->plugin = $this->plugin;
1050
                $this->Fixture->bake($className, $useTable);
1051
        }
1052

    
1053
}