Completed
Push — master ( 58792c...b7079d )
by Dmitry
18:00 queued 08:36
created

BaseMigrateController::extractMigrationVersion()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.0625

Importance

Changes 0
Metric Value
dl 0
loc 7
ccs 3
cts 4
cp 0.75
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 4
nc 2
nop 1
crap 2.0625
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\Controller;
13
use yii\console\Exception;
14
use yii\console\ExitCode;
15
use yii\helpers\Console;
16
use yii\helpers\FileHelper;
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 24
    public function options($actionID)
89
    {
90 24
        return array_merge(
91 24
            parent::options($actionID),
92 24
            ['migrationPath', 'migrationNamespaces'], // global for all actions
93 24
            $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 33
    public function beforeAction($action)
105
    {
106 33
        if (parent::beforeAction($action)) {
107 33
            if (empty($this->migrationNamespaces) && empty($this->migrationPath)) {
108
                throw new InvalidConfigException('At least one of `migrationPath` or `migrationNamespaces` should be specified.');
109
            }
110
111 33
            foreach ($this->migrationNamespaces as $key => $value) {
112 8
                $this->migrationNamespaces[$key] = trim($value, '\\');
113
            }
114
115 33
            if (is_array($this->migrationPath)) {
116 7
                foreach ($this->migrationPath as $i => $path) {
117 7
                    $this->migrationPath[$i] = Yii::getAlias($path);
118
                }
119 26
            } elseif ($this->migrationPath !== null) {
120 20
                $path = Yii::getAlias($this->migrationPath);
121 20
                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 20
                $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 33
            $version = Yii::getVersion();
131 33
            $this->stdout("Yii Migration Tool (based on Yii v{$version})\n\n");
132
133 33
            return true;
134
        }
135
136
        return false;
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 22
    public function actionUp($limit = 0)
154
    {
155 22
        $migrations = $this->getNewMigrations();
156 22
        if (empty($migrations)) {
157 1
            $this->stdout("No new migrations found. Your system is up-to-date.\n", Console::FG_GREEN);
158
159 1
            return ExitCode::OK;
160
        }
161
162 21
        $total = count($migrations);
163 21
        $limit = (int) $limit;
164 21
        if ($limit > 0) {
165 4
            $migrations = array_slice($migrations, 0, $limit);
166
        }
167
168 21
        $n = count($migrations);
169 21
        if ($n === $total) {
170 20
            $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 21
        foreach ($migrations as $migration) {
176 21
            $this->stdout("\t$migration\n");
177
        }
178 21
        $this->stdout("\n");
179
180 21
        $applied = 0;
181 21
        if ($this->confirm('Apply the above ' . ($n === 1 ? 'migration' : 'migrations') . '?')) {
182 21
            foreach ($migrations as $migration) {
183 21
                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 ExitCode::UNSPECIFIED_ERROR;
188
                }
189 21
                $applied++;
190
            }
191
192 21
            $this->stdout("\n$n " . ($n === 1 ? 'migration was' : 'migrations were') . " applied.\n", Console::FG_GREEN);
193 21
            $this->stdout("\nMigrated up successfully.\n", Console::FG_GREEN);
194
        }
195 21
    }
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 ExitCode::OK;
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 ExitCode::UNSPECIFIED_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 ExitCode::OK;
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 2
                    return ExitCode::UNSPECIFIED_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 2
                    return ExitCode::UNSPECIFIED_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/mark app\migrations\M101129185401CreateUser # using full namespace name
371
     * yii migrate/mark m000000_000000_base # reset the complete migration history
372
     * ```
373
     *
374
     * @param string $version the version at which the migration history should be marked.
375
     * This can be either the timestamp or the full name of the migration.
376
     * You may specify the name `m000000_000000_base` to set the migration history to a
377
     * state where no migration has been applied.
378
     * @return int CLI exit code
379
     * @throws Exception if the version argument is invalid or the version cannot be found.
380
     */
381 4
    public function actionMark($version)
382
    {
383 4
        $originalVersion = $version;
384 4
        if (($namespaceVersion = $this->extractNamespaceMigrationVersion($version)) !== false) {
385 1
            $version = $namespaceVersion;
386 3
        } elseif (($migrationName = $this->extractMigrationVersion($version)) !== false) {
387 3
            $version = $migrationName;
388
        } elseif ($version !== static::BASE_MIGRATION) {
389
            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).");
390
        }
391
392
        // try mark up
393 4
        $migrations = $this->getNewMigrations();
394 4
        foreach ($migrations as $i => $migration) {
395 3
            if (strpos($migration, $version) === 0) {
396 3
                if ($this->confirm("Set migration history at $originalVersion?")) {
397 3
                    for ($j = 0; $j <= $i; ++$j) {
398 3
                        $this->addMigrationHistory($migrations[$j]);
399
                    }
400 3
                    $this->stdout("The migration history is set at $originalVersion.\nNo actual migration was performed.\n", Console::FG_GREEN);
401
                }
402
403 3
                return ExitCode::OK;
404
            }
405
        }
406
407
        // try mark down
408 1
        $migrations = array_keys($this->getMigrationHistory(null));
409 1
        $migrations[] = static::BASE_MIGRATION;
410 1
        foreach ($migrations as $i => $migration) {
411 1
            if (strpos($migration, $version) === 0) {
412 1
                if ($i === 0) {
413
                    $this->stdout("Already at '$originalVersion'. Nothing needs to be done.\n", Console::FG_YELLOW);
414
                } else {
415 1
                    if ($this->confirm("Set migration history at $originalVersion?")) {
416 1
                        for ($j = 0; $j < $i; ++$j) {
417 1
                            $this->removeMigrationHistory($migrations[$j]);
418
                        }
419 1
                        $this->stdout("The migration history is set at $originalVersion.\nNo actual migration was performed.\n", Console::FG_GREEN);
420
                    }
421
                }
422
423 1
                return ExitCode::OK;
424
            }
425
        }
426
427
        throw new Exception("Unable to find the version '$originalVersion'.");
428
    }
429
430
    /**
431
     * Truncates the whole database and starts the migration from the beginning.
432
     *
433
     * ```
434
     * yii migrate/fresh
435
     * ```
436
     *
437
     * @since 2.0.13
438
     */
439 1
    public function actionFresh()
440
    {
441 1
        if (YII_ENV_PROD) {
442
            $this->stdout("YII_ENV is set to 'prod'.\nRefreshing migrations is not possible on production systems.\n");
443
            return ExitCode::OK;
444
        }
445
446 1
        if ($this->confirm(
447 1
            "Are you sure you want to reset the database and start the migration from the beginning?\nAll data will be lost irreversibly!")) {
448 1
            $this->refreshDatabase();
449
        } else {
450
            $this->stdout('Action was cancelled by user. Nothing has been performed.');
451
        }
452 1
    }
453
454
    /**
455
     * Checks if given migration version specification matches namespaced migration name.
456
     * @param string $rawVersion raw version specification received from user input.
457
     * @return string|false actual migration version, `false` - if not match.
458
     * @since 2.0.10
459
     */
460 6
    private function extractNamespaceMigrationVersion($rawVersion)
461
    {
462 6
        if (preg_match('/^\\\\?([\w_]+\\\\)+m(\d{6}_?\d{6})(\D.*)?$/is', $rawVersion, $matches)) {
463 2
            return trim($rawVersion, '\\');
464
        }
465 4
        return false;
466
    }
467
468
    /**
469
     * Checks if given migration version specification matches migration base name.
470
     * @param string $rawVersion raw version specification received from user input.
471
     * @return string|false actual migration version, `false` - if not match.
472
     * @since 2.0.10
473
     */
474 4
    private function extractMigrationVersion($rawVersion)
475
    {
476 4
        if (preg_match('/^m?(\d{6}_?\d{6})(\D.*)?$/is', $rawVersion, $matches)) {
477 4
            return 'm' . $matches[1];
478
        }
479
        return false;
480
    }
481
482
    /**
483
     * Displays the migration history.
484
     *
485
     * This command will show the list of migrations that have been applied
486
     * so far. For example,
487
     *
488
     * ```
489
     * yii migrate/history     # showing the last 10 migrations
490
     * yii migrate/history 5   # showing the last 5 migrations
491
     * yii migrate/history all # showing the whole history
492
     * ```
493
     *
494
     * @param int|string $limit the maximum number of migrations to be displayed.
495
     * If it is "all", the whole migration history will be displayed.
496
     * @throws \yii\console\Exception if invalid limit value passed
497
     */
498 4
    public function actionHistory($limit = 10)
499
    {
500 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...
501
            $limit = null;
502
        } else {
503 4
            $limit = (int) $limit;
504 4
            if ($limit < 1) {
505
                throw new Exception('The limit must be greater than 0.');
506
            }
507
        }
508
509 4
        $migrations = $this->getMigrationHistory($limit);
510
511 4
        if (empty($migrations)) {
512 4
            $this->stdout("No migration has been done before.\n", Console::FG_YELLOW);
513
        } else {
514 2
            $n = count($migrations);
515 2
            if ($limit > 0) {
516 2
                $this->stdout("Showing the last $n applied " . ($n === 1 ? 'migration' : 'migrations') . ":\n", Console::FG_YELLOW);
517
            } else {
518
                $this->stdout("Total $n " . ($n === 1 ? 'migration has' : 'migrations have') . " been applied before:\n", Console::FG_YELLOW);
519
            }
520 2
            foreach ($migrations as $version => $time) {
521 2
                $this->stdout("\t(" . date('Y-m-d H:i:s', $time) . ') ' . $version . "\n");
522
            }
523
        }
524 4
    }
525
526
    /**
527
     * Displays the un-applied new migrations.
528
     *
529
     * This command will show the new migrations that have not been applied.
530
     * For example,
531
     *
532
     * ```
533
     * yii migrate/new     # showing the first 10 new migrations
534
     * yii migrate/new 5   # showing the first 5 new migrations
535
     * yii migrate/new all # showing all new migrations
536
     * ```
537
     *
538
     * @param int|string $limit the maximum number of new migrations to be displayed.
539
     * If it is `all`, all available new migrations will be displayed.
540
     * @throws \yii\console\Exception if invalid limit value passed
541
     */
542 1
    public function actionNew($limit = 10)
543
    {
544 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...
545
            $limit = null;
546
        } else {
547 1
            $limit = (int) $limit;
548 1
            if ($limit < 1) {
549
                throw new Exception('The limit must be greater than 0.');
550
            }
551
        }
552
553 1
        $migrations = $this->getNewMigrations();
554
555 1
        if (empty($migrations)) {
556 1
            $this->stdout("No new migrations found. Your system is up-to-date.\n", Console::FG_GREEN);
557
        } else {
558 1
            $n = count($migrations);
559 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...
560
                $migrations = array_slice($migrations, 0, $limit);
561
                $this->stdout("Showing $limit out of $n new " . ($n === 1 ? 'migration' : 'migrations') . ":\n", Console::FG_YELLOW);
562
            } else {
563 1
                $this->stdout("Found $n new " . ($n === 1 ? 'migration' : 'migrations') . ":\n", Console::FG_YELLOW);
564
            }
565
566 1
            foreach ($migrations as $migration) {
567 1
                $this->stdout("\t" . $migration . "\n");
568
            }
569
        }
570 1
    }
571
572
    /**
573
     * Creates a new migration.
574
     *
575
     * This command creates a new migration using the available migration template.
576
     * After using this command, developers should modify the created migration
577
     * skeleton by filling up the actual migration logic.
578
     *
579
     * ```
580
     * yii migrate/create create_user_table
581
     * ```
582
     *
583
     * In order to generate a namespaced migration, you should specify a namespace before the migration's name.
584
     * Note that backslash (`\`) is usually considered a special character in the shell, so you need to escape it
585
     * properly to avoid shell errors or incorrect behavior.
586
     * For example:
587
     *
588
     * ```
589
     * yii migrate/create 'app\\migrations\\createUserTable'
590
     * ```
591
     *
592
     * In case [[migrationPath]] is not set and no namespace is provided, the first entry of [[migrationNamespaces]] will be used.
593
     *
594
     * @param string $name the name of the new migration. This should only contain
595
     * letters, digits, underscores and/or backslashes.
596
     *
597
     * Note: If the migration name is of a special form, for example create_xxx or
598
     * drop_xxx, then the generated migration file will contain extra code,
599
     * in this case for creating/dropping tables.
600
     *
601
     * @throws Exception if the name argument is invalid.
602
     */
603 9
    public function actionCreate($name)
604
    {
605 9
        if (!preg_match('/^[\w\\\\]+$/', $name)) {
606
            throw new Exception('The migration name should contain letters, digits, underscore and/or backslash characters only.');
607
        }
608
609 9
        list($namespace, $className) = $this->generateClassName($name);
610 9
        $migrationPath = $this->findMigrationPath($namespace);
611
612 9
        $file = $migrationPath . DIRECTORY_SEPARATOR . $className . '.php';
613 9
        if ($this->confirm("Create new migration '$file'?")) {
614 9
            $content = $this->generateMigrationSourceCode([
615 9
                'name' => $name,
616 9
                'className' => $className,
617 9
                'namespace' => $namespace,
618
            ]);
619 9
            FileHelper::createDirectory($migrationPath);
620 9
            file_put_contents($file, $content);
621 9
            $this->stdout("New migration created successfully.\n", Console::FG_GREEN);
622
        }
623 9
    }
624
625
    /**
626
     * Generates class base name and namespace from migration name from user input.
627
     * @param string $name migration name from user input.
628
     * @return array list of 2 elements: 'namespace' and 'class base name'
629
     * @since 2.0.10
630
     */
631 9
    private function generateClassName($name)
632
    {
633 9
        $namespace = null;
634 9
        $name = trim($name, '\\');
635 9
        if (strpos($name, '\\') !== false) {
636 1
            $namespace = substr($name, 0, strrpos($name, '\\'));
637 1
            $name = substr($name, strrpos($name, '\\') + 1);
638
        } else {
639 9
            if ($this->migrationPath === null) {
640 1
                $migrationNamespaces = $this->migrationNamespaces;
641 1
                $namespace = array_shift($migrationNamespaces);
642
            }
643
        }
644
645 9
        if ($namespace === null) {
646 9
            $class = 'm' . gmdate('ymd_His') . '_' . $name;
647
        } else {
648 1
            $class = 'M' . gmdate('ymdHis') . ucfirst($name);
649
        }
650
651 9
        return [$namespace, $class];
652
    }
653
654
    /**
655
     * Finds the file path for the specified migration namespace.
656
     * @param string|null $namespace migration namespace.
657
     * @return string migration file path.
658
     * @throws Exception on failure.
659
     * @since 2.0.10
660
     */
661 9
    private function findMigrationPath($namespace)
662
    {
663 9
        if (empty($namespace)) {
664 9
            return is_array($this->migrationPath) ? reset($this->migrationPath) : $this->migrationPath;
665
        }
666
667 1
        if (!in_array($namespace, $this->migrationNamespaces, true)) {
668
            throw new Exception("Namespace '{$namespace}' not found in `migrationNamespaces`");
669
        }
670
671 1
        return $this->getNamespacePath($namespace);
672
    }
673
674
    /**
675
     * Returns the file path matching the give namespace.
676
     * @param string $namespace namespace.
677
     * @return string file path.
678
     * @since 2.0.10
679
     */
680 7
    private function getNamespacePath($namespace)
681
    {
682 7
        return str_replace('/', DIRECTORY_SEPARATOR, Yii::getAlias('@' . str_replace('\\', '/', $namespace)));
683
    }
684
685
    /**
686
     * Upgrades with the specified migration class.
687
     * @param string $class the migration class name
688
     * @return bool whether the migration is successful
689
     */
690 21
    protected function migrateUp($class)
691
    {
692 21
        if ($class === self::BASE_MIGRATION) {
693
            return true;
694
        }
695
696 21
        $this->stdout("*** applying $class\n", Console::FG_YELLOW);
697 21
        $start = microtime(true);
698 21
        $migration = $this->createMigration($class);
699 21
        if ($migration->up() !== false) {
700 21
            $this->addMigrationHistory($class);
701 21
            $time = microtime(true) - $start;
702 21
            $this->stdout("*** applied $class (time: " . sprintf('%.3f', $time) . "s)\n\n", Console::FG_GREEN);
703
704 21
            return true;
705
        }
706
707
        $time = microtime(true) - $start;
708
        $this->stdout("*** failed to apply $class (time: " . sprintf('%.3f', $time) . "s)\n\n", Console::FG_RED);
709
710
        return false;
711
    }
712
713
    /**
714
     * Downgrades with the specified migration class.
715
     * @param string $class the migration class name
716
     * @return bool whether the migration is successful
717
     */
718 12
    protected function migrateDown($class)
719
    {
720 12
        if ($class === self::BASE_MIGRATION) {
721
            return true;
722
        }
723
724 12
        $this->stdout("*** reverting $class\n", Console::FG_YELLOW);
725 12
        $start = microtime(true);
726 12
        $migration = $this->createMigration($class);
727 12
        if ($migration->down() !== false) {
728 12
            $this->removeMigrationHistory($class);
729 12
            $time = microtime(true) - $start;
730 12
            $this->stdout("*** reverted $class (time: " . sprintf('%.3f', $time) . "s)\n\n", Console::FG_GREEN);
731
732 12
            return true;
733
        }
734
735
        $time = microtime(true) - $start;
736
        $this->stdout("*** failed to revert $class (time: " . sprintf('%.3f', $time) . "s)\n\n", Console::FG_RED);
737
738
        return false;
739
    }
740
741
    /**
742
     * Creates a new migration instance.
743
     * @param string $class the migration class name
744
     * @return \yii\db\MigrationInterface the migration instance
745
     */
746
    protected function createMigration($class)
747
    {
748
        $this->includeMigrationFile($class);
749
        return new $class();
750
    }
751
752
    /**
753
     * Includes the migration file for a given migration class name.
754
     *
755
     * This function will do nothing on namespaced migrations, which are loaded by
756
     * autoloading automatically. It will include the migration file, by searching
757
     * [[migrationPath]] for classes without namespace.
758
     * @param string $class the migration class name.
759
     * @since 2.0.12
760
     */
761 21
    protected function includeMigrationFile($class)
762
    {
763 21
        $class = trim($class, '\\');
764 21
        if (strpos($class, '\\') === false) {
765 17
            if (is_array($this->migrationPath)) {
766 7
                foreach ($this->migrationPath as $path) {
767 7
                    $file = $path . DIRECTORY_SEPARATOR . $class . '.php';
768 7
                    if (is_file($file)) {
769 7
                        require_once $file;
770 7
                        break;
771
                    }
772
                }
773
            } else {
774 10
                $file = $this->migrationPath . DIRECTORY_SEPARATOR . $class . '.php';
775 10
                require_once $file;
776
            }
777
        }
778 21
    }
779
780
    /**
781
     * Migrates to the specified apply time in the past.
782
     * @param int $time UNIX timestamp value.
783
     */
784
    protected function migrateToTime($time)
785
    {
786
        $count = 0;
787
        $migrations = array_values($this->getMigrationHistory(null));
788
        while ($count < count($migrations) && $migrations[$count] > $time) {
789
            ++$count;
790
        }
791
        if ($count === 0) {
792
            $this->stdout("Nothing needs to be done.\n", Console::FG_GREEN);
793
        } else {
794
            $this->actionDown($count);
795
        }
796
    }
797
798
    /**
799
     * Migrates to the certain version.
800
     * @param string $version name in the full format.
801
     * @return int CLI exit code
802
     * @throws Exception if the provided version cannot be found.
803
     */
804 3
    protected function migrateToVersion($version)
805
    {
806 3
        $originalVersion = $version;
807
808
        // try migrate up
809 3
        $migrations = $this->getNewMigrations();
810 3
        foreach ($migrations as $i => $migration) {
811 2
            if (strpos($migration, $version) === 0) {
812 2
                $this->actionUp($i + 1);
813
814 2
                return ExitCode::OK;
815
            }
816
        }
817
818
        // try migrate down
819 1
        $migrations = array_keys($this->getMigrationHistory(null));
820 1
        foreach ($migrations as $i => $migration) {
821 1
            if (strpos($migration, $version) === 0) {
822 1
                if ($i === 0) {
823
                    $this->stdout("Already at '$originalVersion'. Nothing needs to be done.\n", Console::FG_YELLOW);
824
                } else {
825 1
                    $this->actionDown($i);
826
                }
827
828 1
                return ExitCode::OK;
829
            }
830
        }
831
832
        throw new Exception("Unable to find the version '$originalVersion'.");
833
    }
834
835
    /**
836
     * Returns the migrations that are not applied.
837
     * @return array list of new migrations
838
     */
839 24
    protected function getNewMigrations()
840
    {
841 24
        $applied = [];
842 24
        foreach ($this->getMigrationHistory(null) as $class => $time) {
843 3
            $applied[trim($class, '\\')] = true;
844
        }
845
846 24
        $migrationPaths = [];
847 24
        if (is_array($this->migrationPath)) {
848 7
            foreach ($this->migrationPath as $path) {
849 7
                $migrationPaths[] = [$path, ''];
850
            }
851 17
        } elseif (!empty($this->migrationPath)) {
852 12
            $migrationPaths[] = [$this->migrationPath, ''];
853
        }
854 24
        foreach ($this->migrationNamespaces as $namespace) {
855 6
            $migrationPaths[] = [$this->getNamespacePath($namespace), $namespace];
856
        }
857
858 24
        $migrations = [];
859 24
        foreach ($migrationPaths as $item) {
860 24
            list($migrationPath, $namespace) = $item;
861 24
            if (!file_exists($migrationPath)) {
862
                continue;
863
            }
864 24
            $handle = opendir($migrationPath);
865 24
            while (($file = readdir($handle)) !== false) {
866 24
                if ($file === '.' || $file === '..') {
867 24
                    continue;
868
                }
869 23
                $path = $migrationPath . DIRECTORY_SEPARATOR . $file;
870 23
                if (preg_match('/^(m(\d{6}_?\d{6})\D.*?)\.php$/is', $file, $matches) && is_file($path)) {
871 23
                    $class = $matches[1];
872 23
                    if (!empty($namespace)) {
873 6
                        $class = $namespace . '\\' . $class;
874
                    }
875 23
                    $time = str_replace('_', '', $matches[2]);
876 23
                    if (!isset($applied[$class])) {
877 23
                        $migrations[$time . '\\' . $class] = $class;
878
                    }
879
                }
880
            }
881 24
            closedir($handle);
882
        }
883 24
        ksort($migrations);
884
885 24
        return array_values($migrations);
886
    }
887
888
    /**
889
     * Generates new migration source PHP code.
890
     * Child class may override this method, adding extra logic or variation to the process.
891
     * @param array $params generation parameters, usually following parameters are present:
892
     *
893
     *  - name: string migration base name
894
     *  - className: string migration class name
895
     *
896
     * @return string generated PHP code.
897
     * @since 2.0.8
898
     */
899
    protected function generateMigrationSourceCode($params)
900
    {
901
        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...
902
    }
903
904
    /**
905
     * Truncates the database and reapplies all migrations from the beginning.
906
     *
907
     * This method will simply print a message in the base class implementation.
908
     * It should be overwritten in subclasses to implement the task of clearing the database.
909
     *
910
     * @since 2.0.13
911
     */
912
    protected function refreshDatabase()
913
    {
914
        $this->stdout('This command is not implemented in ' . get_class($this) . "\n");
915
    }
916
917
    /**
918
     * Returns the migration history.
919
     * @param int $limit the maximum number of records in the history to be returned. `null` for "no limit".
920
     * @return array the migration history
921
     */
922
    abstract protected function getMigrationHistory($limit);
923
924
    /**
925
     * Adds new migration entry to the history.
926
     * @param string $version migration version name.
927
     */
928
    abstract protected function addMigrationHistory($version);
929
930
    /**
931
     * Removes existing migration from the history.
932
     * @param string $version migration version name.
933
     */
934
    abstract protected function removeMigrationHistory($version);
935
}
936