Completed
Push — 2.1-master-merge ( 240673 )
by Alexander
13:45
created

MigrateController::truncateDatabase()   B

Complexity

Conditions 5
Paths 8

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 5.1158

Importance

Changes 0
Metric Value
dl 0
loc 21
ccs 10
cts 12
cp 0.8333
rs 8.7624
c 0
b 0
f 0
cc 5
eloc 11
nc 8
nop 0
crap 5.1158
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 24
    public function options($actionID)
135
    {
136 24
        return array_merge(
137 24
            parent::options($actionID),
138 24
            ['migrationTable', 'db'], // global for all actions
139 24
            $actionID === 'create'
140 9
                ? ['templateFile', 'fields', 'useTablePrefix']
141 24
                : []
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
            'c' => 'compact',
158
        ]);
159
    }
160
161
    /**
162
     * This method is invoked right before an action is to be executed (after all possible filters.)
163
     * It checks the existence of the [[migrationPath]].
164
     * @param \yii\base\Action $action the action to be executed.
165
     * @return bool whether the action should continue to be executed.
166
     */
167 33
    public function beforeAction($action)
168
    {
169 33
        if (parent::beforeAction($action)) {
170 33
            if ($action->id !== 'create') {
171 25
                $this->db = Instance::ensure($this->db, Connection::class);
172
            }
173
174 33
            return true;
175
        }
176
177
        return false;
178
    }
179
180
    /**
181
     * Creates a new migration instance.
182
     * @param string $class the migration class name
183
     * @return \yii\db\Migration the migration instance
184
     */
185 21
    protected function createMigration($class)
186
    {
187 21
        $this->includeMigrationFile($class);
188 21
        return new $class(['db' => $this->db, 'compact' => $this->compact]);
189
    }
190
191
    /**
192
     * @inheritdoc
193
     */
194 25
    protected function getMigrationHistory($limit)
195
    {
196 25
        if ($this->db->schema->getTableSchema($this->migrationTable, true) === null) {
197 19
            $this->createMigrationHistoryTable();
198
        }
199 25
        $query = (new Query())
200 25
            ->select(['version', 'apply_time'])
201 25
            ->from($this->migrationTable)
202 25
            ->orderBy(['apply_time' => SORT_DESC, 'version' => SORT_DESC]);
203
204 25
        if (empty($this->migrationNamespaces)) {
205 18
            $query->limit($limit);
206 18
            $rows = $query->all($this->db);
207 18
            $history = ArrayHelper::map($rows, 'version', 'apply_time');
208 18
            unset($history[self::BASE_MIGRATION]);
209 18
            return $history;
210
        }
211
212 7
        $rows = $query->all($this->db);
213
214 7
        $history = [];
215 7
        foreach ($rows as $key => $row) {
216 7
            if ($row['version'] === self::BASE_MIGRATION) {
217 7
                continue;
218
            }
219 4
            if (preg_match('/m?(\d{6}_?\d{6})(\D.*)?$/is', $row['version'], $matches)) {
220 4
                $time = str_replace('_', '', $matches[1]);
221 4
                $row['canonicalVersion'] = $time;
222
            } else {
223
                $row['canonicalVersion'] = $row['version'];
224
            }
225 4
            $row['apply_time'] = (int) $row['apply_time'];
226 4
            $history[] = $row;
227
        }
228
229 7
        usort($history, function ($a, $b) {
230 4
            if ($a['apply_time'] === $b['apply_time']) {
231 4
                if (($compareResult = strcasecmp($b['canonicalVersion'], $a['canonicalVersion'])) !== 0) {
232 2
                    return $compareResult;
233
                }
234
235 2
                return strcasecmp($b['version'], $a['version']);
236
            }
237
238 1
            return ($a['apply_time'] > $b['apply_time']) ? -1 : +1;
239 7
        });
240
241 7
        $history = array_slice($history, 0, $limit);
242
243 7
        $history = ArrayHelper::map($history, 'version', 'apply_time');
244
245 7
        return $history;
246
    }
247
248
    /**
249
     * Creates the migration history table.
250
     */
251 19
    protected function createMigrationHistoryTable()
252
    {
253 19
        $tableName = $this->db->schema->getRawTableName($this->migrationTable);
254 19
        $this->stdout("Creating migration history table \"$tableName\"...", Console::FG_YELLOW);
255 19
        $this->db->createCommand()->createTable($this->migrationTable, [
256 19
            'version' => 'varchar(180) NOT NULL PRIMARY KEY',
257
            'apply_time' => 'integer',
258 19
        ])->execute();
259 19
        $this->db->createCommand()->insert($this->migrationTable, [
260 19
            'version' => self::BASE_MIGRATION,
261 19
            'apply_time' => time(),
262 19
        ])->execute();
263 19
        $this->stdout("Done.\n", Console::FG_GREEN);
264 19
    }
265
266
    /**
267
     * @inheritdoc
268
     */
269 23
    protected function addMigrationHistory($version)
270
    {
271 23
        $command = $this->db->createCommand();
272 23
        $command->insert($this->migrationTable, [
273 23
            'version' => $version,
274 23
            'apply_time' => time(),
275 23
        ])->execute();
276 23
    }
277
278
    /**
279
     * @inheritdoc
280
     * @since 2.0.13
281
     */
282 1
    protected function truncateDatabase()
283
    {
284 1
        $db = $this->db;
285 1
        $schemas = $db->schema->getTableSchemas();
286
287
        // First drop all foreign keys,
288 1
        foreach ($schemas as $schema) {
289 1
            if ($schema->foreignKeys) {
290
                foreach ($schema->foreignKeys as $name => $foreignKey) {
291
                    $db->createCommand()->dropForeignKey($name, $schema->name)->execute();
292 1
                    $this->stdout("Foreign key $name dropped.\n");
293
                }
294
            }
295
        }
296
297
        // Then drop the tables:
298 1
        foreach ($schemas as $schema) {
299 1
            $db->createCommand()->dropTable($schema->name)->execute();
300 1
            $this->stdout("Table {$schema->name} dropped.\n");
301
        }
302 1
    }
303
304
    /**
305
     * @inheritdoc
306
     */
307 13
    protected function removeMigrationHistory($version)
308
    {
309 13
        $command = $this->db->createCommand();
310 13
        $command->delete($this->migrationTable, [
311 13
            'version' => $version,
312 13
        ])->execute();
313 13
    }
314
315
    /**
316
     * @inheritdoc
317
     * @since 2.0.8
318
     */
319 9
    protected function generateMigrationSourceCode($params)
320
    {
321 9
        $parsedFields = $this->parseFields();
322 9
        $fields = $parsedFields['fields'];
323 9
        $foreignKeys = $parsedFields['foreignKeys'];
324
325 9
        $name = $params['name'];
326
327 9
        $templateFile = $this->templateFile;
328 9
        $table = null;
329 9
        if (preg_match('/^create_junction(?:_table_for_|_for_|_)(.+)_and_(.+)_tables?$/', $name, $matches)) {
330 1
            $templateFile = $this->generatorTemplateFiles['create_junction'];
331 1
            $firstTable = $matches[1];
332 1
            $secondTable = $matches[2];
333
334 1
            $fields = array_merge(
335
                [
336
                    [
337 1
                        'property' => $firstTable . '_id',
338 1
                        'decorators' => 'integer()',
339
                    ],
340
                    [
341 1
                        'property' => $secondTable . '_id',
342 1
                        'decorators' => 'integer()',
343
                    ],
344
                ],
345 1
                $fields,
346
                [
347
                    [
348
                        'property' => 'PRIMARY KEY(' .
349 1
                            $firstTable . '_id, ' .
350 1
                            $secondTable . '_id)',
351
                    ],
352
                ]
353
            );
354
355 1
            $foreignKeys[$firstTable . '_id']['table'] = $firstTable;
356 1
            $foreignKeys[$secondTable . '_id']['table'] = $secondTable;
357 1
            $foreignKeys[$firstTable . '_id']['column'] = null;
358 1
            $foreignKeys[$secondTable . '_id']['column'] = null;
359 1
            $table = $firstTable . '_' . $secondTable;
360 8
        } elseif (preg_match('/^add_(.+)_columns?_to_(.+)_table$/', $name, $matches)) {
361 1
            $templateFile = $this->generatorTemplateFiles['add_column'];
362 1
            $table = $matches[2];
363 7
        } elseif (preg_match('/^drop_(.+)_columns?_from_(.+)_table$/', $name, $matches)) {
364 1
            $templateFile = $this->generatorTemplateFiles['drop_column'];
365 1
            $table = $matches[2];
366 6
        } elseif (preg_match('/^create_(.+)_table$/', $name, $matches)) {
367 1
            $this->addDefaultPrimaryKey($fields);
368 1
            $templateFile = $this->generatorTemplateFiles['create_table'];
369 1
            $table = $matches[1];
370 6
        } elseif (preg_match('/^drop_(.+)_table$/', $name, $matches)) {
371 2
            $this->addDefaultPrimaryKey($fields);
372 2
            $templateFile = $this->generatorTemplateFiles['drop_table'];
373 2
            $table = $matches[1];
374
        }
375
376 9
        foreach ($foreignKeys as $column => $foreignKey) {
377 3
            $relatedColumn = $foreignKey['column'];
378 3
            $relatedTable = $foreignKey['table'];
379
            // Since 2.0.11 if related column name is not specified,
380
            // we're trying to get it from table schema
381
            // @see https://github.com/yiisoft/yii2/issues/12748
382 3
            if ($relatedColumn === null) {
383 3
                $relatedColumn = 'id';
384
                try {
385 3
                    $this->db = Instance::ensure($this->db, Connection::class);
386 3
                    $relatedTableSchema = $this->db->getTableSchema($relatedTable);
387 3
                    if ($relatedTableSchema !== null) {
388
                        $primaryKeyCount = count($relatedTableSchema->primaryKey);
389
                        if ($primaryKeyCount === 1) {
390
                            $relatedColumn = $relatedTableSchema->primaryKey[0];
391
                        } elseif ($primaryKeyCount > 1) {
392
                            $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);
393
                        } elseif ($primaryKeyCount === 0) {
394 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);
395
                        }
396
                    }
397
                } catch (\ReflectionException $e) {
398
                    $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);
399
                }
400
            }
401 3
            $foreignKeys[$column] = [
402 3
                'idx' => $this->generateTableName("idx-$table-$column"),
403 3
                'fk' => $this->generateTableName("fk-$table-$column"),
404 3
                'relatedTable' => $this->generateTableName($relatedTable),
405 3
                'relatedColumn' => $relatedColumn,
406
            ];
407
        }
408
409 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...
410 9
            'table' => $this->generateTableName($table),
411 9
            'fields' => $fields,
412 9
            'foreignKeys' => $foreignKeys,
413
        ]));
414
    }
415
416
    /**
417
     * If `useTablePrefix` equals true, then the table name will contain the
418
     * prefix format.
419
     *
420
     * @param string $tableName the table name to generate.
421
     * @return string
422
     * @since 2.0.8
423
     */
424 9
    protected function generateTableName($tableName)
425
    {
426 9
        if (!$this->useTablePrefix) {
427 9
            return $tableName;
428
        }
429
430 2
        return '{{%' . $tableName . '}}';
431
    }
432
433
    /**
434
     * Parse the command line migration fields.
435
     * @return array parse result with following fields:
436
     *
437
     * - fields: array, parsed fields
438
     * - foreignKeys: array, detected foreign keys
439
     *
440
     * @since 2.0.7
441
     */
442 9
    protected function parseFields()
443
    {
444 9
        $fields = [];
445 9
        $foreignKeys = [];
446
447 9
        foreach ($this->fields as $index => $field) {
448 4
            $chunks = preg_split('/\s?:\s?/', $field, null);
449 4
            $property = array_shift($chunks);
450
451 4
            foreach ($chunks as $i => &$chunk) {
452 4
                if (strpos($chunk, 'foreignKey') === 0) {
453 2
                    preg_match('/foreignKey\((\w*)\s?(\w*)\)/', $chunk, $matches);
454 2
                    $foreignKeys[$property] = [
455 2
                        'table' => isset($matches[1])
456 2
                            ? $matches[1]
457 2
                            : preg_replace('/_id$/', '', $property),
458 2
                        'column' => !empty($matches[2])
459
                            ? $matches[2]
460
                            : null,
461
                    ];
462
463 2
                    unset($chunks[$i]);
464 2
                    continue;
465
                }
466
467 4
                if (!preg_match('/^(.+?)\(([^(]+)\)$/', $chunk)) {
468 4
                    $chunk .= '()';
469
                }
470
            }
471 4
            $fields[] = [
472 4
                'property' => $property,
473 4
                'decorators' => implode('->', $chunks),
474
            ];
475
        }
476
477
        return [
478 9
            'fields' => $fields,
479 9
            'foreignKeys' => $foreignKeys,
480
        ];
481
    }
482
483
    /**
484
     * Adds default primary key to fields list if there's no primary key specified.
485
     * @param array $fields parsed fields
486
     * @since 2.0.7
487
     */
488 2
    protected function addDefaultPrimaryKey(&$fields)
489
    {
490 2
        foreach ($fields as $field) {
491 2
            if (false !== strripos($field['decorators'], 'primarykey()')) {
492 2
                return;
493
            }
494
        }
495 2
        array_unshift($fields, ['property' => 'id', 'decorators' => 'primaryKey()']);
496 2
    }
497
}
498