Completed
Push — master ( 98a71d...264831 )
by
unknown
15:42
created

BaseMigrateController::truncateDatabase()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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