Completed
Pull Request — 2.1 (#12704)
by Robert
08:59
created

BaseMigrateController   D

Complexity

Total Complexity 129

Size/Duplication

Total Lines 824
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 72.78%

Importance

Changes 0
Metric Value
wmc 129
lcom 1
cbo 7
dl 0
loc 824
ccs 262
cts 360
cp 0.7278
rs 4.4444
c 0
b 0
f 0

25 Methods

Rating   Name   Duplication   Size   Complexity  
C actionUp() 0 43 13
C actionDown() 0 43 12
A options() 0 8 2
C beforeAction() 0 30 8
C actionRedo() 0 47 13
B actionTo() 0 14 5
C actionMark() 0 47 12
A extractNamespaceMigrationVersion() 0 7 2
A extractMigrationVersion() 0 7 2
C actionHistory() 0 27 8
D actionNew() 0 29 9
A actionCreate() 0 21 3
B generateClassName() 0 22 4
A findMigrationPath() 0 12 3
A getNamespacePath() 0 4 1
A migrateUp() 0 22 3
A migrateDown() 0 22 3
A createMigration() 0 10 2
A migrateToTime() 0 13 4
B migrateToVersion() 0 30 6
C getNewMigrations() 0 43 13
A generateMigrationSourceCode() 0 4 1
getMigrationHistory() 0 1 ?
addMigrationHistory() 0 1 ?
removeMigrationHistory() 0 1 ?

How to fix   Complexity   

Complex Class

Complex classes like BaseMigrateController often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use BaseMigrateController, and based on these observations, apply Extract Interface, too.

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
     * If you have set up [[migrationNamespaces]], you may set this field to `null` in order
39
     * to disable usage of migrations that are not namespaced.
40
     */
41
    public $migrationPath = '@app/migrations';
42
    /**
43
     * @var array list of namespaces containing the migration classes.
44
     *
45
     * Migration namespaces should be resolvable as a path alias if prefixed with `@`, e.g. if you specify
46
     * the namespace `app\migrations`, the code `Yii::getAlias('@app/migrations')` should be able to return 
47
     * the file path to the directory this namespace refers to.
48
     *
49
     * For example:
50
     *
51
     * ```php
52
     * [
53
     *     'app\migrations',
54
     *     'some\extension\migrations',
55
     * ]
56
     * ```
57
     *
58
     * @since 2.0.10
59
     */
60
    public $migrationNamespaces = [];
61
    /**
62
     * @var string the template file for generating new migrations.
63
     * This can be either a path alias (e.g. "@app/migrations/template.php")
64
     * or a file path.
65
     */
66
    public $templateFile;
67
68
69
    /**
70
     * @inheritdoc
71
     */
72 16
    public function options($actionID)
73
    {
74 16
        return array_merge(
75 16
            parent::options($actionID),
76 16
            ['migrationPath'], // global for all actions
77 16
            $actionID === 'create' ? ['templateFile'] : [] // action create
78 16
        );
79
    }
80
81
    /**
82
     * This method is invoked right before an action is to be executed (after all possible filters.)
83
     * It checks the existence of the [[migrationPath]].
84
     * @param \yii\base\Action $action the action to be executed.
85
     * @throws InvalidConfigException if directory specified in migrationPath doesn't exist and action isn't "create".
86
     * @return boolean whether the action should continue to be executed.
87
     */
88 23
    public function beforeAction($action)
89
    {
90 23
        if (parent::beforeAction($action)) {
91 23
            if (empty($this->migrationNamespaces) && empty($this->migrationPath)) {
92
                throw new InvalidConfigException('At least one of `migrationPath` or `migrationNamespaces` should be specified.');
93
            }
94
95 23
            foreach ($this->migrationNamespaces as $key => $value) {
96 6
                $this->migrationNamespaces[$key] = trim($value, '\\');
97 23
            }
98
99 23
            if ($this->migrationPath !== null) {
100 18
                $path = Yii::getAlias($this->migrationPath);
101 18
                if (!is_dir($path)) {
102 5
                    if ($action->id !== 'create') {
103
                        throw new InvalidConfigException("Migration failed. Directory specified in migrationPath doesn't exist: {$this->migrationPath}");
104
                    }
105 5
                    FileHelper::createDirectory($path);
0 ignored issues
show
Bug introduced by
It seems like $path defined by \Yii::getAlias($this->migrationPath) on line 100 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...
106 5
                }
107 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...
108 18
            }
109
110 23
            $version = Yii::getVersion();
111 23
            $this->stdout("Yii Migration Tool (based on Yii v{$version})\n\n");
112
113 23
            return true;
114
        } else {
115
            return false;
116
        }
117
    }
118
119
    /**
120
     * Upgrades the application by applying new migrations.
121
     * For example,
122
     *
123
     * ```
124
     * yii migrate     # apply all new migrations
125
     * yii migrate 3   # apply the first 3 new migrations
126
     * ```
127
     *
128
     * @param integer $limit the number of new migrations to be applied. If 0, it means
129
     * applying all available new migrations.
130
     *
131
     * @return integer the status of the action execution. 0 means normal, other values mean abnormal.
132
     */
133 13
    public function actionUp($limit = 0)
134
    {
135 13
        $migrations = $this->getNewMigrations();
136 13
        if (empty($migrations)) {
137
            $this->stdout("No new migrations found. Your system is up-to-date.\n", Console::FG_GREEN);
138
139
            return self::EXIT_CODE_NORMAL;
140
        }
141
142 13
        $total = count($migrations);
143 13
        $limit = (int) $limit;
144 13
        if ($limit > 0) {
145 3
            $migrations = array_slice($migrations, 0, $limit);
146 3
        }
147
148 13
        $n = count($migrations);
149 13
        if ($n === $total) {
150 12
            $this->stdout("Total $n new " . ($n === 1 ? 'migration' : 'migrations') . " to be applied:\n", Console::FG_YELLOW);
151 12
        } else {
152 1
            $this->stdout("Total $n out of $total new " . ($total === 1 ? 'migration' : 'migrations') . " to be applied:\n", Console::FG_YELLOW);
153
        }
154
155 13
        foreach ($migrations as $migration) {
156 13
            $this->stdout("\t$migration\n");
157 13
        }
158 13
        $this->stdout("\n");
159
160 13
        $applied = 0;
161 13
        if ($this->confirm('Apply the above ' . ($n === 1 ? 'migration' : 'migrations') . '?')) {
162 13
            foreach ($migrations as $migration) {
163 13
                if (!$this->migrateUp($migration)) {
164
                    $this->stdout("\n$applied from $n " . ($applied === 1 ? 'migration was' : 'migrations were') ." applied.\n", Console::FG_RED);
165
                    $this->stdout("\nMigration failed. The rest of the migrations are canceled.\n", Console::FG_RED);
166
167
                    return self::EXIT_CODE_ERROR;
168
                }
169 13
                $applied++;
170 13
            }
171
172 13
            $this->stdout("\n$n " . ($n === 1 ? 'migration was' : 'migrations were') ." applied.\n", Console::FG_GREEN);
173 13
            $this->stdout("\nMigrated up successfully.\n", Console::FG_GREEN);
174 13
        }
175 13
    }
176
177
    /**
178
     * Downgrades the application by reverting old migrations.
179
     * For example,
180
     *
181
     * ```
182
     * yii migrate/down     # revert the last migration
183
     * yii migrate/down 3   # revert the last 3 migrations
184
     * yii migrate/down all # revert all migrations
185
     * ```
186
     *
187
     * @param integer $limit the number of migrations to be reverted. Defaults to 1,
188
     * meaning the last applied migration will be reverted.
189
     * @throws Exception if the number of the steps specified is less than 1.
190
     *
191
     * @return integer the status of the action execution. 0 means normal, other values mean abnormal.
192
     */
193 15
    public function actionDown($limit = 1)
194
    {
195 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...
196 1
            $limit = null;
197 1
        } else {
198 3
            $limit = (int) $limit;
199 15
            if ($limit < 1) {
200
                throw new Exception('The step argument must be greater than 0.');
201
            }
202
        }
203
204 4
        $migrations = $this->getMigrationHistory($limit);
205
206 4
        if (empty($migrations)) {
207
            $this->stdout("No migration has been done before.\n", Console::FG_YELLOW);
208
209
            return self::EXIT_CODE_NORMAL;
210
        }
211
212 4
        $migrations = array_keys($migrations);
213
214 4
        $n = count($migrations);
215 4
        $this->stdout("Total $n " . ($n === 1 ? 'migration' : 'migrations') . " to be reverted:\n", Console::FG_YELLOW);
216 4
        foreach ($migrations as $migration) {
217 4
            $this->stdout("\t$migration\n");
218 4
        }
219 4
        $this->stdout("\n");
220
221 4
        $reverted = 0;
222 4
        if ($this->confirm('Revert the above ' . ($n === 1 ? 'migration' : 'migrations') . '?')) {
223 4
            foreach ($migrations as $migration) {
224 4
                if (!$this->migrateDown($migration)) {
225
                    $this->stdout("\n$reverted from $n " . ($reverted === 1 ? 'migration was' : 'migrations were') ." reverted.\n", Console::FG_RED);
226
                    $this->stdout("\nMigration failed. The rest of the migrations are canceled.\n", Console::FG_RED);
227
228
                    return self::EXIT_CODE_ERROR;
229
                }
230 4
                $reverted++;
231 4
            }
232 4
            $this->stdout("\n$n " . ($n === 1 ? 'migration was' : 'migrations were') ." reverted.\n", Console::FG_GREEN);
233 4
            $this->stdout("\nMigrated down successfully.\n", Console::FG_GREEN);
234 4
        }
235 4
    }
236
237
    /**
238
     * Redoes the last few migrations.
239
     *
240
     * This command will first revert the specified migrations, and then apply
241
     * them again. For example,
242
     *
243
     * ```
244
     * yii migrate/redo     # redo the last applied migration
245
     * yii migrate/redo 3   # redo the last 3 applied migrations
246
     * yii migrate/redo all # redo all migrations
247
     * ```
248
     *
249
     * @param integer $limit the number of migrations to be redone. Defaults to 1,
250
     * meaning the last applied migration will be redone.
251
     * @throws Exception if the number of the steps specified is less than 1.
252
     *
253
     * @return integer the status of the action execution. 0 means normal, other values mean abnormal.
254
     */
255 1
    public function actionRedo($limit = 1)
256
    {
257 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...
258
            $limit = null;
259
        } else {
260 1
            $limit = (int) $limit;
261 1
            if ($limit < 1) {
262
                throw new Exception('The step argument must be greater than 0.');
263
            }
264
        }
265
266 1
        $migrations = $this->getMigrationHistory($limit);
267
268 1
        if (empty($migrations)) {
269
            $this->stdout("No migration has been done before.\n", Console::FG_YELLOW);
270
271
            return self::EXIT_CODE_NORMAL;
272
        }
273
274 1
        $migrations = array_keys($migrations);
275
276 1
        $n = count($migrations);
277 1
        $this->stdout("Total $n " . ($n === 1 ? 'migration' : 'migrations') . " to be redone:\n", Console::FG_YELLOW);
278 1
        foreach ($migrations as $migration) {
279 1
            $this->stdout("\t$migration\n");
280 1
        }
281 1
        $this->stdout("\n");
282
283 1
        if ($this->confirm('Redo the above ' . ($n === 1 ? 'migration' : 'migrations') . '?')) {
284 1
            foreach ($migrations as $migration) {
285 1
                if (!$this->migrateDown($migration)) {
286
                    $this->stdout("\nMigration failed. The rest of the migrations are canceled.\n", Console::FG_RED);
287
288
                    return self::EXIT_CODE_ERROR;
289
                }
290 1
            }
291 1
            foreach (array_reverse($migrations) as $migration) {
292 1
                if (!$this->migrateUp($migration)) {
293
                    $this->stdout("\nMigration failed. The rest of the migrations are canceled.\n", Console::FG_RED);
294
295
                    return self::EXIT_CODE_ERROR;
296
                }
297 1
            }
298 1
            $this->stdout("\n$n " . ($n === 1 ? 'migration was' : 'migrations were') ." redone.\n", Console::FG_GREEN);
299 1
            $this->stdout("\nMigration redone successfully.\n", Console::FG_GREEN);
300 1
        }
301 1
    }
302
303
    /**
304
     * Upgrades or downgrades till the specified version.
305
     *
306
     * Can also downgrade versions to the certain apply time in the past by providing
307
     * a UNIX timestamp or a string parseable by the strtotime() function. This means
308
     * that all the versions applied after the specified certain time would be reverted.
309
     *
310
     * This command will first revert the specified migrations, and then apply
311
     * them again. For example,
312
     *
313
     * ```
314
     * yii migrate/to 101129_185401                          # using timestamp
315
     * yii migrate/to m101129_185401_create_user_table       # using full name
316
     * yii migrate/to 1392853618                             # using UNIX timestamp
317
     * yii migrate/to "2014-02-15 13:00:50"                  # using strtotime() parseable string
318
     * yii migrate/to app\migrations\M101129185401CreateUser # using full namespace name
319
     * ```
320
     *
321
     * @param string $version either the version name or the certain time value in the past
322
     * that the application should be migrated to. This can be either the timestamp,
323
     * the full name of the migration, the UNIX timestamp, or the parseable datetime
324
     * string.
325
     * @throws Exception if the version argument is invalid.
326
     */
327 2
    public function actionTo($version)
328
    {
329 2
        if (($namespaceVersion = $this->extractNamespaceMigrationVersion($version)) !== false) {
330 1
            $this->migrateToVersion($namespaceVersion);
331 2
        } elseif (($migrationName = $this->extractMigrationVersion($version)) !== false) {
332 1
            $this->migrateToVersion($migrationName);
333 1
        } elseif ((string) (int) $version == $version) {
334
            $this->migrateToTime($version);
335
        } elseif (($time = strtotime($version)) !== false) {
336
            $this->migrateToTime($time);
337
        } else {
338
            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).");
339
        }
340 2
    }
341
342
    /**
343
     * Modifies the migration history to the specified version.
344
     *
345
     * No actual migration will be performed.
346
     *
347
     * ```
348
     * yii migrate/mark 101129_185401                        # using timestamp
349
     * yii migrate/mark m101129_185401_create_user_table     # using full name
350
     * yii migrate/to app\migrations\M101129185401CreateUser # using full namespace name
351
     * ```
352
     *
353
     * @param string $version the version at which the migration history should be marked.
354
     * This can be either the timestamp or the full name of the migration.
355
     * @return integer CLI exit code
356
     * @throws Exception if the version argument is invalid or the version cannot be found.
357
     */
358 2
    public function actionMark($version)
359
    {
360 2
        $originalVersion = $version;
361 2
        if (($namespaceVersion = $this->extractNamespaceMigrationVersion($version)) !== false) {
362 1
            $version = $namespaceVersion;
363 2
        } elseif (($migrationName = $this->extractMigrationVersion($version)) !== false) {
364 1
            $version = $migrationName;
365 1
        } else {
366
            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).");
367
        }
368
369
        // try mark up
370 2
        $migrations = $this->getNewMigrations();
371 2
        foreach ($migrations as $i => $migration) {
372 2
            if (strpos($migration, $version) === 0) {
373 2
                if ($this->confirm("Set migration history at $originalVersion?")) {
374 2
                    for ($j = 0; $j <= $i; ++$j) {
375 2
                        $this->addMigrationHistory($migrations[$j]);
376 2
                    }
377 2
                    $this->stdout("The migration history is set at $originalVersion.\nNo actual migration was performed.\n", Console::FG_GREEN);
378 2
                }
379
380 2
                return self::EXIT_CODE_NORMAL;
381
            }
382
        }
383
384
        // try mark down
385
        $migrations = array_keys($this->getMigrationHistory(null));
386
        foreach ($migrations as $i => $migration) {
387
            if (strpos($migration, $version) === 0) {
388
                if ($i === 0) {
389
                    $this->stdout("Already at '$originalVersion'. Nothing needs to be done.\n", Console::FG_YELLOW);
390
                } else {
391
                    if ($this->confirm("Set migration history at $originalVersion?")) {
392
                        for ($j = 0; $j < $i; ++$j) {
393
                            $this->removeMigrationHistory($migrations[$j]);
394
                        }
395
                        $this->stdout("The migration history is set at $originalVersion.\nNo actual migration was performed.\n", Console::FG_GREEN);
396
                    }
397
                }
398
399
                return self::EXIT_CODE_NORMAL;
400
            }
401
        }
402
403
        throw new Exception("Unable to find the version '$originalVersion'.");
404
    }
405
406
    /**
407
     * Checks if given migration version specification matches namespaced migration name.
408
     * @param string $rawVersion raw version specification received from user input.
409
     * @return string|false actual migration version, `false` - if not match.
410
     * @since 2.0.10
411
     */
412 4
    private function extractNamespaceMigrationVersion($rawVersion)
413
    {
414 4
        if (preg_match('/^\\\\?([\w_]+\\\\)+m(\d{6}_?\d{6})(\D.*)?$/is', $rawVersion, $matches)) {
415 2
            return trim($rawVersion, '\\');
416
        }
417 2
        return false;
418
    }
419
420
    /**
421
     * Checks if given migration version specification matches migration base name.
422
     * @param string $rawVersion raw version specification received from user input.
423
     * @return string|false actual migration version, `false` - if not match.
424
     * @since 2.0.10
425
     */
426 2
    private function extractMigrationVersion($rawVersion)
427
    {
428 2
        if (preg_match('/^m?(\d{6}_?\d{6})(\D.*)?$/is', $rawVersion, $matches)) {
429 2
            return 'm' . $matches[1];
430
        }
431
        return false;
432
    }
433
434
    /**
435
     * Displays the migration history.
436
     *
437
     * This command will show the list of migrations that have been applied
438
     * so far. For example,
439
     *
440
     * ```
441
     * yii migrate/history     # showing the last 10 migrations
442
     * yii migrate/history 5   # showing the last 5 migrations
443
     * yii migrate/history all # showing the whole history
444
     * ```
445
     *
446
     * @param integer $limit the maximum number of migrations to be displayed.
447
     * If it is "all", the whole migration history will be displayed.
448
     * @throws \yii\console\Exception if invalid limit value passed
449
     */
450 3
    public function actionHistory($limit = 10)
451
    {
452 3
        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...
453
            $limit = null;
454
        } else {
455 3
            $limit = (int) $limit;
456 3
            if ($limit < 1) {
457
                throw new Exception('The limit must be greater than 0.');
458
            }
459
        }
460
461 3
        $migrations = $this->getMigrationHistory($limit);
462
463 3
        if (empty($migrations)) {
464 3
            $this->stdout("No migration has been done before.\n", Console::FG_YELLOW);
465 3
        } else {
466 2
            $n = count($migrations);
467 2
            if ($limit > 0) {
468 2
                $this->stdout("Showing the last $n applied " . ($n === 1 ? 'migration' : 'migrations') . ":\n", Console::FG_YELLOW);
469 2
            } else {
470
                $this->stdout("Total $n " . ($n === 1 ? 'migration has' : 'migrations have') . " been applied before:\n", Console::FG_YELLOW);
471
            }
472 2
            foreach ($migrations as $version => $time) {
473 2
                $this->stdout("\t(" . date('Y-m-d H:i:s', $time) . ') ' . $version . "\n");
474 2
            }
475
        }
476 3
    }
477
478
    /**
479
     * Displays the un-applied new migrations.
480
     *
481
     * This command will show the new migrations that have not been applied.
482
     * For example,
483
     *
484
     * ```
485
     * yii migrate/new     # showing the first 10 new migrations
486
     * yii migrate/new 5   # showing the first 5 new migrations
487
     * yii migrate/new all # showing all new migrations
488
     * ```
489
     *
490
     * @param integer $limit the maximum number of new migrations to be displayed.
491
     * If it is `all`, all available new migrations will be displayed.
492
     * @throws \yii\console\Exception if invalid limit value passed
493
     */
494 1
    public function actionNew($limit = 10)
495
    {
496 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...
497
            $limit = null;
498
        } else {
499 1
            $limit = (int) $limit;
500 1
            if ($limit < 1) {
501
                throw new Exception('The limit must be greater than 0.');
502
            }
503
        }
504
505 1
        $migrations = $this->getNewMigrations();
506
507 1
        if (empty($migrations)) {
508 1
            $this->stdout("No new migrations found. Your system is up-to-date.\n", Console::FG_GREEN);
509 1
        } else {
510 1
            $n = count($migrations);
511 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...
512
                $migrations = array_slice($migrations, 0, $limit);
513
                $this->stdout("Showing $limit out of $n new " . ($n === 1 ? 'migration' : 'migrations') . ":\n", Console::FG_YELLOW);
514
            } else {
515 1
                $this->stdout("Found $n new " . ($n === 1 ? 'migration' : 'migrations') . ":\n", Console::FG_YELLOW);
516
            }
517
518 1
            foreach ($migrations as $migration) {
519 1
                $this->stdout("\t" . $migration . "\n");
520 1
            }
521
        }
522 1
    }
523
524
    /**
525
     * Creates a new migration.
526
     *
527
     * This command creates a new migration using the available migration template.
528
     * After using this command, developers should modify the created migration
529
     * skeleton by filling up the actual migration logic.
530
     *
531
     * ```
532
     * yii migrate/create create_user_table
533
     * ```
534
     *
535
     * In order to generate a namespaced migration, you should specify a namespace before the migration's name.
536
     * Note that backslash (`\`) is usually considered a special character in the shell, so you need to escape it
537
     * properly to avoid shell errors or incorrect behavior.
538
     * For example:
539
     *
540
     * ```
541
     * yii migrate/create 'app\\migrations\\createUserTable'
542
     * ```
543
     *
544
     * In case [[migrationPath]] is not set and no namespace is provided, the first entry of [[migrationNamespaces]] will be used.
545
     *
546
     * @param string $name the name of the new migration. This should only contain
547
     * letters, digits, underscores and/or backslashes.
548
     *
549
     * Note: If the migration name is of a special form, for example create_xxx or
550
     * drop_xxx, then the generated migration file will contain extra code,
551
     * in this case for creating/dropping tables.
552
     *
553
     * @throws Exception if the name argument is invalid.
554
     */
555 8
    public function actionCreate($name)
556
    {
557 8
        if (!preg_match('/^[\w\\\\]+$/', $name)) {
558
            throw new Exception('The migration name should contain letters, digits, underscore and/or backslash characters only.');
559
        }
560
561 8
        list($namespace, $className) = $this->generateClassName($name);
562 8
        $migrationPath = $this->findMigrationPath($namespace);
563
564 8
        $file = $migrationPath . DIRECTORY_SEPARATOR . $className . '.php';
565 8
        if ($this->confirm("Create new migration '$file'?")) {
566 8
            $content = $this->generateMigrationSourceCode([
567 8
                'name' => $name,
568 8
                'className' => $className,
569 8
                'namespace' => $namespace,
570 8
            ]);
571 8
            FileHelper::createDirectory($migrationPath);
572 8
            file_put_contents($file, $content);
573 8
            $this->stdout("New migration created successfully.\n", Console::FG_GREEN);
574 8
        }
575 8
    }
576
577
    /**
578
     * Generates class base name and namespace from migration name from user input.
579
     * @param string $name migration name from user input.
580
     * @return array list of 2 elements: 'namespace' and 'class base name'
581
     * @since 2.0.10
582
     */
583 8
    private function generateClassName($name)
584
    {
585 8
        $namespace = null;
586 8
        $name = trim($name, '\\');
587 8
        if (strpos($name, '\\') !== false) {
588 1
            $namespace = substr($name, 0, strrpos($name, '\\'));
589 1
            $name = substr($name, strrpos($name, '\\') + 1);
590 1
        } else {
591 8
            if ($this->migrationPath === null) {
592 1
                $migrationNamespaces = $this->migrationNamespaces;
593 1
                $namespace = array_shift($migrationNamespaces);
594 1
            }
595
        }
596
597 8
        if ($namespace === null) {
598 8
            $class = 'm' . gmdate('ymd_His') . '_' . $name;
599 8
        } else {
600 1
            $class = 'M' . gmdate('ymdHis') . ucfirst($name);
601
        }
602
603 8
        return [$namespace, $class];
604
    }
605
606
    /**
607
     * Finds the file path for the specified migration namespace.
608
     * @param string|null $namespace migration namespace.
609
     * @return string migration file path.
610
     * @throws Exception on failure.
611
     * @since 2.0.10
612
     */
613 8
    private function findMigrationPath($namespace)
614
    {
615 8
        if (empty($namespace)) {
616 8
            return $this->migrationPath;
617
        }
618
619 1
        if (!in_array($namespace, $this->migrationNamespaces, true)) {
620
            throw new Exception("Namespace '{$namespace}' not found in `migrationNamespaces`");
621
        }
622
623 1
        return $this->getNamespacePath($namespace);
624
    }
625
626
    /**
627
     * Returns the file path matching the give namespace.
628
     * @param string $namespace namespace.
629
     * @return string file path.
630
     * @since 2.0.10
631
     */
632 6
    private function getNamespacePath($namespace)
633
    {
634 6
        return str_replace('/', DIRECTORY_SEPARATOR, Yii::getAlias('@' . str_replace('\\', '/', $namespace)));
635
    }
636
637
    /**
638
     * Upgrades with the specified migration class.
639
     * @param string $class the migration class name
640
     * @return boolean whether the migration is successful
641
     */
642 13
    protected function migrateUp($class)
643
    {
644 13
        if ($class === self::BASE_MIGRATION) {
645
            return true;
646
        }
647
648 13
        $this->stdout("*** applying $class\n", Console::FG_YELLOW);
649 13
        $start = microtime(true);
650 13
        $migration = $this->createMigration($class);
651 13
        if ($migration->up() !== false) {
652 13
            $this->addMigrationHistory($class);
653 13
            $time = microtime(true) - $start;
654 13
            $this->stdout("*** applied $class (time: " . sprintf('%.3f', $time) . "s)\n\n", Console::FG_GREEN);
655
656 13
            return true;
657
        } else {
658
            $time = microtime(true) - $start;
659
            $this->stdout("*** failed to apply $class (time: " . sprintf('%.3f', $time) . "s)\n\n", Console::FG_RED);
660
661
            return false;
662
        }
663
    }
664
665
    /**
666
     * Downgrades with the specified migration class.
667
     * @param string $class the migration class name
668
     * @return boolean whether the migration is successful
669
     */
670 5
    protected function migrateDown($class)
671
    {
672 5
        if ($class === self::BASE_MIGRATION) {
673
            return true;
674
        }
675
676 5
        $this->stdout("*** reverting $class\n", Console::FG_YELLOW);
677 5
        $start = microtime(true);
678 5
        $migration = $this->createMigration($class);
679 5
        if ($migration->down() !== false) {
680 5
            $this->removeMigrationHistory($class);
681 5
            $time = microtime(true) - $start;
682 5
            $this->stdout("*** reverted $class (time: " . sprintf('%.3f', $time) . "s)\n\n", Console::FG_GREEN);
683
684 5
            return true;
685
        } else {
686
            $time = microtime(true) - $start;
687
            $this->stdout("*** failed to revert $class (time: " . sprintf('%.3f', $time) . "s)\n\n", Console::FG_RED);
688
689
            return false;
690
        }
691
    }
692
693
    /**
694
     * Creates a new migration instance.
695
     * @param string $class the migration class name
696
     * @return \yii\db\MigrationInterface the migration instance
697
     */
698
    protected function createMigration($class)
699
    {
700
        $class = trim($class, '\\');
701
        if (strpos($class, '\\') === false) {
702
            $file = $this->migrationPath . DIRECTORY_SEPARATOR . $class . '.php';
703
            require_once($file);
704
        }
705
706
        return new $class();
707
    }
708
709
    /**
710
     * Migrates to the specified apply time in the past.
711
     * @param integer $time UNIX timestamp value.
712
     */
713
    protected function migrateToTime($time)
714
    {
715
        $count = 0;
716
        $migrations = array_values($this->getMigrationHistory(null));
717
        while ($count < count($migrations) && $migrations[$count] > $time) {
718
            ++$count;
719
        }
720
        if ($count === 0) {
721
            $this->stdout("Nothing needs to be done.\n", Console::FG_GREEN);
722
        } else {
723
            $this->actionDown($count);
724
        }
725
    }
726
727
    /**
728
     * Migrates to the certain version.
729
     * @param string $version name in the full format.
730
     * @return integer CLI exit code
731
     * @throws Exception if the provided version cannot be found.
732
     */
733 2
    protected function migrateToVersion($version)
734
    {
735 2
        $originalVersion = $version;
736
737
        // try migrate up
738 2
        $migrations = $this->getNewMigrations();
739 2
        foreach ($migrations as $i => $migration) {
740 2
            if (strpos($migration, $version) === 0) {
741 2
                $this->actionUp($i + 1);
742
743 2
                return self::EXIT_CODE_NORMAL;
744
            }
745
        }
746
747
        // try migrate down
748
        $migrations = array_keys($this->getMigrationHistory(null));
749
        foreach ($migrations as $i => $migration) {
750
            if (strpos($migration, $version) === 0) {
751
                if ($i === 0) {
752
                    $this->stdout("Already at '$originalVersion'. Nothing needs to be done.\n", Console::FG_YELLOW);
753
                } else {
754
                    $this->actionDown($i);
755
                }
756
757
                return self::EXIT_CODE_NORMAL;
758
            }
759
        }
760
761
        throw new Exception("Unable to find the version '$originalVersion'.");
762
    }
763
764
    /**
765
     * Returns the migrations that are not applied.
766
     * @return array list of new migrations
767
     */
768 15
    protected function getNewMigrations()
769
    {
770 15
        $applied = [];
771 15
        foreach ($this->getMigrationHistory(null) as $class => $time) {
772 1
            $applied[trim($class, '\\')] = true;
773 15
        }
774
775 15
        $migrationPaths = [];
776 15
        if (!empty($this->migrationPath)) {
777 10
            $migrationPaths[''] = $this->migrationPath;
778 10
        }
779 15
        foreach ($this->migrationNamespaces as $namespace) {
780 5
            $migrationPaths[$namespace] = $this->getNamespacePath($namespace);
781 15
        }
782
783 15
        $migrations = [];
784 15
        foreach ($migrationPaths as $namespace => $migrationPath) {
785 15
            if (!file_exists($migrationPath)) {
786
                continue;
787
            }
788 15
            $handle = opendir($migrationPath);
789 15
            while (($file = readdir($handle)) !== false) {
790 15
                if ($file === '.' || $file === '..') {
791 15
                    continue;
792
                }
793 15
                $path = $migrationPath . DIRECTORY_SEPARATOR . $file;
794 15
                if (preg_match('/^(m(\d{6}_?\d{6})\D.*?)\.php$/is', $file, $matches) && is_file($path)) {
795 15
                    $class = $matches[1];
796 15
                    if (!empty($namespace)) {
797 5
                        $class = $namespace . '\\' . $class;
798 5
                    }
799 15
                    $time = str_replace('_', '', $matches[2]);
800 15
                    if (!isset($applied[$class])) {
801 15
                        $migrations[$time . '\\' . $class] = $class;
802 15
                    }
803 15
                }
804 15
            }
805 15
            closedir($handle);
806 15
        }
807 15
        ksort($migrations);
808
809 15
        return array_values($migrations);
810
    }
811
812
    /**
813
     * Generates new migration source PHP code.
814
     * Child class may override this method, adding extra logic or variation to the process.
815
     * @param array $params generation parameters, usually following parameters are present:
816
     *
817
     *  - name: string migration base name
818
     *  - className: string migration class name
819
     *
820
     * @return string generated PHP code.
821
     * @since 2.0.8
822
     */
823
    protected function generateMigrationSourceCode($params)
824
    {
825
        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...
826
    }
827
828
    /**
829
     * Returns the migration history.
830
     * @param integer $limit the maximum number of records in the history to be returned. `null` for "no limit".
831
     * @return array the migration history
832
     */
833
    abstract protected function getMigrationHistory($limit);
834
835
    /**
836
     * Adds new migration entry to the history.
837
     * @param string $version migration version name.
838
     */
839
    abstract protected function addMigrationHistory($version);
840
841
    /**
842
     * Removes existing migration from the history.
843
     * @param string $version migration version name.
844
     */
845
    abstract protected function removeMigrationHistory($version);
846
}
847