Completed
Push — master ( 625d55...2d12e1 )
by Carsten
12:37
created

MigrateController::createMigration()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 10
ccs 6
cts 6
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 6
nc 2
nop 1
crap 2
1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\console\controllers;
9
10
use Yii;
11
use yii\db\Connection;
12
use yii\db\Query;
13
use yii\di\Instance;
14
use yii\helpers\ArrayHelper;
15
use yii\helpers\Console;
16
17
/**
18
 * Manages application migrations.
19
 *
20
 * A migration means a set of persistent changes to the application environment
21
 * that is shared among different developers. For example, in an application
22
 * backed by a database, a migration may refer to a set of changes to
23
 * the database, such as creating a new table, adding a new table column.
24
 *
25
 * This command provides support for tracking the migration history, upgrading
26
 * or downloading with migrations, and creating new migration skeletons.
27
 *
28
 * The migration history is stored in a database table named
29
 * as [[migrationTable]]. The table will be automatically created the first time
30
 * this command is executed, if it does not exist. You may also manually
31
 * create it as follows:
32
 *
33
 * ```sql
34
 * CREATE TABLE migration (
35
 *     version varchar(180) PRIMARY KEY,
36
 *     apply_time integer
37
 * )
38
 * ```
39
 *
40
 * Below are some common usages of this command:
41
 *
42
 * ```
43
 * # creates a new migration named 'create_user_table'
44
 * yii migrate/create create_user_table
45
 *
46
 * # applies ALL new migrations
47
 * yii migrate
48
 *
49
 * # reverts the last applied migration
50
 * yii migrate/down
51
 * ```
52
 *
53
 * Since 2.0.10 you can use namespaced migrations. In order to enable this feature you should configure [[migrationNamespaces]]
54
 * property for the controller at application configuration:
55
 *
56
 * ```php
57
 * return [
58
 *     'controllerMap' => [
59
 *         'migrate' => [
60
 *             'class' => 'yii\console\controllers\MigrateController',
61
 *             'migrationNamespaces' => [
62
 *                 'app\migrations',
63
 *                 'some\extension\migrations',
64
 *             ],
65
 *             //'migrationPath' => null, // allows to disable not namespaced migration completely
66
 *         ],
67
 *     ],
68
 * ];
69
 * ```
70
 *
71
 * @author Qiang Xue <[email protected]>
72
 * @since 2.0
73
 */
74
class MigrateController extends BaseMigrateController
75
{
76
    /**
77
     * @var string the name of the table for keeping applied migration information.
78
     */
79
    public $migrationTable = '{{%migration}}';
80
    /**
81
     * @inheritdoc
82
     */
83
    public $templateFile = '@yii/views/migration.php';
84
    /**
85
     * @var array a set of template paths for generating migration code automatically.
86
     *
87
     * The key is the template type, the value is a path or the alias. Supported types are:
88
     * - `create_table`: table creating template
89
     * - `drop_table`: table dropping template
90
     * - `add_column`: adding new column template
91
     * - `drop_column`: dropping column template
92
     * - `create_junction`: create junction template
93
     *
94
     * @since 2.0.7
95
     */
96
    public $generatorTemplateFiles = [
97
        'create_table' => '@yii/views/createTableMigration.php',
98
        'drop_table' => '@yii/views/dropTableMigration.php',
99
        'add_column' => '@yii/views/addColumnMigration.php',
100
        'drop_column' => '@yii/views/dropColumnMigration.php',
101
        'create_junction' => '@yii/views/createTableMigration.php',
102
    ];
103
    /**
104
     * @var bool indicates whether the table names generated should consider
105
     * the `tablePrefix` setting of the DB connection. For example, if the table
106
     * name is `post` the generator wil return `{{%post}}`.
107
     * @since 2.0.8
108
     */
109
    public $useTablePrefix = false;
110
    /**
111
     * @var array column definition strings used for creating migration code.
112
     *
113
     * The format of each definition is `COLUMN_NAME:COLUMN_TYPE:COLUMN_DECORATOR`. Delimiter is `,`.
114
     * For example, `--fields="name:string(12):notNull:unique"`
115
     * produces a string column of size 12 which is not null and unique values.
116
     *
117
     * Note: primary key is added automatically and is named id by default.
118
     * If you want to use another name you may specify it explicitly like
119
     * `--fields="id_key:primaryKey,name:string(12):notNull:unique"`
120
     * @since 2.0.7
121
     */
122
    public $fields = [];
123
    /**
124
     * @var Connection|array|string the DB connection object or the application component ID of the DB connection to use
125
     * when applying migrations. Starting from version 2.0.3, this can also be a configuration array
126
     * for creating the object.
127
     */
128
    public $db = 'db';
129
130
131
    /**
132
     * @inheritdoc
133
     */
134 22
    public function options($actionID)
135
    {
136 22
        return array_merge(
137 22
            parent::options($actionID),
138 22
            ['migrationTable', 'db'], // global for all actions
139 22
            $actionID === 'create'
140 8
                ? ['templateFile', 'fields', 'useTablePrefix']
141 22
                : []
142
        );
143
    }
144
145
    /**
146
     * @inheritdoc
147
     * @since 2.0.8
148
     */
149
    public function optionAliases()
150
    {
151
        return array_merge(parent::optionAliases(), [
152
            'f' => 'fields',
153
            'p' => 'migrationPath',
154
            't' => 'migrationTable',
155
            'F' => 'templateFile',
156
            'P' => 'useTablePrefix',
157
        ]);
158
    }
159
160
    /**
161
     * This method is invoked right before an action is to be executed (after all possible filters.)
162
     * It checks the existence of the [[migrationPath]].
163
     * @param \yii\base\Action $action the action to be executed.
164
     * @return bool whether the action should continue to be executed.
165
     */
166 30
    public function beforeAction($action)
167
    {
168 30
        if (parent::beforeAction($action)) {
169 30
            if ($action->id !== 'create') {
170 22
                $this->db = Instance::ensure($this->db, Connection::className());
171
            }
172 30
            return true;
173
        } else {
174
            return false;
175
        }
176
    }
177
178
    /**
179
     * Creates a new migration instance.
180
     * @param string $class the migration class name
181
     * @return \yii\db\Migration the migration instance
182
     */
183 19
    protected function createMigration($class)
184
    {
185 19
        $class = trim($class, '\\');
186 19
        if (strpos($class, '\\') === false) {
187 15
            $file = $this->migrationPath . DIRECTORY_SEPARATOR . $class . '.php';
188 15
            require_once($file);
189
        }
190
191 19
        return new $class(['db' => $this->db]);
192
    }
193
194
    /**
195
     * @inheritdoc
196
     */
197 22
    protected function getMigrationHistory($limit)
198
    {
199 22
        if ($this->db->schema->getTableSchema($this->migrationTable, true) === null) {
200 16
            $this->createMigrationHistoryTable();
201
        }
202 22
        $query = (new Query())
203 22
            ->select(['version', 'apply_time'])
204 22
            ->from($this->migrationTable)
205 22
            ->orderBy(['apply_time' => SORT_DESC, 'version' => SORT_DESC]);
206
207 22
        if (empty($this->migrationNamespaces)) {
208 16
            $query->limit($limit);
209 16
            $rows = $query->all($this->db);
210 16
            $history = ArrayHelper::map($rows, 'version', 'apply_time');
211 16
            unset($history[self::BASE_MIGRATION]);
212 16
            return $history;
213
        }
214
215 6
        $rows = $query->all($this->db);
216
217 6
        $history = [];
218 6
        foreach ($rows as $key => $row) {
219 6
            if ($row['version'] === self::BASE_MIGRATION) {
220 6
                continue;
221
            }
222 3
            if (preg_match('/m?(\d{6}_?\d{6})(\D.*)?$/is', $row['version'], $matches)) {
223 3
                $time = str_replace('_', '', $matches[1]);
224 3
                $row['canonicalVersion'] = $time;
225
            } else {
226
                $row['canonicalVersion'] = $row['version'];
227
            }
228 3
            $row['apply_time'] = (int)$row['apply_time'];
229 3
            $history[] = $row;
230
        }
231
232 6
        usort($history, function ($a, $b) {
233 3
            if ($a['apply_time'] === $b['apply_time']) {
234 2
                if (($compareResult = strcasecmp($b['canonicalVersion'], $a['canonicalVersion'])) !== 0) {
235 1
                    return $compareResult;
236
                }
237 1
                return strcasecmp($b['version'], $a['version']);
238
            }
239 2
            return ($a['apply_time'] > $b['apply_time']) ? -1 : +1;
240 6
        });
241
242 6
        $history = array_slice($history, 0, $limit);
243
244 6
        $history = ArrayHelper::map($history, 'version', 'apply_time');
245
246 6
        return $history;
247
    }
248
249
    /**
250
     * Creates the migration history table.
251
     */
252 16
    protected function createMigrationHistoryTable()
253
    {
254 16
        $tableName = $this->db->schema->getRawTableName($this->migrationTable);
255 16
        $this->stdout("Creating migration history table \"$tableName\"...", Console::FG_YELLOW);
256 16
        $this->db->createCommand()->createTable($this->migrationTable, [
257 16
            'version' => 'varchar(180) NOT NULL PRIMARY KEY',
258
            'apply_time' => 'integer',
259 16
        ])->execute();
260 16
        $this->db->createCommand()->insert($this->migrationTable, [
261 16
            'version' => self::BASE_MIGRATION,
262 16
            'apply_time' => time(),
263 16
        ])->execute();
264 16
        $this->stdout("Done.\n", Console::FG_GREEN);
265 16
    }
266
267
    /**
268
     * @inheritdoc
269
     */
270 21
    protected function addMigrationHistory($version)
271
    {
272 21
        $command = $this->db->createCommand();
273 21
        $command->insert($this->migrationTable, [
274 21
            'version' => $version,
275 21
            'apply_time' => time(),
276 21
        ])->execute();
277 21
    }
278
279
    /**
280
     * @inheritdoc
281
     */
282 11
    protected function removeMigrationHistory($version)
283
    {
284 11
        $command = $this->db->createCommand();
285 11
        $command->delete($this->migrationTable, [
286 11
            'version' => $version,
287 11
        ])->execute();
288 11
    }
289
290
    /**
291
     * @inheritdoc
292
     * @since 2.0.8
293
     */
294 8
    protected function generateMigrationSourceCode($params)
295
    {
296 8
        $parsedFields = $this->parseFields();
297 8
        $fields = $parsedFields['fields'];
298 8
        $foreignKeys = $parsedFields['foreignKeys'];
299
300 8
        $name = $params['name'];
301
302 8
        $templateFile = $this->templateFile;
303 8
        $table = null;
304 8
        if (preg_match('/^create_junction(?:_table_for_|_for_|_)(.+)_and_(.+)_tables?$/', $name, $matches)) {
305 1
            $templateFile = $this->generatorTemplateFiles['create_junction'];
306 1
            $firstTable = $matches[1];
307 1
            $secondTable = $matches[2];
308
309 1
            $fields = array_merge(
310
                [
311
                    [
312 1
                        'property' => $firstTable . '_id',
313 1
                        'decorators' => 'integer()',
314
                    ],
315
                    [
316 1
                        'property' => $secondTable . '_id',
317 1
                        'decorators' => 'integer()',
318
                    ],
319
                ],
320
                $fields,
321
                [
322
                    [
323
                        'property' => 'PRIMARY KEY(' .
324 1
                            $firstTable . '_id, ' .
325 1
                            $secondTable . '_id)',
326
                    ],
327
                ]
328
            );
329
330 1
            $foreignKeys[$firstTable . '_id']['table'] = $firstTable;
331 1
            $foreignKeys[$secondTable . '_id']['table'] = $secondTable;
332 1
            $foreignKeys[$firstTable . '_id']['column'] = null;
333 1
            $foreignKeys[$secondTable . '_id']['column'] = null;
334 1
            $table = $firstTable . '_' . $secondTable;
335 7
        } elseif (preg_match('/^add_(.+)_columns?_to_(.+)_table$/', $name, $matches)) {
336 1
            $templateFile = $this->generatorTemplateFiles['add_column'];
337 1
            $table = $matches[2];
338 6
        } elseif (preg_match('/^drop_(.+)_columns?_from_(.+)_table$/', $name, $matches)) {
339 1
            $templateFile = $this->generatorTemplateFiles['drop_column'];
340 1
            $table = $matches[2];
341 5
        } elseif (preg_match('/^create_(.+)_table$/', $name, $matches)) {
342 1
            $this->addDefaultPrimaryKey($fields);
343 1
            $templateFile = $this->generatorTemplateFiles['create_table'];
344 1
            $table = $matches[1];
345 5
        } elseif (preg_match('/^drop_(.+)_table$/', $name, $matches)) {
346 2
            $this->addDefaultPrimaryKey($fields);
347 2
            $templateFile = $this->generatorTemplateFiles['drop_table'];
348 2
            $table = $matches[1];
349
        }
350
351 8
        foreach ($foreignKeys as $column => $foreignKey) {
352 3
            $relatedColumn = $foreignKey['column'];
353 3
            $relatedTable = $foreignKey['table'];
354
            // Since 2.0.11 if related column name is not specified,
355
            // we're trying to get it from table schema
356
            // @see https://github.com/yiisoft/yii2/issues/12748
357 3
            if ($relatedColumn === null) {
358 3
                $relatedColumn = 'id';
359
                try {
360 3
                    $this->db = Instance::ensure($this->db, Connection::className());
361 3
                    $relatedTableSchema = $this->db->getTableSchema($relatedTable);
362 3
                    if ($relatedTableSchema !== null) {
363
                        $primaryKeyCount = count($relatedTableSchema->primaryKey);
364
                        if ($primaryKeyCount === 1) {
365
                            $relatedColumn = $relatedTableSchema->primaryKey[0];
366
                        } elseif ($primaryKeyCount > 1) {
367
                            $this->stdout("Related table for field \"{$column}\" exists, but primary key is composite. Default name \"id\" will be used for related field\n", Console::FG_YELLOW);
368
                        } elseif ($primaryKeyCount === 0) {
369
                            $this->stdout("Related table for field \"{$column}\" exists, but does not have a primary key. Default name \"id\" will be used for related field.\n", Console::FG_YELLOW);
370
                        }
371
                    }
372
                } catch (\ReflectionException $e) {
373
                    $this->stdout("Cannot initialize database component to try reading referenced table schema for field \"{$column}\". Default name \"id\" will be used for related field.\n", Console::FG_YELLOW);
374
                }
375
            }
376 3
            $foreignKeys[$column] = [
377 3
                'idx' => $this->generateTableName("idx-$table-$column"),
378 3
                'fk' => $this->generateTableName("fk-$table-$column"),
379 3
                'relatedTable' => $this->generateTableName($relatedTable),
380 3
                'relatedColumn' => $relatedColumn,
381
            ];
382
        }
383
384 8
        return $this->renderFile(Yii::getAlias($templateFile), array_merge($params, [
0 ignored issues
show
Bug introduced by
It seems like \Yii::getAlias($templateFile) targeting yii\BaseYii::getAlias() can also be of type boolean; however, yii\base\Controller::renderFile() does only seem to accept string, maybe add an additional type check?

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

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

An additional type check may prevent trouble.

Loading history...
385 8
            'table' => $this->generateTableName($table),
386 8
            'fields' => $fields,
387 8
            'foreignKeys' => $foreignKeys,
388
        ]));
389
    }
390
391
    /**
392
     * If `useTablePrefix` equals true, then the table name will contain the
393
     * prefix format.
394
     *
395
     * @param string $tableName the table name to generate.
396
     * @return string
397
     * @since 2.0.8
398
     */
399 8
    protected function generateTableName($tableName)
400
    {
401 8
        if (!$this->useTablePrefix) {
402 8
            return $tableName;
403
        }
404 2
        return '{{%' . $tableName . '}}';
405
    }
406
407
    /**
408
     * Parse the command line migration fields
409
     * @return array parse result with following fields:
410
     *
411
     * - fields: array, parsed fields
412
     * - foreignKeys: array, detected foreign keys
413
     *
414
     * @since 2.0.7
415
     */
416 8
    protected function parseFields()
417
    {
418 8
        $fields = [];
419 8
        $foreignKeys = [];
420
421 8
        foreach ($this->fields as $index => $field) {
422 4
            $chunks = preg_split('/\s?:\s?/', $field, null);
423 4
            $property = array_shift($chunks);
424
425 4
            foreach ($chunks as $i => &$chunk) {
426 4
                if (strpos($chunk, 'foreignKey') === 0) {
427 2
                    preg_match('/foreignKey\((\w*)\s?(\w*)\)/', $chunk, $matches);
428 2
                    $foreignKeys[$property] = [
429 2
                        'table' => isset($matches[1])
430 2
                            ? $matches[1]
431 2
                            : preg_replace('/_id$/', '', $property),
432 2
                        'column' => !empty($matches[2])
433
                            ? $matches[2]
434
                            : null,
435
                    ];
436
437 2
                    unset($chunks[$i]);
438 2
                    continue;
439
                }
440
441 4
                if (!preg_match('/^(.+?)\(([^(]+)\)$/', $chunk)) {
442 4
                    $chunk .= '()';
443
                }
444
            }
445 4
            $fields[] = [
446 4
                'property' => $property,
447 4
                'decorators' => implode('->', $chunks),
448
            ];
449
        }
450
451
        return [
452 8
            'fields' => $fields,
453 8
            'foreignKeys' => $foreignKeys,
454
        ];
455
    }
456
457
    /**
458
     * Adds default primary key to fields list if there's no primary key specified
459
     * @param array $fields parsed fields
460
     * @since 2.0.7
461
     */
462 2
    protected function addDefaultPrimaryKey(&$fields)
463
    {
464 2
        foreach ($fields as $field) {
465 2
            if (false !== strripos($field['decorators'], 'primarykey()')) {
466 1
                return;
467
            }
468
        }
469 2
        array_unshift($fields, ['property' => 'id', 'decorators' => 'primaryKey()']);
470 2
    }
471
}
472