Completed
Push — master ( cd3c64...b8d5a3 )
by Carsten
37:00
created

BaseMigrateController::getNewMigrations()   D

Complexity

Conditions 15
Paths 126

Size

Total Lines 48
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 81.6765

Importance

Changes 0
Metric Value
dl 0
loc 48
ccs 1
cts 3
cp 0.3333
rs 4.7139
c 0
b 0
f 0
cc 15
eloc 32
nc 126
nop 0
crap 81.6765

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\console\controllers;
9
10
use Yii;
11
use yii\base\InvalidConfigException;
12
use yii\console\Exception;
13
use yii\console\Controller;
14
use yii\helpers\Console;
15
use yii\helpers\FileHelper;
16
use yii\helpers\StringHelper;
17
18
/**
19
 * BaseMigrateController is the base class for migrate controllers.
20
 *
21
 * @author Qiang Xue <[email protected]>
22
 * @since 2.0
23
 */
24
abstract class BaseMigrateController extends Controller
25
{
26
    /**
27
     * The name of the dummy migration that marks the beginning of the whole migration history.
28
     */
29
    const BASE_MIGRATION = 'm000000_000000_base';
30
31
    /**
32
     * @var string the default command action.
33
     */
34
    public $defaultAction = 'up';
35
    /**
36
     * @var string|array the directory containing the migration classes. This can be either
37
     * a [path alias](guide:concept-aliases) or a directory path.
38
     *
39
     * Migration classes located at this path should be declared without a namespace.
40
     * Use [[migrationNamespaces]] property in case you are using namespaced migrations.
41
     *
42
     * If you have set up [[migrationNamespaces]], you may set this field to `null` in order
43
     * to disable usage of migrations that are not namespaced.
44
     *
45
     * Since version 2.0.12 you may also specify an array of migration paths that should be searched for
46
     * migrations to load. This is mainly useful to support old extensions that provide migrations
47
     * without namespace and to adopt the new feature of namespaced migrations while keeping existing migrations.
48
     *
49
     * In general, to load migrations from different locations, [[migrationNamespaces]] is the preferable solution
50
     * as the migration name contains the origin of the migration in the history, which is not the case when
51
     * using multiple migration paths.
52
     *
53
     * @see $migrationNamespaces
54
     */
55
    public $migrationPath = ['@app/migrations'];
56
    /**
57
     * @var array list of namespaces containing the migration classes.
58
     *
59
     * Migration namespaces should be resolvable as a [path alias](guide:concept-aliases) if prefixed with `@`, e.g. if you specify
60
     * the namespace `app\migrations`, the code `Yii::getAlias('@app/migrations')` should be able to return
61
     * the file path to the directory this namespace refers to.
62
     * This corresponds with the [autoloading conventions](guide:concept-autoloading) of Yii.
63
     *
64
     * For example:
65
     *
66
     * ```php
67
     * [
68
     *     'app\migrations',
69
     *     'some\extension\migrations',
70
     * ]
71
     * ```
72
     *
73
     * @since 2.0.10
74
     * @see $migrationPath
75 22
     */
76
    public $migrationNamespaces = [];
77 22
    /**
78 22
     * @var string the template file for generating new migrations.
79 22
     * This can be either a [path alias](guide:concept-aliases) (e.g. "@app/migrations/template.php")
80 22
     * or a file path.
81
     */
82
    public $templateFile;
83
84
85
    /**
86
     * @inheritdoc
87
     */
88
    public function options($actionID)
89
    {
90
        return array_merge(
91 30
            parent::options($actionID),
92
            ['migrationPath', 'migrationNamespaces'], // global for all actions
93 30
            $actionID === 'create' ? ['templateFile'] : [] // action create
94 30
        );
95
    }
96
97
    /**
98 30
     * This method is invoked right before an action is to be executed (after all possible filters.)
99 7
     * 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 30
     * @return bool whether the action should continue to be executed.
103 24
     */
104 24
    public function beforeAction($action)
105 5
    {
106
        if (parent::beforeAction($action)) {
107
            if (empty($this->migrationNamespaces) && empty($this->migrationPath)) {
108 5
                throw new InvalidConfigException('At least one of `migrationPath` or `migrationNamespaces` should be specified.');
109
            }
110 24
111
            foreach ($this->migrationNamespaces as $key => $value) {
112
                $this->migrationNamespaces[$key] = trim($value, '\\');
113 30
            }
114 30
115
            if (is_array($this->migrationPath)) {
116 30
                foreach($this->migrationPath as $i => $path) {
117
                    $this->migrationPath[$i] = Yii::getAlias($path);
118
                }
119
            } elseif ($this->migrationPath !== null) {
120
                $path = Yii::getAlias($this->migrationPath);
121
                if (!is_dir($path)) {
122
                    if ($action->id !== 'create') {
123
                        throw new InvalidConfigException("Migration failed. Directory specified in migrationPath doesn't exist: {$this->migrationPath}");
124
                    }
125
                    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
                $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
            $version = Yii::getVersion();
131
            $this->stdout("Yii Migration Tool (based on Yii v{$version})\n\n");
132
133
            return true;
134
        } else {
135
            return false;
136 19
        }
137
    }
138 19
139 19
    /**
140
     * Upgrades the application by applying new migrations.
141
     * For example,
142
     *
143
     * ```
144
     * yii migrate     # apply all new migrations
145 19
     * yii migrate 3   # apply the first 3 new migrations
146 19
     * ```
147 19
     *
148 3
     * @param int $limit the number of new migrations to be applied. If 0, it means
149
     * applying all available new migrations.
150
     *
151 19
     * @return int the status of the action execution. 0 means normal, other values mean abnormal.
152 19
     */
153 18
    public function actionUp($limit = 0)
154
    {
155 1
        $migrations = $this->getNewMigrations();
156
        if (empty($migrations)) {
157
            $this->stdout("No new migrations found. Your system is up-to-date.\n", Console::FG_GREEN);
158 19
159 19
            return self::EXIT_CODE_NORMAL;
160
        }
161 19
162
        $total = count($migrations);
163 19
        $limit = (int) $limit;
164 19
        if ($limit > 0) {
165 19
            $migrations = array_slice($migrations, 0, $limit);
166 19
        }
167
168
        $n = count($migrations);
169
        if ($n === $total) {
170
            $this->stdout("Total $n new " . ($n === 1 ? 'migration' : 'migrations') . " to be applied:\n", Console::FG_YELLOW);
171
        } else {
172 19
            $this->stdout("Total $n out of $total new " . ($total === 1 ? 'migration' : 'migrations') . " to be applied:\n", Console::FG_YELLOW);
173
        }
174
175 19
        foreach ($migrations as $migration) {
176 19
            $this->stdout("\t$migration\n");
177
        }
178 19
        $this->stdout("\n");
179
180
        $applied = 0;
181
        if ($this->confirm('Apply the above ' . ($n === 1 ? 'migration' : 'migrations') . '?')) {
182
            foreach ($migrations as $migration) {
183
                if (!$this->migrateUp($migration)) {
184
                    $this->stdout("\n$applied from $n " . ($applied === 1 ? 'migration was' : 'migrations were') ." applied.\n", Console::FG_RED);
185
                    $this->stdout("\nMigration failed. The rest of the migrations are canceled.\n", Console::FG_RED);
186
187
                    return self::EXIT_CODE_ERROR;
188
                }
189
                $applied++;
190
            }
191
192
            $this->stdout("\n$n " . ($n === 1 ? 'migration was' : 'migrations were') ." applied.\n", Console::FG_GREEN);
193
            $this->stdout("\nMigrated up successfully.\n", Console::FG_GREEN);
194
        }
195
    }
196 10
197
    /**
198 10
     * Downgrades the application by reverting old migrations.
199 1
     * For example,
200
     *
201 9
     * ```
202 9
     * 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 10
     * @param int $limit the number of migrations to be reverted. Defaults to 1,
208
     * meaning the last applied migration will be reverted.
209 10
     * @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
    public function actionDown($limit = 1)
214
    {
215 10
        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
            $limit = null;
217 10
        } else {
218 10
            $limit = (int) $limit;
219 10
            if ($limit < 1) {
220 10
                throw new Exception('The step argument must be greater than 0.');
221
            }
222 10
        }
223
224 10
        $migrations = $this->getMigrationHistory($limit);
225 10
226 10
        if (empty($migrations)) {
227 10
            $this->stdout("No migration has been done before.\n", Console::FG_YELLOW);
228
229
            return self::EXIT_CODE_NORMAL;
230
        }
231
232
        $migrations = array_keys($migrations);
233 10
234
        $n = count($migrations);
235 10
        $this->stdout("Total $n " . ($n === 1 ? 'migration' : 'migrations') . " to be reverted:\n", Console::FG_YELLOW);
236 10
        foreach ($migrations as $migration) {
237
            $this->stdout("\t$migration\n");
238 10
        }
239
        $this->stdout("\n");
240
241
        $reverted = 0;
242
        if ($this->confirm('Revert the above ' . ($n === 1 ? 'migration' : 'migrations') . '?')) {
243
            foreach ($migrations as $migration) {
244
                if (!$this->migrateDown($migration)) {
245
                    $this->stdout("\n$reverted from $n " . ($reverted === 1 ? 'migration was' : 'migrations were') ." reverted.\n", Console::FG_RED);
246
                    $this->stdout("\nMigration failed. The rest of the migrations are canceled.\n", Console::FG_RED);
247
248
                    return self::EXIT_CODE_ERROR;
249
                }
250
                $reverted++;
251
            }
252
            $this->stdout("\n$n " . ($n === 1 ? 'migration was' : 'migrations were') ." reverted.\n", Console::FG_GREEN);
253
            $this->stdout("\nMigrated down successfully.\n", Console::FG_GREEN);
254
        }
255
    }
256
257
    /**
258 1
     * Redoes the last few migrations.
259
     *
260 1
     * This command will first revert the specified migrations, and then apply
261
     * them again. For example,
262
     *
263 1
     * ```
264 1
     * 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 1
     * @param int $limit the number of migrations to be redone. Defaults to 1,
270
     * meaning the last applied migration will be redone.
271 1
     * @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
    public function actionRedo($limit = 1)
276
    {
277 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...
278
            $limit = null;
279 1
        } else {
280 1
            $limit = (int) $limit;
281 1
            if ($limit < 1) {
282 1
                throw new Exception('The step argument must be greater than 0.');
283
            }
284 1
        }
285
286 1
        $migrations = $this->getMigrationHistory($limit);
287 1
288 1
        if (empty($migrations)) {
289
            $this->stdout("No migration has been done before.\n", Console::FG_YELLOW);
290
291
            return self::EXIT_CODE_NORMAL;
292
        }
293
294 1
        $migrations = array_keys($migrations);
295 1
296
        $n = count($migrations);
297
        $this->stdout("Total $n " . ($n === 1 ? 'migration' : 'migrations') . " to be redone:\n", Console::FG_YELLOW);
298
        foreach ($migrations as $migration) {
299
            $this->stdout("\t$migration\n");
300
        }
301 1
        $this->stdout("\n");
302 1
303
        if ($this->confirm('Redo the above ' . ($n === 1 ? 'migration' : 'migrations') . '?')) {
304 1
            foreach ($migrations as $migration) {
305
                if (!$this->migrateDown($migration)) {
306
                    $this->stdout("\nMigration failed. The rest of the migrations are canceled.\n", Console::FG_RED);
307
308
                    return self::EXIT_CODE_ERROR;
309
                }
310
            }
311
            foreach (array_reverse($migrations) as $migration) {
312
                if (!$this->migrateUp($migration)) {
313
                    $this->stdout("\nMigration failed. The rest of the migrations are canceled.\n", Console::FG_RED);
314
315
                    return self::EXIT_CODE_ERROR;
316
                }
317
            }
318
            $this->stdout("\n$n " . ($n === 1 ? 'migration was' : 'migrations were') ." redone.\n", Console::FG_GREEN);
319
            $this->stdout("\nMigration redone successfully.\n", Console::FG_GREEN);
320
        }
321
    }
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 2
     * This command will first revert the specified migrations, and then apply
331
     * them again. For example,
332 2
     *
333 1
     * ```
334 1
     * yii migrate/to 101129_185401                          # using timestamp
335 1
     * 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 2
     * 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
    public function actionTo($version)
348
    {
349
        if (($namespaceVersion = $this->extractNamespaceMigrationVersion($version)) !== false) {
350
            $this->migrateToVersion($namespaceVersion);
351
        } elseif (($migrationName = $this->extractMigrationVersion($version)) !== false) {
352
            $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
    }
361 2
362
    /**
363 2
     * Modifies the migration history to the specified version.
364 2
     *
365 1
     * No actual migration will be performed.
366 1
     *
367 1
     * ```
368
     * yii migrate/mark 101129_185401                        # using timestamp
369
     * yii migrate/mark m101129_185401_create_user_table     # using full name
370
     * yii migrate/to app\migrations\M101129185401CreateUser # using full namespace name
371
     * ```
372
     *
373 2
     * @param string $version the version at which the migration history should be marked.
374 2
     * This can be either the timestamp or the full name of the migration.
375 2
     * @return int CLI exit code
376 2
     * @throws Exception if the version argument is invalid or the version cannot be found.
377 2
     */
378 2
    public function actionMark($version)
379
    {
380 2
        $originalVersion = $version;
381
        if (($namespaceVersion = $this->extractNamespaceMigrationVersion($version)) !== false) {
382
            $version = $namespaceVersion;
383 2
        } elseif (($migrationName = $this->extractMigrationVersion($version)) !== false) {
384
            $version = $migrationName;
385
        } else {
386
            throw new Exception("The version argument must be either a timestamp (e.g. 101129_185401)\nor the full name of a migration (e.g. m101129_185401_create_user_table)\nor the full name of a namespaced migration (e.g. app\\migrations\\M101129185401CreateUserTable).");
387
        }
388
389
        // try mark up
390
        $migrations = $this->getNewMigrations();
391
        foreach ($migrations as $i => $migration) {
392
            if (strpos($migration, $version) === 0) {
393
                if ($this->confirm("Set migration history at $originalVersion?")) {
394
                    for ($j = 0; $j <= $i; ++$j) {
395
                        $this->addMigrationHistory($migrations[$j]);
396
                    }
397
                    $this->stdout("The migration history is set at $originalVersion.\nNo actual migration was performed.\n", Console::FG_GREEN);
398
                }
399
400
                return self::EXIT_CODE_NORMAL;
401
            }
402
        }
403
404
        // try mark down
405
        $migrations = array_keys($this->getMigrationHistory(null));
406
        foreach ($migrations as $i => $migration) {
407
            if (strpos($migration, $version) === 0) {
408
                if ($i === 0) {
409
                    $this->stdout("Already at '$originalVersion'. Nothing needs to be done.\n", Console::FG_YELLOW);
410
                } else {
411
                    if ($this->confirm("Set migration history at $originalVersion?")) {
412
                        for ($j = 0; $j < $i; ++$j) {
413
                            $this->removeMigrationHistory($migrations[$j]);
414
                        }
415 4
                        $this->stdout("The migration history is set at $originalVersion.\nNo actual migration was performed.\n", Console::FG_GREEN);
416
                    }
417 4
                }
418 2
419
                return self::EXIT_CODE_NORMAL;
420 2
            }
421
        }
422
423
        throw new Exception("Unable to find the version '$originalVersion'.");
424
    }
425
426
    /**
427
     * Checks if given migration version specification matches namespaced migration name.
428
     * @param string $rawVersion raw version specification received from user input.
429 2
     * @return string|false actual migration version, `false` - if not match.
430
     * @since 2.0.10
431 2
     */
432 2
    private function extractNamespaceMigrationVersion($rawVersion)
433
    {
434
        if (preg_match('/^\\\\?([\w_]+\\\\)+m(\d{6}_?\d{6})(\D.*)?$/is', $rawVersion, $matches)) {
435
            return trim($rawVersion, '\\');
436
        }
437
        return false;
438
    }
439
440
    /**
441
     * Checks if given migration version specification matches migration base name.
442
     * @param string $rawVersion raw version specification received from user input.
443
     * @return string|false actual migration version, `false` - if not match.
444
     * @since 2.0.10
445
     */
446
    private function extractMigrationVersion($rawVersion)
447
    {
448
        if (preg_match('/^m?(\d{6}_?\d{6})(\D.*)?$/is', $rawVersion, $matches)) {
449
            return 'm' . $matches[1];
450
        }
451
        return false;
452
    }
453 4
454
    /**
455 4
     * Displays the migration history.
456
     *
457
     * This command will show the list of migrations that have been applied
458 4
     * so far. For example,
459 4
     *
460
     * ```
461
     * yii migrate/history     # showing the last 10 migrations
462
     * yii migrate/history 5   # showing the last 5 migrations
463
     * yii migrate/history all # showing the whole history
464 4
     * ```
465
     *
466 4
     * @param int $limit the maximum number of migrations to be displayed.
467 4
     * If it is "all", the whole migration history will be displayed.
468
     * @throws \yii\console\Exception if invalid limit value passed
469 2
     */
470 2
    public function actionHistory($limit = 10)
471 2
    {
472
        if ($limit === 'all') {
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison === seems to always evaluate to false as the types of $limit (integer) and 'all' (string) can never be identical. Maybe you want to use a loose comparison == instead?
Loading history...
473
            $limit = null;
474
        } else {
475 2
            $limit = (int) $limit;
476 2
            if ($limit < 1) {
477
                throw new Exception('The limit must be greater than 0.');
478
            }
479 4
        }
480
481
        $migrations = $this->getMigrationHistory($limit);
482
483
        if (empty($migrations)) {
484
            $this->stdout("No migration has been done before.\n", Console::FG_YELLOW);
485
        } else {
486
            $n = count($migrations);
487
            if ($limit > 0) {
488
                $this->stdout("Showing the last $n applied " . ($n === 1 ? 'migration' : 'migrations') . ":\n", Console::FG_YELLOW);
489
            } else {
490
                $this->stdout("Total $n " . ($n === 1 ? 'migration has' : 'migrations have') . " been applied before:\n", Console::FG_YELLOW);
491
            }
492
            foreach ($migrations as $version => $time) {
493
                $this->stdout("\t(" . date('Y-m-d H:i:s', $time) . ') ' . $version . "\n");
494
            }
495
        }
496
    }
497 1
498
    /**
499 1
     * Displays the un-applied new migrations.
500
     *
501
     * This command will show the new migrations that have not been applied.
502 1
     * For example,
503 1
     *
504
     * ```
505
     * yii migrate/new     # showing the first 10 new migrations
506
     * yii migrate/new 5   # showing the first 5 new migrations
507
     * yii migrate/new all # showing all new migrations
508 1
     * ```
509
     *
510 1
     * @param int $limit the maximum number of new migrations to be displayed.
511 1
     * If it is `all`, all available new migrations will be displayed.
512
     * @throws \yii\console\Exception if invalid limit value passed
513 1
     */
514 1
    public function actionNew($limit = 10)
515
    {
516
        if ($limit === 'all') {
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison === seems to always evaluate to false as the types of $limit (integer) and 'all' (string) can never be identical. Maybe you want to use a loose comparison == instead?
Loading history...
517
            $limit = null;
518 1
        } else {
519
            $limit = (int) $limit;
520
            if ($limit < 1) {
521 1
                throw new Exception('The limit must be greater than 0.');
522 1
            }
523
        }
524
525 1
        $migrations = $this->getNewMigrations();
526
527
        if (empty($migrations)) {
528
            $this->stdout("No new migrations found. Your system is up-to-date.\n", Console::FG_GREEN);
529
        } else {
530
            $n = count($migrations);
531
            if ($limit && $n > $limit) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $limit of type null|integer is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
532
                $migrations = array_slice($migrations, 0, $limit);
533
                $this->stdout("Showing $limit out of $n new " . ($n === 1 ? 'migration' : 'migrations') . ":\n", Console::FG_YELLOW);
534
            } else {
535
                $this->stdout("Found $n new " . ($n === 1 ? 'migration' : 'migrations') . ":\n", Console::FG_YELLOW);
536
            }
537
538
            foreach ($migrations as $migration) {
539
                $this->stdout("\t" . $migration . "\n");
540
            }
541
        }
542
    }
543
544
    /**
545
     * Creates a new migration.
546
     *
547
     * This command creates a new migration using the available migration template.
548
     * After using this command, developers should modify the created migration
549
     * skeleton by filling up the actual migration logic.
550
     *
551
     * ```
552
     * yii migrate/create create_user_table
553
     * ```
554
     *
555
     * In order to generate a namespaced migration, you should specify a namespace before the migration's name.
556
     * Note that backslash (`\`) is usually considered a special character in the shell, so you need to escape it
557
     * properly to avoid shell errors or incorrect behavior.
558 8
     * For example:
559
     *
560 8
     * ```
561
     * yii migrate/create 'app\\migrations\\createUserTable'
562
     * ```
563
     *
564 8
     * In case [[migrationPath]] is not set and no namespace is provided, the first entry of [[migrationNamespaces]] will be used.
565 8
     *
566
     * @param string $name the name of the new migration. This should only contain
567 8
     * letters, digits, underscores and/or backslashes.
568 8
     *
569 8
     * Note: If the migration name is of a special form, for example create_xxx or
570 8
     * drop_xxx, then the generated migration file will contain extra code,
571 8
     * in this case for creating/dropping tables.
572 8
     *
573
     * @throws Exception if the name argument is invalid.
574 8
     */
575 8
    public function actionCreate($name)
576 8
    {
577
        if (!preg_match('/^[\w\\\\]+$/', $name)) {
578 8
            throw new Exception('The migration name should contain letters, digits, underscore and/or backslash characters only.');
579
        }
580
581
        list($namespace, $className) = $this->generateClassName($name);
582
        $migrationPath = $this->findMigrationPath($namespace);
583
584
        $file = $migrationPath . DIRECTORY_SEPARATOR . $className . '.php';
585
        if ($this->confirm("Create new migration '$file'?")) {
586 8
            $content = $this->generateMigrationSourceCode([
587
                'name' => $name,
588 8
                'className' => $className,
589 8
                'namespace' => $namespace,
590 8
            ]);
591 1
            FileHelper::createDirectory($migrationPath);
592 1
            file_put_contents($file, $content);
593
            $this->stdout("New migration created successfully.\n", Console::FG_GREEN);
594 8
        }
595 1
    }
596 1
597
    /**
598
     * Generates class base name and namespace from migration name from user input.
599
     * @param string $name migration name from user input.
600 8
     * @return array list of 2 elements: 'namespace' and 'class base name'
601 8
     * @since 2.0.10
602
     */
603 1
    private function generateClassName($name)
604
    {
605
        $namespace = null;
606 8
        $name = trim($name, '\\');
607
        if (strpos($name, '\\') !== false) {
608
            $namespace = substr($name, 0, strrpos($name, '\\'));
609
            $name = substr($name, strrpos($name, '\\') + 1);
610
        } else {
611
            if ($this->migrationPath === null) {
612
                $migrationNamespaces = $this->migrationNamespaces;
613
                $namespace = array_shift($migrationNamespaces);
614
            }
615
        }
616 8
617
        if ($namespace === null) {
618 8
            $class = 'm' . gmdate('ymd_His') . '_' . $name;
619 8
        } else {
620
            $class = 'M' . gmdate('ymdHis') . ucfirst($name);
621
        }
622 1
623
        return [$namespace, $class];
624
    }
625
626 1
    /**
627
     * Finds the file path for the specified migration namespace.
628
     * @param string|null $namespace migration namespace.
629
     * @return string migration file path.
630
     * @throws Exception on failure.
631
     * @since 2.0.10
632
     */
633
    private function findMigrationPath($namespace)
634
    {
635 6
        if (empty($namespace)) {
636
            return is_array($this->migrationPath) ? reset($this->migrationPath) : $this->migrationPath;
637 6
        }
638
639
        if (!in_array($namespace, $this->migrationNamespaces, true)) {
640
            throw new Exception("Namespace '{$namespace}' not found in `migrationNamespaces`");
641
        }
642
643
        return $this->getNamespacePath($namespace);
644
    }
645 19
646
    /**
647 19
     * Returns the file path matching the give namespace.
648
     * @param string $namespace namespace.
649
     * @return string file path.
650
     * @since 2.0.10
651 19
     */
652 19
    private function getNamespacePath($namespace)
653 19
    {
654 19
        return str_replace('/', DIRECTORY_SEPARATOR, Yii::getAlias('@' . str_replace('\\', '/', $namespace)));
655 19
    }
656 19
657 19
    /**
658
     * Upgrades with the specified migration class.
659 19
     * @param string $class the migration class name
660
     * @return bool whether the migration is successful
661
     */
662
    protected function migrateUp($class)
663
    {
664
        if ($class === self::BASE_MIGRATION) {
665
            return true;
666
        }
667
668
        $this->stdout("*** applying $class\n", Console::FG_YELLOW);
669
        $start = microtime(true);
670
        $migration = $this->createMigration($class);
671
        if ($migration->up() !== false) {
672
            $this->addMigrationHistory($class);
673 11
            $time = microtime(true) - $start;
674
            $this->stdout("*** applied $class (time: " . sprintf('%.3f', $time) . "s)\n\n", Console::FG_GREEN);
675 11
676
            return true;
677
        } else {
678
            $time = microtime(true) - $start;
679 11
            $this->stdout("*** failed to apply $class (time: " . sprintf('%.3f', $time) . "s)\n\n", Console::FG_RED);
680 11
681 11
            return false;
682 11
        }
683 11
    }
684 11
685 11
    /**
686
     * Downgrades with the specified migration class.
687 11
     * @param string $class the migration class name
688
     * @return bool whether the migration is successful
689
     */
690
    protected function migrateDown($class)
691
    {
692
        if ($class === self::BASE_MIGRATION) {
693
            return true;
694
        }
695
696
        $this->stdout("*** reverting $class\n", Console::FG_YELLOW);
697
        $start = microtime(true);
698
        $migration = $this->createMigration($class);
699
        if ($migration->down() !== false) {
700
            $this->removeMigrationHistory($class);
701
            $time = microtime(true) - $start;
702
            $this->stdout("*** reverted $class (time: " . sprintf('%.3f', $time) . "s)\n\n", Console::FG_GREEN);
703
704
            return true;
705
        } else {
706
            $time = microtime(true) - $start;
707
            $this->stdout("*** failed to revert $class (time: " . sprintf('%.3f', $time) . "s)\n\n", Console::FG_RED);
708
709
            return false;
710
        }
711
    }
712
713
    /**
714
     * Creates a new migration instance.
715
     * @param string $class the migration class name
716
     * @return \yii\db\MigrationInterface the migration instance
717
     */
718
    protected function createMigration($class)
719
    {
720
        $this->includeMigrationFile($class);
721
        return new $class();
722
    }
723
724
    /**
725
     * Includes the migration file for a given migration class name.
726
     *
727
     * This function will do nothing on namespaced migrations, which are loaded by
728
     * autoloading automatically. It will include the migration file, by searching
729
     * [[migrationPath]] for classes without namespace.
730
     * @param string $class the migration class name.
731
     * @since 2.0.12
732
     */
733
    protected function includeMigrationFile($class)
734
    {
735
        $class = trim($class, '\\');
736 2
        if (strpos($class, '\\') === false) {
737
            if (is_array($this->migrationPath)) {
738 2
                foreach($this->migrationPath as $path) {
739
                    $file = $path . DIRECTORY_SEPARATOR . $class . '.php';
740
                    if (is_file($file)) {
741 2
                        require_once($file);
742 2
                        break;
743 2
                    }
744 2
                }
745
            } else {
746 2
                $file = $this->migrationPath . DIRECTORY_SEPARATOR . $class . '.php';
747
                require_once($file);
748
            }
749
        }
750
    }
751
752
    /**
753
     * Migrates to the specified apply time in the past.
754
     * @param int $time UNIX timestamp value.
755
     */
756
    protected function migrateToTime($time)
757
    {
758
        $count = 0;
759
        $migrations = array_values($this->getMigrationHistory(null));
760
        while ($count < count($migrations) && $migrations[$count] > $time) {
761
            ++$count;
762
        }
763
        if ($count === 0) {
764
            $this->stdout("Nothing needs to be done.\n", Console::FG_GREEN);
765
        } else {
766
            $this->actionDown($count);
767
        }
768
    }
769
770
    /**
771 21
     * Migrates to the certain version.
772
     * @param string $version name in the full format.
773 21
     * @return int CLI exit code
774 21
     * @throws Exception if the provided version cannot be found.
775 1
     */
776
    protected function migrateToVersion($version)
777
    {
778 21
        $originalVersion = $version;
779 21
780 16
        // try migrate up
781
        $migrations = $this->getNewMigrations();
782 21
        foreach ($migrations as $i => $migration) {
783 5
            if (strpos($migration, $version) === 0) {
784
                $this->actionUp($i + 1);
785
786 21
                return self::EXIT_CODE_NORMAL;
787 21
            }
788 21
        }
789
790
        // try migrate down
791 21
        $migrations = array_keys($this->getMigrationHistory(null));
792 21
        foreach ($migrations as $i => $migration) {
793 21
            if (strpos($migration, $version) === 0) {
794 21
                if ($i === 0) {
795
                    $this->stdout("Already at '$originalVersion'. Nothing needs to be done.\n", Console::FG_YELLOW);
796 21
                } else {
797 21
                    $this->actionDown($i);
798 21
                }
799 21
800 5
                return self::EXIT_CODE_NORMAL;
801
            }
802 21
        }
803 21
804 21
        throw new Exception("Unable to find the version '$originalVersion'.");
805
    }
806
807
    /**
808 21
     * Returns the migrations that are not applied.
809
     * @return array list of new migrations
810 21
     */
811
    protected function getNewMigrations()
812 21
    {
813
        $applied = [];
814
        foreach ($this->getMigrationHistory(null) as $class => $time) {
815
            $applied[trim($class, '\\')] = true;
816
        }
817
818
        $migrationPaths = [];
819
        if (is_array($this->migrationPath)) {
820
            foreach($this->migrationPath as $path) {
821
                $migrationPaths[] = [$path, ''];
822
            }
823
        } elseif (!empty($this->migrationPath)) {
824
            $migrationPaths[] = [$this->migrationPath, ''];
825
        }
826
        foreach ($this->migrationNamespaces as $namespace) {
827
            $migrationPaths[] = [$this->getNamespacePath($namespace), $namespace];
828
        }
829
830
        $migrations = [];
831
        foreach ($migrationPaths as $item) {
832
            list($migrationPath, $namespace) = $item;
833
            if (!file_exists($migrationPath)) {
834
                continue;
835
            }
836
            $handle = opendir($migrationPath);
837
            while (($file = readdir($handle)) !== false) {
838
                if ($file === '.' || $file === '..') {
839
                    continue;
840
                }
841
                $path = $migrationPath . DIRECTORY_SEPARATOR . $file;
842
                if (preg_match('/^(m(\d{6}_?\d{6})\D.*?)\.php$/is', $file, $matches) && is_file($path)) {
843
                    $class = $matches[1];
844
                    if (!empty($namespace)) {
845
                        $class = $namespace . '\\' . $class;
846
                    }
847
                    $time = str_replace('_', '', $matches[2]);
848
                    if (!isset($applied[$class])) {
849
                        $migrations[$time . '\\' . $class] = $class;
850
                    }
851
                }
852
            }
853
            closedir($handle);
854
        }
855
        ksort($migrations);
856
857
        return array_values($migrations);
858
    }
859
860
    /**
861
     * Generates new migration source PHP code.
862
     * Child class may override this method, adding extra logic or variation to the process.
863
     * @param array $params generation parameters, usually following parameters are present:
864
     *
865
     *  - name: string migration base name
866
     *  - className: string migration class name
867
     *
868
     * @return string generated PHP code.
869
     * @since 2.0.8
870
     */
871
    protected function generateMigrationSourceCode($params)
872
    {
873
        return $this->renderFile(Yii::getAlias($this->templateFile), $params);
0 ignored issues
show
Bug introduced by
It seems like \Yii::getAlias($this->templateFile) targeting yii\BaseYii::getAlias() can also be of type boolean; however, yii\base\Controller::renderFile() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
874
    }
875
876
    /**
877
     * Returns the migration history.
878
     * @param int $limit the maximum number of records in the history to be returned. `null` for "no limit".
879
     * @return array the migration history
880
     */
881
    abstract protected function getMigrationHistory($limit);
882
883
    /**
884
     * Adds new migration entry to the history.
885
     * @param string $version migration version name.
886
     */
887
    abstract protected function addMigrationHistory($version);
888
889
    /**
890
     * Removes existing migration from the history.
891
     * @param string $version migration version name.
892
     */
893
    abstract protected function removeMigrationHistory($version);
894
}
895