Completed
Pull Request — master (#14017)
by Dmitry
14:29 queued 11s
created

BaseMigrateController::actionRedo()   C

Complexity

Conditions 13
Paths 35

Size

Total Lines 47
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 13

Importance

Changes 0
Metric Value
dl 0
loc 47
ccs 20
cts 20
cp 1
rs 5.0999
c 0
b 0
f 0
cc 13
eloc 28
nc 35
nop 1
crap 13

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * @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\base\InvalidConfigException;
12
use yii\console\Exception;
13
use yii\console\Controller;
14
use yii\helpers\Console;
15
use yii\helpers\FileHelper;
16
17
/**
18
 * BaseMigrateController is the base class for migrate controllers.
19
 *
20
 * @author Qiang Xue <[email protected]>
21
 * @since 2.0
22
 */
23
abstract class BaseMigrateController extends Controller
24
{
25
    /**
26
     * The name of the dummy migration that marks the beginning of the whole migration history.
27
     */
28
    const BASE_MIGRATION = 'm000000_000000_base';
29
30
    /**
31
     * @var string the default command action.
32
     */
33
    public $defaultAction = 'up';
34
    /**
35
     * @var string the directory containing the migration classes. This can be either
36
     * a path alias or a directory path.
37
     *
38
     * Migration classes located at this path should be declared without a namespace.
39
     * Use [[migrationNamespaces]] property in case you are using namespaced migrations.
40
     *
41
     * If you have set up [[migrationNamespaces]], you may set this field to `null` in order
42
     * to disable usage of migrations that are not namespaced.
43
     */
44
    public $migrationPath = '@app/migrations';
45
    /**
46
     * @var array list of namespaces containing the migration classes.
47
     *
48
     * Migration namespaces should be resolvable as a path alias if prefixed with `@`, e.g. if you specify
49
     * the namespace `app\migrations`, the code `Yii::getAlias('@app/migrations')` should be able to return
50
     * the file path to the directory this namespace refers to.
51
     *
52
     * For example:
53
     *
54
     * ```php
55
     * [
56
     *     'app\migrations',
57
     *     'some\extension\migrations',
58
     * ]
59
     * ```
60
     *
61
     * @since 2.0.10
62
     */
63
    public $migrationNamespaces = [];
64
    /**
65
     * @var string the template file for generating new migrations.
66
     * This can be either a path alias (e.g. "@app/migrations/template.php")
67
     * or a file path.
68
     */
69
    public $templateFile;
70
71
72
    /**
73
     * @inheritdoc
74
     */
75 16
    public function options($actionID)
76
    {
77 16
        return array_merge(
78 16
            parent::options($actionID),
79 16
            ['migrationPath', 'migrationNamespaces'], // global for all actions
80 16
            $actionID === 'create' ? ['templateFile'] : [] // action create
81
        );
82
    }
83
84
    /**
85
     * This method is invoked right before an action is to be executed (after all possible filters.)
86
     * It checks the existence of the [[migrationPath]].
87
     * @param \yii\base\Action $action the action to be executed.
88
     * @throws InvalidConfigException if directory specified in migrationPath doesn't exist and action isn't "create".
89
     * @return bool whether the action should continue to be executed.
90
     */
91 24
    public function beforeAction($action)
92
    {
93 24
        if (parent::beforeAction($action)) {
94 24
            if (empty($this->migrationNamespaces) && empty($this->migrationPath)) {
95
                throw new InvalidConfigException('At least one of `migrationPath` or `migrationNamespaces` should be specified.');
96
            }
97
98 24
            foreach ($this->migrationNamespaces as $key => $value) {
99 7
                $this->migrationNamespaces[$key] = trim($value, '\\');
100
            }
101
102 24
            if ($this->migrationPath !== null) {
103 18
                $path = Yii::getAlias($this->migrationPath);
104 18
                if (!is_dir($path)) {
105 5
                    if ($action->id !== 'create') {
106
                        throw new InvalidConfigException("Migration failed. Directory specified in migrationPath doesn't exist: {$this->migrationPath}");
107
                    }
108 5
                    FileHelper::createDirectory($path);
0 ignored issues
show
Bug introduced by
It seems like $path defined by \Yii::getAlias($this->migrationPath) on line 103 can also be of type boolean; however, yii\helpers\BaseFileHelper::createDirectory() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
109
                }
110 18
                $this->migrationPath = $path;
0 ignored issues
show
Documentation Bug introduced by
It seems like $path can also be of type boolean. However, the property $migrationPath is declared as type string. Maybe add an additional type check?

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
111
            }
112
113 24
            $version = Yii::getVersion();
114 24
            $this->stdout("Yii Migration Tool (based on Yii v{$version})\n\n");
115
116 24
            return true;
117
        } else {
118
            return false;
119
        }
120
    }
121
122
    /**
123
     * Upgrades the application by applying new migrations.
124
     * For example,
125
     *
126
     * ```
127
     * yii migrate     # apply all new migrations
128
     * yii migrate 3   # apply the first 3 new migrations
129
     * ```
130
     *
131
     * @param int $limit the number of new migrations to be applied. If 0, it means
132
     * applying all available new migrations.
133
     *
134
     * @return int the status of the action execution. 0 means normal, other values mean abnormal.
135
     */
136 13
    public function actionUp($limit = 0)
137
    {
138 13
        $migrations = $this->getNewMigrations();
139 13
        if (empty($migrations)) {
140
            $this->stdout("No new migrations found. Your system is up-to-date.\n", Console::FG_GREEN);
141
142
            return self::EXIT_CODE_NORMAL;
143
        }
144
145 13
        $total = count($migrations);
146 13
        $limit = (int) $limit;
147 13
        if ($limit > 0) {
148 3
            $migrations = array_slice($migrations, 0, $limit);
149
        }
150
151 13
        $n = count($migrations);
152 13
        if ($n === $total) {
153 12
            $this->stdout("Total $n new " . ($n === 1 ? 'migration' : 'migrations') . " to be applied:\n", Console::FG_YELLOW);
154
        } else {
155 1
            $this->stdout("Total $n out of $total new " . ($total === 1 ? 'migration' : 'migrations') . " to be applied:\n", Console::FG_YELLOW);
156
        }
157
158 13
        foreach ($migrations as $migration) {
159 13
            $this->stdout("\t$migration\n");
160
        }
161 13
        $this->stdout("\n");
162
163 13
        $applied = 0;
164 13
        if ($this->confirm('Apply the above ' . ($n === 1 ? 'migration' : 'migrations') . '?')) {
165 13
            foreach ($migrations as $migration) {
166 13
                if (!$this->migrateUp($migration)) {
167
                    $this->stdout("\n$applied from $n " . ($applied === 1 ? 'migration was' : 'migrations were') ." applied.\n", Console::FG_RED);
168
                    $this->stdout("\nMigration failed. The rest of the migrations are canceled.\n", Console::FG_RED);
169
170
                    return self::EXIT_CODE_ERROR;
171
                }
172 13
                $applied++;
173
            }
174
175 13
            $this->stdout("\n$n " . ($n === 1 ? 'migration was' : 'migrations were') ." applied.\n", Console::FG_GREEN);
176 13
            $this->stdout("\nMigrated up successfully.\n", Console::FG_GREEN);
177
        }
178 13
    }
179
180
    /**
181
     * Downgrades the application by reverting old migrations.
182
     * For example,
183
     *
184
     * ```
185
     * yii migrate/down     # revert the last migration
186
     * yii migrate/down 3   # revert the last 3 migrations
187
     * yii migrate/down all # revert all migrations
188
     * ```
189
     *
190
     * @param int $limit the number of migrations to be reverted. Defaults to 1,
191
     * meaning the last applied migration will be reverted.
192
     * @throws Exception if the number of the steps specified is less than 1.
193
     *
194
     * @return int the status of the action execution. 0 means normal, other values mean abnormal.
195
     */
196 4
    public function actionDown($limit = 1)
197
    {
198 4
        if ($limit === 'all') {
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison === seems to always evaluate to false as the types of $limit (integer) and 'all' (string) can never be identical. Maybe you want to use a loose comparison == instead?
Loading history...
199 1
            $limit = null;
200
        } else {
201 3
            $limit = (int) $limit;
202 3
            if ($limit < 1) {
203
                throw new Exception('The step argument must be greater than 0.');
204
            }
205
        }
206
207 4
        $migrations = $this->getMigrationHistory($limit);
208
209 4
        if (empty($migrations)) {
210
            $this->stdout("No migration has been done before.\n", Console::FG_YELLOW);
211
212
            return self::EXIT_CODE_NORMAL;
213
        }
214
215 4
        $migrations = array_keys($migrations);
216
217 4
        $n = count($migrations);
218 4
        $this->stdout("Total $n " . ($n === 1 ? 'migration' : 'migrations') . " to be reverted:\n", Console::FG_YELLOW);
219 4
        foreach ($migrations as $migration) {
220 4
            $this->stdout("\t$migration\n");
221
        }
222 4
        $this->stdout("\n");
223
224 4
        $reverted = 0;
225 4
        if ($this->confirm('Revert the above ' . ($n === 1 ? 'migration' : 'migrations') . '?')) {
226 4
            foreach ($migrations as $migration) {
227 4
                if (!$this->migrateDown($migration)) {
228
                    $this->stdout("\n$reverted from $n " . ($reverted === 1 ? 'migration was' : 'migrations were') ." reverted.\n", Console::FG_RED);
229
                    $this->stdout("\nMigration failed. The rest of the migrations are canceled.\n", Console::FG_RED);
230
231
                    return self::EXIT_CODE_ERROR;
232
                }
233 4
                $reverted++;
234
            }
235 4
            $this->stdout("\n$n " . ($n === 1 ? 'migration was' : 'migrations were') ." reverted.\n", Console::FG_GREEN);
236 4
            $this->stdout("\nMigrated down successfully.\n", Console::FG_GREEN);
237
        }
238 4
    }
239
240
    /**
241
     * Redoes the last few migrations.
242
     *
243
     * This command will first revert the specified migrations, and then apply
244
     * them again. For example,
245
     *
246
     * ```
247
     * yii migrate/redo     # redo the last applied migration
248
     * yii migrate/redo 3   # redo the last 3 applied migrations
249
     * yii migrate/redo all # redo all migrations
250
     * ```
251
     *
252
     * @param int $limit the number of migrations to be redone. Defaults to 1,
253
     * meaning the last applied migration will be redone.
254
     * @throws Exception if the number of the steps specified is less than 1.
255
     *
256
     * @return int the status of the action execution. 0 means normal, other values mean abnormal.
257
     */
258 1
    public function actionRedo($limit = 1)
259
    {
260 1
        if ($limit === 'all') {
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison === seems to always evaluate to false as the types of $limit (integer) and 'all' (string) can never be identical. Maybe you want to use a loose comparison == instead?
Loading history...
261
            $limit = null;
262
        } else {
263 1
            $limit = (int) $limit;
264 1
            if ($limit < 1) {
265
                throw new Exception('The step argument must be greater than 0.');
266
            }
267
        }
268
269 1
        $migrations = $this->getMigrationHistory($limit);
270
271 1
        if (empty($migrations)) {
272
            $this->stdout("No migration has been done before.\n", Console::FG_YELLOW);
273
274
            return self::EXIT_CODE_NORMAL;
275
        }
276
277 1
        $migrations = array_keys($migrations);
278
279 1
        $n = count($migrations);
280 1
        $this->stdout("Total $n " . ($n === 1 ? 'migration' : 'migrations') . " to be redone:\n", Console::FG_YELLOW);
281 1
        foreach ($migrations as $migration) {
282 1
            $this->stdout("\t$migration\n");
283
        }
284 1
        $this->stdout("\n");
285
286 1
        if ($this->confirm('Redo the above ' . ($n === 1 ? 'migration' : 'migrations') . '?')) {
287 1
            foreach ($migrations as $migration) {
288 1
                if (!$this->migrateDown($migration)) {
289
                    $this->stdout("\nMigration failed. The rest of the migrations are canceled.\n", Console::FG_RED);
290
291
                    return self::EXIT_CODE_ERROR;
292
                }
293
            }
294 1
            foreach (array_reverse($migrations) as $migration) {
295 1
                if (!$this->migrateUp($migration)) {
296
                    $this->stdout("\nMigration failed. The rest of the migrations are canceled.\n", Console::FG_RED);
297
298
                    return self::EXIT_CODE_ERROR;
299
                }
300
            }
301 1
            $this->stdout("\n$n " . ($n === 1 ? 'migration was' : 'migrations were') ." redone.\n", Console::FG_GREEN);
302 1
            $this->stdout("\nMigration redone successfully.\n", Console::FG_GREEN);
303
        }
304 1
    }
305
306
    /**
307
     * Upgrades or downgrades till the specified version.
308
     *
309
     * Can also downgrade versions to the certain apply time in the past by providing
310
     * a UNIX timestamp or a string parseable by the strtotime() function. This means
311
     * that all the versions applied after the specified certain time would be reverted.
312
     *
313
     * This command will first revert the specified migrations, and then apply
314
     * them again. For example,
315
     *
316
     * ```
317
     * yii migrate/to 101129_185401                          # using timestamp
318
     * yii migrate/to m101129_185401_create_user_table       # using full name
319
     * yii migrate/to 1392853618                             # using UNIX timestamp
320
     * yii migrate/to "2014-02-15 13:00:50"                  # using strtotime() parseable string
321
     * yii migrate/to app\migrations\M101129185401CreateUser # using full namespace name
322
     * ```
323
     *
324
     * @param string $version either the version name or the certain time value in the past
325
     * that the application should be migrated to. This can be either the timestamp,
326
     * the full name of the migration, the UNIX timestamp, or the parseable datetime
327
     * string.
328
     * @throws Exception if the version argument is invalid.
329
     */
330 2
    public function actionTo($version)
331
    {
332 2
        if (($namespaceVersion = $this->extractNamespaceMigrationVersion($version)) !== false) {
333 1
            $this->migrateToVersion($namespaceVersion);
334 1
        } elseif (($migrationName = $this->extractMigrationVersion($version)) !== false) {
335 1
            $this->migrateToVersion($migrationName);
336
        } elseif ((string) (int) $version == $version) {
337
            $this->migrateToTime($version);
338
        } elseif (($time = strtotime($version)) !== false) {
339
            $this->migrateToTime($time);
340
        } else {
341
            throw new Exception("The version argument must be either a timestamp (e.g. 101129_185401),\n the full name of a migration (e.g. m101129_185401_create_user_table),\n the full namespaced name of a migration (e.g. app\\migrations\\M101129185401CreateUserTable),\n a UNIX timestamp (e.g. 1392853000), or a datetime string parseable\nby the strtotime() function (e.g. 2014-02-15 13:00:50).");
342
        }
343 2
    }
344
345
    /**
346
     * Modifies the migration history to the specified version.
347
     *
348
     * No actual migration will be performed.
349
     *
350
     * ```
351
     * yii migrate/mark 101129_185401                        # using timestamp
352
     * yii migrate/mark m101129_185401_create_user_table     # using full name
353
     * yii migrate/to app\migrations\M101129185401CreateUser # using full namespace name
354
     * ```
355
     *
356
     * @param string $version the version at which the migration history should be marked.
357
     * This can be either the timestamp or the full name of the migration.
358
     * @return int CLI exit code
359
     * @throws Exception if the version argument is invalid or the version cannot be found.
360
     */
361 2
    public function actionMark($version)
362
    {
363 2
        $originalVersion = $version;
364 2
        if (($namespaceVersion = $this->extractNamespaceMigrationVersion($version)) !== false) {
365 1
            $version = $namespaceVersion;
366 1
        } elseif (($migrationName = $this->extractMigrationVersion($version)) !== false) {
367 1
            $version = $migrationName;
368
        } else {
369
            throw new Exception("The version argument must be either a timestamp (e.g. 101129_185401)\nor the full name of a migration (e.g. m101129_185401_create_user_table)\nor the full name of a namespaced migration (e.g. app\\migrations\\M101129185401CreateUserTable).");
370
        }
371
372
        // try mark up
373 2
        $migrations = $this->getNewMigrations();
374 2
        foreach ($migrations as $i => $migration) {
375 2
            if (strpos($migration, $version) === 0) {
376 2
                if ($this->confirm("Set migration history at $originalVersion?")) {
377 2
                    for ($j = 0; $j <= $i; ++$j) {
378 2
                        $this->addMigrationHistory($migrations[$j]);
379
                    }
380 2
                    $this->stdout("The migration history is set at $originalVersion.\nNo actual migration was performed.\n", Console::FG_GREEN);
381
                }
382
383 2
                return self::EXIT_CODE_NORMAL;
384
            }
385
        }
386
387
        // try mark down
388
        $migrations = array_keys($this->getMigrationHistory(null));
389
        foreach ($migrations as $i => $migration) {
390
            if (strpos($migration, $version) === 0) {
391
                if ($i === 0) {
392
                    $this->stdout("Already at '$originalVersion'. Nothing needs to be done.\n", Console::FG_YELLOW);
393
                } else {
394
                    if ($this->confirm("Set migration history at $originalVersion?")) {
395
                        for ($j = 0; $j < $i; ++$j) {
396
                            $this->removeMigrationHistory($migrations[$j]);
397
                        }
398
                        $this->stdout("The migration history is set at $originalVersion.\nNo actual migration was performed.\n", Console::FG_GREEN);
399
                    }
400
                }
401
402
                return self::EXIT_CODE_NORMAL;
403
            }
404
        }
405
406
        throw new Exception("Unable to find the version '$originalVersion'.");
407
    }
408
409
    /**
410
     * Checks if given migration version specification matches namespaced migration name.
411
     * @param string $rawVersion raw version specification received from user input.
412
     * @return string|false actual migration version, `false` - if not match.
413
     * @since 2.0.10
414
     */
415 4
    private function extractNamespaceMigrationVersion($rawVersion)
416
    {
417 4
        if (preg_match('/^\\\\?([\w_]+\\\\)+m(\d{6}_?\d{6})(\D.*)?$/is', $rawVersion, $matches)) {
418 2
            return trim($rawVersion, '\\');
419
        }
420 2
        return false;
421
    }
422
423
    /**
424
     * Checks if given migration version specification matches migration base name.
425
     * @param string $rawVersion raw version specification received from user input.
426
     * @return string|false actual migration version, `false` - if not match.
427
     * @since 2.0.10
428
     */
429 2
    private function extractMigrationVersion($rawVersion)
430
    {
431 2
        if (preg_match('/^m?(\d{6}_?\d{6})(\D.*)?$/is', $rawVersion, $matches)) {
432 2
            return 'm' . $matches[1];
433
        }
434
        return false;
435
    }
436
437
    /**
438
     * Displays the migration history.
439
     *
440
     * This command will show the list of migrations that have been applied
441
     * so far. For example,
442
     *
443
     * ```
444
     * yii migrate/history     # showing the last 10 migrations
445
     * yii migrate/history 5   # showing the last 5 migrations
446
     * yii migrate/history all # showing the whole history
447
     * ```
448
     *
449
     * @param int $limit the maximum number of migrations to be displayed.
450
     * If it is "all", the whole migration history will be displayed.
451
     * @throws \yii\console\Exception if invalid limit value passed
452
     */
453 4
    public function actionHistory($limit = 10)
454
    {
455 4
        if ($limit === 'all') {
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison === seems to always evaluate to false as the types of $limit (integer) and 'all' (string) can never be identical. Maybe you want to use a loose comparison == instead?
Loading history...
456
            $limit = null;
457
        } else {
458 4
            $limit = (int) $limit;
459 4
            if ($limit < 1) {
460
                throw new Exception('The limit must be greater than 0.');
461
            }
462
        }
463
464 4
        $migrations = $this->getMigrationHistory($limit);
465
466 4
        if (empty($migrations)) {
467 4
            $this->stdout("No migration has been done before.\n", Console::FG_YELLOW);
468
        } else {
469 2
            $n = count($migrations);
470 2
            if ($limit > 0) {
471 2
                $this->stdout("Showing the last $n applied " . ($n === 1 ? 'migration' : 'migrations') . ":\n", Console::FG_YELLOW);
472
            } else {
473
                $this->stdout("Total $n " . ($n === 1 ? 'migration has' : 'migrations have') . " been applied before:\n", Console::FG_YELLOW);
474
            }
475 2
            foreach ($migrations as $version => $time) {
476 2
                $this->stdout("\t(" . date('Y-m-d H:i:s', $time) . ') ' . $version . "\n");
477
            }
478
        }
479 4
    }
480
481
    /**
482
     * Displays the un-applied new migrations.
483
     *
484
     * This command will show the new migrations that have not been applied.
485
     * For example,
486
     *
487
     * ```
488
     * yii migrate/new     # showing the first 10 new migrations
489
     * yii migrate/new 5   # showing the first 5 new migrations
490
     * yii migrate/new all # showing all new migrations
491
     * ```
492
     *
493
     * @param int $limit the maximum number of new migrations to be displayed.
494
     * If it is `all`, all available new migrations will be displayed.
495
     * @throws \yii\console\Exception if invalid limit value passed
496
     */
497 1
    public function actionNew($limit = 10)
498
    {
499 1
        if ($limit === 'all') {
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison === seems to always evaluate to false as the types of $limit (integer) and 'all' (string) can never be identical. Maybe you want to use a loose comparison == instead?
Loading history...
500
            $limit = null;
501
        } else {
502 1
            $limit = (int) $limit;
503 1
            if ($limit < 1) {
504
                throw new Exception('The limit must be greater than 0.');
505
            }
506
        }
507
508 1
        $migrations = $this->getNewMigrations();
509
510 1
        if (empty($migrations)) {
511 1
            $this->stdout("No new migrations found. Your system is up-to-date.\n", Console::FG_GREEN);
512
        } else {
513 1
            $n = count($migrations);
514 1
            if ($limit && $n > $limit) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $limit of type null|integer is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
515
                $migrations = array_slice($migrations, 0, $limit);
516
                $this->stdout("Showing $limit out of $n new " . ($n === 1 ? 'migration' : 'migrations') . ":\n", Console::FG_YELLOW);
517
            } else {
518 1
                $this->stdout("Found $n new " . ($n === 1 ? 'migration' : 'migrations') . ":\n", Console::FG_YELLOW);
519
            }
520
521 1
            foreach ($migrations as $migration) {
522 1
                $this->stdout("\t" . $migration . "\n");
523
            }
524
        }
525 1
    }
526
527
    /**
528
     * Creates a new migration.
529
     *
530
     * This command creates a new migration using the available migration template.
531
     * After using this command, developers should modify the created migration
532
     * skeleton by filling up the actual migration logic.
533
     *
534
     * ```
535
     * yii migrate/create create_user_table
536
     * ```
537
     *
538
     * In order to generate a namespaced migration, you should specify a namespace before the migration's name.
539
     * Note that backslash (`\`) is usually considered a special character in the shell, so you need to escape it
540
     * properly to avoid shell errors or incorrect behavior.
541
     * For example:
542
     *
543
     * ```
544
     * yii migrate/create 'app\\migrations\\createUserTable'
545
     * ```
546
     *
547
     * In case [[migrationPath]] is not set and no namespace is provided, the first entry of [[migrationNamespaces]] will be used.
548
     *
549
     * @param string $name the name of the new migration. This should only contain
550
     * letters, digits, underscores and/or backslashes.
551
     *
552
     * Note: If the migration name is of a special form, for example create_xxx or
553
     * drop_xxx, then the generated migration file will contain extra code,
554
     * in this case for creating/dropping tables.
555
     *
556
     * @throws Exception if the name argument is invalid.
557
     */
558 8
    public function actionCreate($name)
559
    {
560 8
        if (!preg_match('/^[\w\\\\]+$/', $name)) {
561
            throw new Exception('The migration name should contain letters, digits, underscore and/or backslash characters only.');
562
        }
563
564 8
        list($namespace, $className) = $this->generateClassName($name);
565 8
        $migrationPath = $this->findMigrationPath($namespace);
566
567 8
        $file = $migrationPath . DIRECTORY_SEPARATOR . $className . '.php';
568 8
        if ($this->confirm("Create new migration '$file'?")) {
569 8
            $content = $this->generateMigrationSourceCode([
570 8
                'name' => $name,
571 8
                'className' => $className,
572 8
                'namespace' => $namespace,
573
            ]);
574 8
            FileHelper::createDirectory($migrationPath);
575 8
            file_put_contents($file, $content);
576 8
            $this->stdout("New migration created successfully.\n", Console::FG_GREEN);
577
        }
578 8
    }
579
580
    /**
581
     * Generates class base name and namespace from migration name from user input.
582
     * @param string $name migration name from user input.
583
     * @return array list of 2 elements: 'namespace' and 'class base name'
584
     * @since 2.0.10
585
     */
586 8
    private function generateClassName($name)
587
    {
588 8
        $namespace = null;
589 8
        $name = trim($name, '\\');
590 8
        if (strpos($name, '\\') !== false) {
591 1
            $namespace = substr($name, 0, strrpos($name, '\\'));
592 1
            $name = substr($name, strrpos($name, '\\') + 1);
593
        } else {
594 8
            if ($this->migrationPath === null) {
595 1
                $migrationNamespaces = $this->migrationNamespaces;
596 1
                $namespace = array_shift($migrationNamespaces);
597
            }
598
        }
599
600 8
        if ($namespace === null) {
601 8
            $class = 'm' . gmdate('ymd_His') . '_' . $name;
602
        } else {
603 1
            $class = 'M' . gmdate('ymdHis') . ucfirst($name);
604
        }
605
606 8
        return [$namespace, $class];
607
    }
608
609
    /**
610
     * Finds the file path for the specified migration namespace.
611
     * @param string|null $namespace migration namespace.
612
     * @return string migration file path.
613
     * @throws Exception on failure.
614
     * @since 2.0.10
615
     */
616 8
    private function findMigrationPath($namespace)
617
    {
618 8
        if (empty($namespace)) {
619 8
            return $this->migrationPath;
620
        }
621
622 1
        if (!in_array($namespace, $this->migrationNamespaces, true)) {
623
            throw new Exception("Namespace '{$namespace}' not found in `migrationNamespaces`");
624
        }
625
626 1
        return $this->getNamespacePath($namespace);
627
    }
628
629
    /**
630
     * Returns the file path matching the give namespace.
631
     * @param string $namespace namespace.
632
     * @return string file path.
633
     * @since 2.0.10
634
     */
635 6
    private function getNamespacePath($namespace)
636
    {
637 6
        return str_replace('/', DIRECTORY_SEPARATOR, Yii::getAlias('@' . str_replace('\\', '/', $namespace)));
638
    }
639
640
    /**
641
     * Upgrades with the specified migration class.
642
     * @param string $class the migration class name
643
     * @return bool whether the migration is successful
644
     */
645 13
    protected function migrateUp($class)
646
    {
647 13
        if ($class === self::BASE_MIGRATION) {
648
            return true;
649
        }
650
651 13
        $this->stdout("*** applying $class\n", Console::FG_YELLOW);
652 13
        $start = microtime(true);
653 13
        $migration = $this->createMigration($class);
654 13
        if ($migration->up() !== false) {
655 13
            $this->addMigrationHistory($class);
656 13
            $time = microtime(true) - $start;
657 13
            $this->stdout("*** applied $class (time: " . sprintf('%.3f', $time) . "s)\n\n", Console::FG_GREEN);
658
659 13
            return true;
660
        } else {
661
            $time = microtime(true) - $start;
662
            $this->stdout("*** failed to apply $class (time: " . sprintf('%.3f', $time) . "s)\n\n", Console::FG_RED);
663
664
            return false;
665
        }
666
    }
667
668
    /**
669
     * Downgrades with the specified migration class.
670
     * @param string $class the migration class name
671
     * @return bool whether the migration is successful
672
     */
673 5
    protected function migrateDown($class)
674
    {
675 5
        if ($class === self::BASE_MIGRATION) {
676
            return true;
677
        }
678
679 5
        $this->stdout("*** reverting $class\n", Console::FG_YELLOW);
680 5
        $start = microtime(true);
681 5
        $migration = $this->createMigration($class);
682 5
        if ($migration->down() !== false) {
683 5
            $this->removeMigrationHistory($class);
684 5
            $time = microtime(true) - $start;
685 5
            $this->stdout("*** reverted $class (time: " . sprintf('%.3f', $time) . "s)\n\n", Console::FG_GREEN);
686
687 5
            return true;
688
        } else {
689
            $time = microtime(true) - $start;
690
            $this->stdout("*** failed to revert $class (time: " . sprintf('%.3f', $time) . "s)\n\n", Console::FG_RED);
691
692
            return false;
693
        }
694
    }
695
696
    /**
697
     * Creates a new migration instance.
698
     * @param string $class the migration class name
699
     * @return \yii\db\MigrationInterface the migration instance
700
     */
701
    protected function createMigration($class)
702
    {
703
        $class = trim($class, '\\');
704
        if (strpos($class, '\\') === false) {
705
            $file = $this->migrationPath . DIRECTORY_SEPARATOR . $class . '.php';
706
            require_once($file);
707
        }
708
709
        return new $class();
710
    }
711
712
    /**
713
     * Migrates to the specified apply time in the past.
714
     * @param int $time UNIX timestamp value.
715
     */
716
    protected function migrateToTime($time)
717
    {
718
        $count = 0;
719
        $migrations = array_values($this->getMigrationHistory(null));
720
        while ($count < count($migrations) && $migrations[$count] > $time) {
721
            ++$count;
722
        }
723
        if ($count === 0) {
724
            $this->stdout("Nothing needs to be done.\n", Console::FG_GREEN);
725
        } else {
726
            $this->actionDown($count);
727
        }
728
    }
729
730
    /**
731
     * Migrates to the certain version.
732
     * @param string $version name in the full format.
733
     * @return int CLI exit code
734
     * @throws Exception if the provided version cannot be found.
735
     */
736 2
    protected function migrateToVersion($version)
737
    {
738 2
        $originalVersion = $version;
739
740
        // try migrate up
741 2
        $migrations = $this->getNewMigrations();
742 2
        foreach ($migrations as $i => $migration) {
743 2
            if (strpos($migration, $version) === 0) {
744 2
                $this->actionUp($i + 1);
745
746 2
                return self::EXIT_CODE_NORMAL;
747
            }
748
        }
749
750
        // try migrate down
751
        $migrations = array_keys($this->getMigrationHistory(null));
752
        foreach ($migrations as $i => $migration) {
753
            if (strpos($migration, $version) === 0) {
754
                if ($i === 0) {
755
                    $this->stdout("Already at '$originalVersion'. Nothing needs to be done.\n", Console::FG_YELLOW);
756
                } else {
757
                    $this->actionDown($i);
758
                }
759
760
                return self::EXIT_CODE_NORMAL;
761
            }
762
        }
763
764
        throw new Exception("Unable to find the version '$originalVersion'.");
765
    }
766
767
    /**
768
     * Returns the migrations that are not applied.
769
     * @return array list of new migrations
770
     */
771 15
    protected function getNewMigrations()
772
    {
773 15
        $applied = [];
774 15
        foreach ($this->getMigrationHistory(null) as $class => $time) {
775 1
            $applied[trim($class, '\\')] = true;
776
        }
777
778 15
        $migrationPaths = [];
779 15
        if (!empty($this->migrationPath)) {
780 10
            $migrationPaths[''] = $this->migrationPath;
781
        }
782 15
        foreach ($this->migrationNamespaces as $namespace) {
783 5
            $migrationPaths[$namespace] = $this->getNamespacePath($namespace);
784
        }
785
786 15
        $migrations = [];
787 15
        foreach ($migrationPaths as $namespace => $migrationPath) {
788 15
            if (!file_exists($migrationPath)) {
789
                continue;
790
            }
791 15
            $handle = opendir($migrationPath);
792 15
            while (($file = readdir($handle)) !== false) {
793 15
                if ($file === '.' || $file === '..') {
794 15
                    continue;
795
                }
796 15
                $path = $migrationPath . DIRECTORY_SEPARATOR . $file;
797 15
                if (preg_match('/^(m(\d{6}_?\d{6})\D.*?)\.php$/is', $file, $matches) && is_file($path)) {
798 15
                    $class = $matches[1];
799 15
                    if (!empty($namespace)) {
800 5
                        $class = $namespace . '\\' . $class;
801
                    }
802 15
                    $time = str_replace('_', '', $matches[2]);
803 15
                    if (!isset($applied[$class])) {
804 15
                        $migrations[$time . '\\' . $class] = $class;
805
                    }
806
                }
807
            }
808 15
            closedir($handle);
809
        }
810 15
        ksort($migrations);
811
812 15
        return array_values($migrations);
813
    }
814
815
    /**
816
     * Generates new migration source PHP code.
817
     * Child class may override this method, adding extra logic or variation to the process.
818
     * @param array $params generation parameters, usually following parameters are present:
819
     *
820
     *  - name: string migration base name
821
     *  - className: string migration class name
822
     *
823
     * @return string generated PHP code.
824
     * @since 2.0.8
825
     */
826
    protected function generateMigrationSourceCode($params)
827
    {
828
        return $this->renderFile(Yii::getAlias($this->templateFile), $params);
0 ignored issues
show
Bug introduced by
It seems like \Yii::getAlias($this->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...
829
    }
830
831
    /**
832
     * Returns the migration history.
833
     * @param int $limit the maximum number of records in the history to be returned. `null` for "no limit".
834
     * @return array the migration history
835
     */
836
    abstract protected function getMigrationHistory($limit);
837
838
    /**
839
     * Adds new migration entry to the history.
840
     * @param string $version migration version name.
841
     */
842
    abstract protected function addMigrationHistory($version);
843
844
    /**
845
     * Removes existing migration from the history.
846
     * @param string $version migration version name.
847
     */
848
    abstract protected function removeMigrationHistory($version);
849
}
850