Completed
Push — fix-error-handling-regression ( ef2bf2 )
by Alexander
09:24
created

BaseMigrateController::addMigrationHistory()

Size

Total Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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