Completed
Push — master ( 336404...e31100 )
by Alexander
11:57
created

MigrateController::getMigrationNameLimit()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 12
ccs 7
cts 7
cp 1
rs 9.2
c 0
b 0
f 0
cc 4
eloc 7
nc 5
nop 0
crap 4
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
     * Maximum length of a migration name.
78
     * @since 2.0.13
79
     */
80
    const MAX_NAME_LENGTH = 180;
81
82
83
    /**
84
     * @var string the name of the table for keeping applied migration information.
85
     */
86
    public $migrationTable = '{{%migration}}';
87
    /**
88
     * @inheritdoc
89
     */
90
    public $templateFile = '@yii/views/migration.php';
91
    /**
92
     * @var array a set of template paths for generating migration code automatically.
93
     *
94
     * The key is the template type, the value is a path or the alias. Supported types are:
95
     * - `create_table`: table creating template
96
     * - `drop_table`: table dropping template
97
     * - `add_column`: adding new column template
98
     * - `drop_column`: dropping column template
99
     * - `create_junction`: create junction template
100
     *
101
     * @since 2.0.7
102
     */
103
    public $generatorTemplateFiles = [
104
        'create_table' => '@yii/views/createTableMigration.php',
105
        'drop_table' => '@yii/views/dropTableMigration.php',
106
        'add_column' => '@yii/views/addColumnMigration.php',
107
        'drop_column' => '@yii/views/dropColumnMigration.php',
108
        'create_junction' => '@yii/views/createTableMigration.php',
109
    ];
110
    /**
111
     * @var bool indicates whether the table names generated should consider
112
     * the `tablePrefix` setting of the DB connection. For example, if the table
113
     * name is `post` the generator wil return `{{%post}}`.
114
     * @since 2.0.8
115
     */
116
    public $useTablePrefix = false;
117
    /**
118
     * @var array column definition strings used for creating migration code.
119
     *
120
     * The format of each definition is `COLUMN_NAME:COLUMN_TYPE:COLUMN_DECORATOR`. Delimiter is `,`.
121
     * For example, `--fields="name:string(12):notNull:unique"`
122
     * produces a string column of size 12 which is not null and unique values.
123
     *
124
     * Note: primary key is added automatically and is named id by default.
125
     * If you want to use another name you may specify it explicitly like
126
     * `--fields="id_key:primaryKey,name:string(12):notNull:unique"`
127
     * @since 2.0.7
128
     */
129
    public $fields = [];
130
    /**
131
     * @var Connection|array|string the DB connection object or the application component ID of the DB connection to use
132
     * when applying migrations. Starting from version 2.0.3, this can also be a configuration array
133
     * for creating the object.
134
     */
135
    public $db = 'db';
136
137
138
    /**
139
     * @inheritdoc
140
     */
141 25
    public function options($actionID)
142
    {
143 25
        return array_merge(
144 25
            parent::options($actionID),
145 25
            ['migrationTable', 'db'], // global for all actions
146 25
            $actionID === 'create'
147 10
                ? ['templateFile', 'fields', 'useTablePrefix']
148 25
                : []
149
        );
150
    }
151
152
    /**
153
     * @inheritdoc
154
     * @since 2.0.8
155
     */
156
    public function optionAliases()
157
    {
158
        return array_merge(parent::optionAliases(), [
159
            'f' => 'fields',
160
            'p' => 'migrationPath',
161
            't' => 'migrationTable',
162
            'F' => 'templateFile',
163
            'P' => 'useTablePrefix',
164
            'c' => 'compact',
165
        ]);
166
    }
167
168
    /**
169
     * This method is invoked right before an action is to be executed (after all possible filters.)
170
     * It checks the existence of the [[migrationPath]].
171
     * @param \yii\base\Action $action the action to be executed.
172
     * @return bool whether the action should continue to be executed.
173
     */
174 36
    public function beforeAction($action)
175
    {
176 36
        if (parent::beforeAction($action)) {
177 36
            $this->db = Instance::ensure($this->db, Connection::className());
178 36
            return true;
179
        }
180
181
        return false;
182
    }
183
184
    /**
185
     * Creates a new migration instance.
186
     * @param string $class the migration class name
187
     * @return \yii\db\Migration the migration instance
188
     */
189 22
    protected function createMigration($class)
190
    {
191 22
        $this->includeMigrationFile($class);
192
193 22
        return Yii::createObject([
194 22
            'class' => $class,
195 22
            'db' => $this->db,
196 22
            'compact' => $this->compact,
197
        ]);
198
    }
199
200
    /**
201
     * @inheritdoc
202
     */
203 27
    protected function getMigrationHistory($limit)
204
    {
205 27
        if ($this->db->schema->getTableSchema($this->migrationTable, true) === null) {
206 20
            $this->createMigrationHistoryTable();
207
        }
208 27
        $query = (new Query())
209 27
            ->select(['version', 'apply_time'])
210 27
            ->from($this->migrationTable)
211 27
            ->orderBy(['apply_time' => SORT_DESC, 'version' => SORT_DESC]);
212
213 27
        if (empty($this->migrationNamespaces)) {
214 20
            $query->limit($limit);
215 20
            $rows = $query->all($this->db);
216 20
            $history = ArrayHelper::map($rows, 'version', 'apply_time');
217 20
            unset($history[self::BASE_MIGRATION]);
218 20
            return $history;
219
        }
220
221 7
        $rows = $query->all($this->db);
222
223 7
        $history = [];
224 7
        foreach ($rows as $key => $row) {
225 7
            if ($row['version'] === self::BASE_MIGRATION) {
226 7
                continue;
227
            }
228 4
            if (preg_match('/m?(\d{6}_?\d{6})(\D.*)?$/is', $row['version'], $matches)) {
229 4
                $time = str_replace('_', '', $matches[1]);
230 4
                $row['canonicalVersion'] = $time;
231
            } else {
232
                $row['canonicalVersion'] = $row['version'];
233
            }
234 4
            $row['apply_time'] = (int) $row['apply_time'];
235 4
            $history[] = $row;
236
        }
237
238 7
        usort($history, function ($a, $b) {
239 4
            if ($a['apply_time'] === $b['apply_time']) {
240 4
                if (($compareResult = strcasecmp($b['canonicalVersion'], $a['canonicalVersion'])) !== 0) {
241 2
                    return $compareResult;
242
                }
243
244 2
                return strcasecmp($b['version'], $a['version']);
245
            }
246
247 1
            return ($a['apply_time'] > $b['apply_time']) ? -1 : +1;
248 7
        });
249
250 7
        $history = array_slice($history, 0, $limit);
251
252 7
        $history = ArrayHelper::map($history, 'version', 'apply_time');
253
254 7
        return $history;
255
    }
256
257
    /**
258
     * Creates the migration history table.
259
     */
260 20
    protected function createMigrationHistoryTable()
261
    {
262 20
        $tableName = $this->db->schema->getRawTableName($this->migrationTable);
263 20
        $this->stdout("Creating migration history table \"$tableName\"...", Console::FG_YELLOW);
264 20
        $this->db->createCommand()->createTable($this->migrationTable, [
265 20
            'version' => 'varchar(' . static::MAX_NAME_LENGTH . ') NOT NULL PRIMARY KEY',
266 20
            'apply_time' => 'integer',
267 20
        ])->execute();
268 20
        $this->db->createCommand()->insert($this->migrationTable, [
269 20
            'version' => self::BASE_MIGRATION,
270 20
            'apply_time' => time(),
271 20
        ])->execute();
272 20
        $this->stdout("Done.\n", Console::FG_GREEN);
273 20
    }
274
275
    /**
276
     * @inheritdoc
277
     */
278 24
    protected function addMigrationHistory($version)
279
    {
280 24
        $command = $this->db->createCommand();
281 24
        $command->insert($this->migrationTable, [
282 24
            'version' => $version,
283 24
            'apply_time' => time(),
284 24
        ])->execute();
285 24
    }
286
287
    /**
288
     * @inheritdoc
289
     * @since 2.0.13
290
     */
291 1
    protected function truncateDatabase()
292
    {
293 1
        $db = $this->db;
294 1
        $schemas = $db->schema->getTableSchemas();
295
296
        // First drop all foreign keys,
297 1
        foreach ($schemas as $schema) {
298 1
            if ($schema->foreignKeys) {
299
                foreach ($schema->foreignKeys as $name => $foreignKey) {
300
                    $db->createCommand()->dropForeignKey($name, $schema->name)->execute();
301 1
                    $this->stdout("Foreign key $name dropped.\n");
302
                }
303
            }
304
        }
305
306
        // Then drop the tables:
307 1
        foreach ($schemas as $schema) {
308 1
            $db->createCommand()->dropTable($schema->name)->execute();
309 1
            $this->stdout("Table {$schema->name} dropped.\n");
310
        }
311 1
    }
312
313
    /**
314
     * @inheritdoc
315
     */
316 13
    protected function removeMigrationHistory($version)
317
    {
318 13
        $command = $this->db->createCommand();
319 13
        $command->delete($this->migrationTable, [
320 13
            'version' => $version,
321 13
        ])->execute();
322 13
    }
323
324
    private $_migrationNameLimit;
325
326
    /**
327
     * @inheritdoc
328
     * @since 2.0.13
329
     */
330 32
    protected function getMigrationNameLimit()
331
    {
332 32
        if ($this->_migrationNameLimit !== null) {
333 8
            return $this->_migrationNameLimit;
334
        }
335 32
        $tableSchema = $this->db->schema ? $this->db->schema->getTableSchema($this->migrationTable, true) : null;
336 32
        if ($tableSchema !== null) {
337 23
            return $this->_migrationNameLimit = $tableSchema->columns['version']->size;
338
        }
339
340 9
        return static::MAX_NAME_LENGTH;
341
    }
342
343
    /**
344
     * @inheritdoc
345
     * @since 2.0.8
346
     */
347 9
    protected function generateMigrationSourceCode($params)
348
    {
349 9
        $parsedFields = $this->parseFields();
350 9
        $fields = $parsedFields['fields'];
351 9
        $foreignKeys = $parsedFields['foreignKeys'];
352
353 9
        $name = $params['name'];
354
355 9
        $templateFile = $this->templateFile;
356 9
        $table = null;
357 9
        if (preg_match('/^create_junction(?:_table_for_|_for_|_)(.+)_and_(.+)_tables?$/', $name, $matches)) {
358 1
            $templateFile = $this->generatorTemplateFiles['create_junction'];
359 1
            $firstTable = $matches[1];
360 1
            $secondTable = $matches[2];
361
362 1
            $fields = array_merge(
363
                [
364
                    [
365 1
                        'property' => $firstTable . '_id',
366 1
                        'decorators' => 'integer()',
367
                    ],
368
                    [
369 1
                        'property' => $secondTable . '_id',
370 1
                        'decorators' => 'integer()',
371
                    ],
372
                ],
373 1
                $fields,
374
                [
375
                    [
376
                        'property' => 'PRIMARY KEY(' .
377 1
                            $firstTable . '_id, ' .
378 1
                            $secondTable . '_id)',
379
                    ],
380
                ]
381
            );
382
383 1
            $foreignKeys[$firstTable . '_id']['table'] = $firstTable;
384 1
            $foreignKeys[$secondTable . '_id']['table'] = $secondTable;
385 1
            $foreignKeys[$firstTable . '_id']['column'] = null;
386 1
            $foreignKeys[$secondTable . '_id']['column'] = null;
387 1
            $table = $firstTable . '_' . $secondTable;
388 8
        } elseif (preg_match('/^add_(.+)_columns?_to_(.+)_table$/', $name, $matches)) {
389 1
            $templateFile = $this->generatorTemplateFiles['add_column'];
390 1
            $table = $matches[2];
391 7
        } elseif (preg_match('/^drop_(.+)_columns?_from_(.+)_table$/', $name, $matches)) {
392 1
            $templateFile = $this->generatorTemplateFiles['drop_column'];
393 1
            $table = $matches[2];
394 6
        } elseif (preg_match('/^create_(.+)_table$/', $name, $matches)) {
395 1
            $this->addDefaultPrimaryKey($fields);
396 1
            $templateFile = $this->generatorTemplateFiles['create_table'];
397 1
            $table = $matches[1];
398 6
        } elseif (preg_match('/^drop_(.+)_table$/', $name, $matches)) {
399 2
            $this->addDefaultPrimaryKey($fields);
400 2
            $templateFile = $this->generatorTemplateFiles['drop_table'];
401 2
            $table = $matches[1];
402
        }
403
404 9
        foreach ($foreignKeys as $column => $foreignKey) {
405 3
            $relatedColumn = $foreignKey['column'];
406 3
            $relatedTable = $foreignKey['table'];
407
            // Since 2.0.11 if related column name is not specified,
408
            // we're trying to get it from table schema
409
            // @see https://github.com/yiisoft/yii2/issues/12748
410 3
            if ($relatedColumn === null) {
411 3
                $relatedColumn = 'id';
412
                try {
413 3
                    $this->db = Instance::ensure($this->db, Connection::className());
414 3
                    $relatedTableSchema = $this->db->getTableSchema($relatedTable);
415 3
                    if ($relatedTableSchema !== null) {
416
                        $primaryKeyCount = count($relatedTableSchema->primaryKey);
417
                        if ($primaryKeyCount === 1) {
418
                            $relatedColumn = $relatedTableSchema->primaryKey[0];
419
                        } elseif ($primaryKeyCount > 1) {
420
                            $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);
421
                        } elseif ($primaryKeyCount === 0) {
422 3
                            $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);
423
                        }
424
                    }
425
                } catch (\ReflectionException $e) {
426
                    $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);
427
                }
428
            }
429 3
            $foreignKeys[$column] = [
430 3
                'idx' => $this->generateTableName("idx-$table-$column"),
431 3
                'fk' => $this->generateTableName("fk-$table-$column"),
432 3
                'relatedTable' => $this->generateTableName($relatedTable),
433 3
                'relatedColumn' => $relatedColumn,
434
            ];
435
        }
436
437 9
        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...
438 9
            'table' => $this->generateTableName($table),
439 9
            'fields' => $fields,
440 9
            'foreignKeys' => $foreignKeys,
441
        ]));
442
    }
443
444
    /**
445
     * If `useTablePrefix` equals true, then the table name will contain the
446
     * prefix format.
447
     *
448
     * @param string $tableName the table name to generate.
449
     * @return string
450
     * @since 2.0.8
451
     */
452 9
    protected function generateTableName($tableName)
453
    {
454 9
        if (!$this->useTablePrefix) {
455 9
            return $tableName;
456
        }
457
458 2
        return '{{%' . $tableName . '}}';
459
    }
460
461
    /**
462
     * Parse the command line migration fields.
463
     * @return array parse result with following fields:
464
     *
465
     * - fields: array, parsed fields
466
     * - foreignKeys: array, detected foreign keys
467
     *
468
     * @since 2.0.7
469
     */
470 9
    protected function parseFields()
471
    {
472 9
        $fields = [];
473 9
        $foreignKeys = [];
474
475 9
        foreach ($this->fields as $index => $field) {
476 4
            $chunks = preg_split('/\s?:\s?/', $field, null);
477 4
            $property = array_shift($chunks);
478
479 4
            foreach ($chunks as $i => &$chunk) {
480 4
                if (strpos($chunk, 'foreignKey') === 0) {
481 2
                    preg_match('/foreignKey\((\w*)\s?(\w*)\)/', $chunk, $matches);
482 2
                    $foreignKeys[$property] = [
483 2
                        'table' => isset($matches[1])
484 2
                            ? $matches[1]
485 2
                            : preg_replace('/_id$/', '', $property),
486 2
                        'column' => !empty($matches[2])
487
                            ? $matches[2]
488
                            : null,
489
                    ];
490
491 2
                    unset($chunks[$i]);
492 2
                    continue;
493
                }
494
495 4
                if (!preg_match('/^(.+?)\(([^(]+)\)$/', $chunk)) {
496 4
                    $chunk .= '()';
497
                }
498
            }
499 4
            $fields[] = [
500 4
                'property' => $property,
501 4
                'decorators' => implode('->', $chunks),
502
            ];
503
        }
504
505
        return [
506 9
            'fields' => $fields,
507 9
            'foreignKeys' => $foreignKeys,
508
        ];
509
    }
510
511
    /**
512
     * Adds default primary key to fields list if there's no primary key specified.
513
     * @param array $fields parsed fields
514
     * @since 2.0.7
515
     */
516 2
    protected function addDefaultPrimaryKey(&$fields)
517
    {
518 2
        foreach ($fields as $field) {
519 2
            if (false !== strripos($field['decorators'], 'primarykey()')) {
520 2
                return;
521
            }
522
        }
523 2
        array_unshift($fields, ['property' => 'id', 'decorators' => 'primaryKey()']);
524 2
    }
525
}
526