Completed
Push — 2.1 ( 7c8525...0afc41 )
by Alexander
21:05 queued 16:02
created

BaseMigrateController::actionDown()   C

Complexity

Conditions 12
Paths 19

Size

Total Lines 43
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 13.5797

Importance

Changes 0
Metric Value
dl 0
loc 43
ccs 21
cts 27
cp 0.7778
rs 5.1612
c 0
b 0
f 0
cc 12
eloc 27
nc 19
nop 1
crap 13.5797

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