BaseMigrateController::includeMigrationFile()   A
last analyzed

Complexity

Conditions 5
Paths 5

Size

Total Lines 15
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
cc 5
eloc 11
nc 5
nop 1
dl 0
loc 15
ccs 0
cts 11
cp 0
crap 30
rs 9.6111
c 0
b 0
f 0
1
<?php
2
/**
3
 * @link https://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license https://www.yiiframework.com/license/
6
 */
7
8
namespace yii\console\controllers;
9
10
use Yii;
11
use yii\base\BaseObject;
12
use yii\base\InvalidConfigException;
13
use yii\base\NotSupportedException;
14
use yii\console\Controller;
15
use yii\console\Exception;
16
use yii\console\ExitCode;
17
use yii\db\MigrationInterface;
18
use yii\helpers\Console;
19
use yii\helpers\FileHelper;
20
use yii\helpers\Inflector;
21
22
/**
23
 * BaseMigrateController is the base class for migrate controllers.
24
 *
25
 * @author Qiang Xue <[email protected]>
26
 * @since 2.0
27
 */
28
abstract class BaseMigrateController extends Controller
29
{
30
    /**
31
     * The name of the dummy migration that marks the beginning of the whole migration history.
32
     */
33
    const BASE_MIGRATION = 'm000000_000000_base';
34
35
    /**
36
     * @var string the default command action.
37
     */
38
    public $defaultAction = 'up';
39
    /**
40
     * @var string|array|null the directory containing the migration classes. This can be either
41
     * a [path alias](guide:concept-aliases) or a directory path.
42
     *
43
     * Migration classes located at this path should be declared without a namespace.
44
     * Use [[migrationNamespaces]] property in case you are using namespaced migrations.
45
     *
46
     * If you have set up [[migrationNamespaces]], you may set this field to `null` in order
47
     * to disable usage of migrations that are not namespaced.
48
     *
49
     * Since version 2.0.12 you may also specify an array of migration paths that should be searched for
50
     * migrations to load. This is mainly useful to support old extensions that provide migrations
51
     * without namespace and to adopt the new feature of namespaced migrations while keeping existing migrations.
52
     *
53
     * In general, to load migrations from different locations, [[migrationNamespaces]] is the preferable solution
54
     * as the migration name contains the origin of the migration in the history, which is not the case when
55
     * using multiple migration paths.
56
     *
57
     * @see migrationNamespaces
58
     */
59
    public $migrationPath = ['@app/migrations'];
60
    /**
61
     * @var array list of namespaces containing the migration classes.
62
     *
63
     * Migration namespaces should be resolvable as a [path alias](guide:concept-aliases) if prefixed with `@`, e.g. if you specify
64
     * the namespace `app\migrations`, the code `Yii::getAlias('@app/migrations')` should be able to return
65
     * the file path to the directory this namespace refers to.
66
     * This corresponds with the [autoloading conventions](guide:concept-autoloading) of Yii.
67
     *
68
     * For example:
69
     *
70
     * ```php
71
     * [
72
     *     'app\migrations',
73
     *     'some\extension\migrations',
74
     * ]
75
     * ```
76
     *
77
     * @since 2.0.10
78
     * @see migrationPath
79
     */
80
    public $migrationNamespaces = [];
81
    /**
82
     * @var string the template file for generating new migrations.
83
     * This can be either a [path alias](guide:concept-aliases) (e.g. "@app/migrations/template.php")
84
     * or a file path.
85
     */
86
    public $templateFile;
87
    /**
88
     * @var int|null the permission to be set for newly generated migration files.
89
     * This value will be used by PHP chmod() function. No umask will be applied.
90
     * If not set, the permission will be determined by the current environment.
91
     * @since 2.0.43
92
     */
93
    public $newFileMode;
94
    /**
95
     * @var string|int|null the user and/or group ownership to be set for newly generated migration files.
96
     * If not set, the ownership will be determined by the current environment.
97
     * @since 2.0.43
98
     * @see FileHelper::changeOwnership()
99
     */
100
    public $newFileOwnership;
101
    /**
102
     * @var bool indicates whether the console output should be compacted.
103
     * If this is set to true, the individual commands ran within the migration will not be output to the console.
104
     * Default is false, in other words the output is fully verbose by default.
105
     * @since 2.0.13
106
     */
107
    public $compact = false;
108
109
110
    /**
111
     * {@inheritdoc}
112
     */
113
    public function options($actionID)
114
    {
115
        return array_merge(
116
            parent::options($actionID),
117
            ['migrationPath', 'migrationNamespaces', 'compact'], // global for all actions
118
            $actionID === 'create' ? ['templateFile'] : [] // action create
119
        );
120
    }
121
122
    /**
123
     * This method is invoked right before an action is to be executed (after all possible filters.)
124
     * It checks the existence of the [[migrationPath]].
125
     * @param \yii\base\Action $action the action to be executed.
126
     * @throws InvalidConfigException if directory specified in migrationPath doesn't exist and action isn't "create".
127
     * @return bool whether the action should continue to be executed.
128
     */
129
    public function beforeAction($action)
130
    {
131
        if (parent::beforeAction($action)) {
132
            if (empty($this->migrationNamespaces) && empty($this->migrationPath)) {
133
                throw new InvalidConfigException('At least one of `migrationPath` or `migrationNamespaces` should be specified.');
134
            }
135
136
            $this->migrationNamespaces = (array) $this->migrationNamespaces;
137
138
            foreach ($this->migrationNamespaces as $key => $value) {
139
                $this->migrationNamespaces[$key] = trim($value, '\\');
140
            }
141
142
            if (is_array($this->migrationPath)) {
143
                foreach ($this->migrationPath as $i => $path) {
144
                    $this->migrationPath[$i] = Yii::getAlias($path);
145
                }
146
            } elseif ($this->migrationPath !== null) {
147
                $path = Yii::getAlias($this->migrationPath);
148
                if (!is_dir($path)) {
0 ignored issues
show
Bug introduced by
It seems like $path can also be of type false; however, parameter $filename of is_dir() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

148
                if (!is_dir(/** @scrutinizer ignore-type */ $path)) {
Loading history...
149
                    if ($action->id !== 'create') {
150
                        throw new InvalidConfigException("Migration failed. Directory specified in migrationPath doesn't exist: {$this->migrationPath}");
151
                    }
152
                    FileHelper::createDirectory($path);
0 ignored issues
show
Bug introduced by
It seems like $path can also be of type false; however, parameter $path of yii\helpers\BaseFileHelper::createDirectory() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

152
                    FileHelper::createDirectory(/** @scrutinizer ignore-type */ $path);
Loading history...
153
                }
154
                $this->migrationPath = $path;
0 ignored issues
show
Documentation Bug introduced by
It seems like $path can also be of type false. However, the property $migrationPath is declared as type array|null|string. 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...
155
            }
156
157
            $version = Yii::getVersion();
158
            $this->stdout("Yii Migration Tool (based on Yii v{$version})\n\n");
159
160
            return true;
161
        }
162
163
        return false;
164
    }
165
166
    /**
167
     * Upgrades the application by applying new migrations.
168
     *
169
     * For example,
170
     *
171
     * ```
172
     * yii migrate     # apply all new migrations
173
     * yii migrate 3   # apply the first 3 new migrations
174
     * ```
175
     *
176
     * @param int $limit the number of new migrations to be applied. If 0, it means
177
     * applying all available new migrations.
178
     *
179
     * @return int the status of the action execution. 0 means normal, other values mean abnormal.
180
     */
181
    public function actionUp($limit = 0)
182
    {
183
        $migrations = $this->getNewMigrations();
184
        if (empty($migrations)) {
185
            $this->stdout("No new migrations found. Your system is up-to-date.\n", Console::FG_GREEN);
186
187
            return ExitCode::OK;
188
        }
189
190
        $total = count($migrations);
191
        $limit = (int) $limit;
192
        if ($limit > 0) {
193
            $migrations = array_slice($migrations, 0, $limit);
194
        }
195
196
        $n = count($migrations);
197
        if ($n === $total) {
198
            $this->stdout("Total $n new " . ($n === 1 ? 'migration' : 'migrations') . " to be applied:\n", Console::FG_YELLOW);
199
        } else {
200
            $this->stdout("Total $n out of $total new " . ($total === 1 ? 'migration' : 'migrations') . " to be applied:\n", Console::FG_YELLOW);
201
        }
202
203
        foreach ($migrations as $migration) {
204
            $nameLimit = $this->getMigrationNameLimit();
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $nameLimit is correct as $this->getMigrationNameLimit() targeting yii\console\controllers\...getMigrationNameLimit() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
205
            if ($nameLimit !== null && strlen($migration) > $nameLimit) {
206
                $this->stdout("\nThe migration name '$migration' is too long. Its not possible to apply this migration.\n", Console::FG_RED);
207
                return ExitCode::UNSPECIFIED_ERROR;
208
            }
209
            $this->stdout("\t$migration\n");
210
        }
211
        $this->stdout("\n");
212
213
        $applied = 0;
214
        if ($this->confirm('Apply the above ' . ($n === 1 ? 'migration' : 'migrations') . '?')) {
215
            foreach ($migrations as $migration) {
216
                if (!$this->migrateUp($migration)) {
217
                    $this->stdout("\n$applied from $n " . ($applied === 1 ? 'migration was' : 'migrations were') . " applied.\n", Console::FG_RED);
218
                    $this->stdout("\nMigration failed. The rest of the migrations are canceled.\n", Console::FG_RED);
219
220
                    return ExitCode::UNSPECIFIED_ERROR;
221
                }
222
                $applied++;
223
            }
224
225
            $this->stdout("\n$n " . ($n === 1 ? 'migration was' : 'migrations were') . " applied.\n", Console::FG_GREEN);
226
            $this->stdout("\nMigrated up successfully.\n", Console::FG_GREEN);
227
        }
228
229
        return ExitCode::OK;
230
    }
231
232
    /**
233
     * Downgrades the application by reverting old migrations.
234
     *
235
     * For example,
236
     *
237
     * ```
238
     * yii migrate/down     # revert the last migration
239
     * yii migrate/down 3   # revert the last 3 migrations
240
     * yii migrate/down all # revert all migrations
241
     * ```
242
     *
243
     * @param int|string $limit the number of migrations to be reverted. Defaults to 1,
244
     * meaning the last applied migration will be reverted. When value is "all", all migrations will be reverted.
245
     * @throws Exception if the number of the steps specified is less than 1.
246
     *
247
     * @return int the status of the action execution. 0 means normal, other values mean abnormal.
248
     */
249
    public function actionDown($limit = 1)
250
    {
251
        if ($limit === 'all') {
252
            $limit = null;
253
        } else {
254
            $limit = (int) $limit;
255
            if ($limit < 1) {
256
                throw new Exception('The step argument must be greater than 0.');
257
            }
258
        }
259
260
        $migrations = $this->getMigrationHistory($limit);
261
262
        if (empty($migrations)) {
263
            $this->stdout("No migration has been done before.\n", Console::FG_YELLOW);
264
265
            return ExitCode::OK;
266
        }
267
268
        $migrations = array_keys($migrations);
269
270
        $n = count($migrations);
271
        $this->stdout("Total $n " . ($n === 1 ? 'migration' : 'migrations') . " to be reverted:\n", Console::FG_YELLOW);
272
        foreach ($migrations as $migration) {
273
            $this->stdout("\t$migration\n");
274
        }
275
        $this->stdout("\n");
276
277
        $reverted = 0;
278
        if ($this->confirm('Revert the above ' . ($n === 1 ? 'migration' : 'migrations') . '?')) {
279
            foreach ($migrations as $migration) {
280
                if (!$this->migrateDown($migration)) {
281
                    $this->stdout("\n$reverted from $n " . ($reverted === 1 ? 'migration was' : 'migrations were') . " reverted.\n", Console::FG_RED);
282
                    $this->stdout("\nMigration failed. The rest of the migrations are canceled.\n", Console::FG_RED);
283
284
                    return ExitCode::UNSPECIFIED_ERROR;
285
                }
286
                $reverted++;
287
            }
288
            $this->stdout("\n$n " . ($n === 1 ? 'migration was' : 'migrations were') . " reverted.\n", Console::FG_GREEN);
289
            $this->stdout("\nMigrated down successfully.\n", Console::FG_GREEN);
290
        }
291
292
        return ExitCode::OK;
293
    }
294
295
    /**
296
     * Redoes the last few migrations.
297
     *
298
     * This command will first revert the specified migrations, and then apply
299
     * them again. For example,
300
     *
301
     * ```
302
     * yii migrate/redo     # redo the last applied migration
303
     * yii migrate/redo 3   # redo the last 3 applied migrations
304
     * yii migrate/redo all # redo all migrations
305
     * ```
306
     *
307
     * @param int|string $limit the number of migrations to be redone. Defaults to 1,
308
     * meaning the last applied migration will be redone. When equals "all", all migrations will be redone.
309
     * @throws Exception if the number of the steps specified is less than 1.
310
     *
311
     * @return int the status of the action execution. 0 means normal, other values mean abnormal.
312
     */
313
    public function actionRedo($limit = 1)
314
    {
315
        if ($limit === 'all') {
316
            $limit = null;
317
        } else {
318
            $limit = (int) $limit;
319
            if ($limit < 1) {
320
                throw new Exception('The step argument must be greater than 0.');
321
            }
322
        }
323
324
        $migrations = $this->getMigrationHistory($limit);
325
326
        if (empty($migrations)) {
327
            $this->stdout("No migration has been done before.\n", Console::FG_YELLOW);
328
329
            return ExitCode::OK;
330
        }
331
332
        $migrations = array_keys($migrations);
333
334
        $n = count($migrations);
335
        $this->stdout("Total $n " . ($n === 1 ? 'migration' : 'migrations') . " to be redone:\n", Console::FG_YELLOW);
336
        foreach ($migrations as $migration) {
337
            $this->stdout("\t$migration\n");
338
        }
339
        $this->stdout("\n");
340
341
        if ($this->confirm('Redo the above ' . ($n === 1 ? 'migration' : 'migrations') . '?')) {
342
            foreach ($migrations as $migration) {
343
                if (!$this->migrateDown($migration)) {
344
                    $this->stdout("\nMigration failed. The rest of the migrations are canceled.\n", Console::FG_RED);
345
346
                    return ExitCode::UNSPECIFIED_ERROR;
347
                }
348
            }
349
            foreach (array_reverse($migrations) as $migration) {
350
                if (!$this->migrateUp($migration)) {
351
                    $this->stdout("\nMigration failed. The rest of the migrations are canceled.\n", Console::FG_RED);
352
353
                    return ExitCode::UNSPECIFIED_ERROR;
354
                }
355
            }
356
            $this->stdout("\n$n " . ($n === 1 ? 'migration was' : 'migrations were') . " redone.\n", Console::FG_GREEN);
357
            $this->stdout("\nMigration redone successfully.\n", Console::FG_GREEN);
358
        }
359
360
        return ExitCode::OK;
361
    }
362
363
    /**
364
     * Upgrades or downgrades till the specified version.
365
     *
366
     * Can also downgrade versions to the certain apply time in the past by providing
367
     * a UNIX timestamp or a string parseable by the strtotime() function. This means
368
     * that all the versions applied after the specified certain time would be reverted.
369
     *
370
     * This command will first revert the specified migrations, and then apply
371
     * them again. For example,
372
     *
373
     * ```
374
     * yii migrate/to 101129_185401                          # using timestamp
375
     * yii migrate/to m101129_185401_create_user_table       # using full name
376
     * yii migrate/to 1392853618                             # using UNIX timestamp
377
     * yii migrate/to "2014-02-15 13:00:50"                  # using strtotime() parseable string
378
     * yii migrate/to app\migrations\M101129185401CreateUser # using full namespace name
379
     * ```
380
     *
381
     * @param string $version either the version name or the certain time value in the past
382
     * that the application should be migrated to. This can be either the timestamp,
383
     * the full name of the migration, the UNIX timestamp, or the parseable datetime
384
     * string.
385
     * @throws Exception if the version argument is invalid.
386
     */
387
    public function actionTo($version)
388
    {
389
        if (($namespaceVersion = $this->extractNamespaceMigrationVersion($version)) !== false) {
390
            return $this->migrateToVersion($namespaceVersion);
391
        } elseif (($migrationName = $this->extractMigrationVersion($version)) !== false) {
392
            return $this->migrateToVersion($migrationName);
393
        } elseif ((string) (int) $version == $version) {
394
            return $this->migrateToTime($version);
0 ignored issues
show
Bug introduced by
$version of type string is incompatible with the type integer expected by parameter $time of yii\console\controllers\...roller::migrateToTime(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

394
            return $this->migrateToTime(/** @scrutinizer ignore-type */ $version);
Loading history...
395
        } elseif (($time = strtotime($version)) !== false) {
396
            return $this->migrateToTime($time);
397
        } else {
398
            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).");
399
        }
400
    }
401
402
    /**
403
     * Modifies the migration history to the specified version.
404
     *
405
     * No actual migration will be performed.
406
     *
407
     * ```
408
     * yii migrate/mark 101129_185401                        # using timestamp
409
     * yii migrate/mark m101129_185401_create_user_table     # using full name
410
     * yii migrate/mark app\migrations\M101129185401CreateUser # using full namespace name
411
     * yii migrate/mark m000000_000000_base # reset the complete migration history
412
     * ```
413
     *
414
     * @param string $version the version at which the migration history should be marked.
415
     * This can be either the timestamp or the full name of the migration.
416
     * You may specify the name `m000000_000000_base` to set the migration history to a
417
     * state where no migration has been applied.
418
     * @return int CLI exit code
419
     * @throws Exception if the version argument is invalid or the version cannot be found.
420
     */
421
    public function actionMark($version)
422
    {
423
        $originalVersion = $version;
424
        if (($namespaceVersion = $this->extractNamespaceMigrationVersion($version)) !== false) {
425
            $version = $namespaceVersion;
426
        } elseif (($migrationName = $this->extractMigrationVersion($version)) !== false) {
427
            $version = $migrationName;
428
        } elseif ($version !== static::BASE_MIGRATION) {
429
            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).");
430
        }
431
432
        // try mark up
433
        $migrations = $this->getNewMigrations();
434
        foreach ($migrations as $i => $migration) {
435
            if (strpos($migration, $version) === 0) {
436
                if ($this->confirm("Set migration history at $originalVersion?")) {
437
                    for ($j = 0; $j <= $i; ++$j) {
438
                        $this->addMigrationHistory($migrations[$j]);
439
                    }
440
                    $this->stdout("The migration history is set at $originalVersion.\nNo actual migration was performed.\n", Console::FG_GREEN);
441
                }
442
443
                return ExitCode::OK;
444
            }
445
        }
446
447
        // try mark down
448
        $migrations = array_keys($this->getMigrationHistory(null));
449
        $migrations[] = static::BASE_MIGRATION;
450
        foreach ($migrations as $i => $migration) {
451
            if (strpos($migration, $version) === 0) {
452
                if ($i === 0) {
453
                    $this->stdout("Already at '$originalVersion'. Nothing needs to be done.\n", Console::FG_YELLOW);
454
                } elseif ($this->confirm("Set migration history at $originalVersion?")) {
455
                    for ($j = 0; $j < $i; ++$j) {
456
                        $this->removeMigrationHistory($migrations[$j]);
457
                    }
458
                    $this->stdout("The migration history is set at $originalVersion.\nNo actual migration was performed.\n", Console::FG_GREEN);
459
                }
460
461
                return ExitCode::OK;
462
            }
463
        }
464
465
        throw new Exception("Unable to find the version '$originalVersion'.");
466
    }
467
468
    /**
469
     * Drops all tables and related constraints. Starts the migration from the beginning.
470
     *
471
     * ```
472
     * yii migrate/fresh
473
     * ```
474
     *
475
     * @since 2.0.13
476
     */
477
    public function actionFresh()
478
    {
479
        if (YII_ENV_PROD) {
480
            $this->stdout("YII_ENV is set to 'prod'.\nRefreshing migrations is not possible on production systems.\n");
481
482
            return ExitCode::OK;
483
        }
484
485
        if ($this->confirm("Are you sure you want to drop all tables and related constraints and start the migration from the beginning?\nAll data will be lost irreversibly!")) {
486
            $this->truncateDatabase();
487
488
            return $this->actionUp();
489
        }
490
491
        $this->stdout('Action was cancelled by user. Nothing has been performed.');
492
493
        return ExitCode::OK;
494
    }
495
496
    /**
497
     * Checks if given migration version specification matches namespaced migration name.
498
     * @param string $rawVersion raw version specification received from user input.
499
     * @return string|false actual migration version, `false` - if not match.
500
     * @since 2.0.10
501
     */
502
    private function extractNamespaceMigrationVersion($rawVersion)
503
    {
504
        if (preg_match('/^\\\\?([\w_]+\\\\)+m(\d{6}_?\d{6})(\D.*)?$/is', $rawVersion, $matches)) {
505
            return trim($rawVersion, '\\');
506
        }
507
508
        return false;
509
    }
510
511
    /**
512
     * Checks if given migration version specification matches migration base name.
513
     * @param string $rawVersion raw version specification received from user input.
514
     * @return string|false actual migration version, `false` - if not match.
515
     * @since 2.0.10
516
     */
517
    private function extractMigrationVersion($rawVersion)
518
    {
519
        if (preg_match('/^m?(\d{6}_?\d{6})(\D.*)?$/is', $rawVersion, $matches)) {
520
            return 'm' . $matches[1];
521
        }
522
523
        return false;
524
    }
525
526
    /**
527
     * Displays the migration history.
528
     *
529
     * This command will show the list of migrations that have been applied
530
     * so far. For example,
531
     *
532
     * ```
533
     * yii migrate/history     # showing the last 10 migrations
534
     * yii migrate/history 5   # showing the last 5 migrations
535
     * yii migrate/history all # showing the whole history
536
     * ```
537
     *
538
     * @param int|string $limit the maximum number of migrations to be displayed.
539
     * If it is "all", the whole migration history will be displayed.
540
     * @throws \yii\console\Exception if invalid limit value passed
541
     */
542
    public function actionHistory($limit = 10)
543
    {
544
        if ($limit === 'all') {
545
            $limit = null;
546
        } else {
547
            $limit = (int) $limit;
548
            if ($limit < 1) {
549
                throw new Exception('The limit must be greater than 0.');
550
            }
551
        }
552
553
        $migrations = $this->getMigrationHistory($limit);
554
555
        if (empty($migrations)) {
556
            $this->stdout("No migration has been done before.\n", Console::FG_YELLOW);
557
        } else {
558
            $n = count($migrations);
559
            if ($limit > 0) {
560
                $this->stdout("Showing the last $n applied " . ($n === 1 ? 'migration' : 'migrations') . ":\n", Console::FG_YELLOW);
561
            } else {
562
                $this->stdout("Total $n " . ($n === 1 ? 'migration has' : 'migrations have') . " been applied before:\n", Console::FG_YELLOW);
563
            }
564
            foreach ($migrations as $version => $time) {
565
                $this->stdout("\t(" . date('Y-m-d H:i:s', $time) . ') ' . $version . "\n");
566
            }
567
        }
568
569
        return ExitCode::OK;
570
    }
571
572
    /**
573
     * Displays the un-applied new migrations.
574
     *
575
     * This command will show the new migrations that have not been applied.
576
     * For example,
577
     *
578
     * ```
579
     * yii migrate/new     # showing the first 10 new migrations
580
     * yii migrate/new 5   # showing the first 5 new migrations
581
     * yii migrate/new all # showing all new migrations
582
     * ```
583
     *
584
     * @param int|string $limit the maximum number of new migrations to be displayed.
585
     * If it is `all`, all available new migrations will be displayed.
586
     * @throws \yii\console\Exception if invalid limit value passed
587
     */
588
    public function actionNew($limit = 10)
589
    {
590
        if ($limit !== 'all') {
591
            $limit = (int) $limit;
592
            if ($limit < 1) {
593
                throw new Exception('The limit must be greater than 0.');
594
            }
595
        }
596
597
        $migrations = $this->getNewMigrations();
598
599
        if (empty($migrations)) {
600
            $this->stdout("No new migrations found. Your system is up-to-date.\n", Console::FG_GREEN);
601
        } else {
602
            $n = count($migrations);
603
            if ($limit !== 'all' && $n > $limit) {
604
                $migrations = array_slice($migrations, 0, $limit);
0 ignored issues
show
Bug introduced by
It seems like $limit can also be of type string; however, parameter $length of array_slice() does only seem to accept integer|null, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

604
                $migrations = array_slice($migrations, 0, /** @scrutinizer ignore-type */ $limit);
Loading history...
605
                $this->stdout("Showing $limit out of $n new " . ($n === 1 ? 'migration' : 'migrations') . ":\n", Console::FG_YELLOW);
606
            } else {
607
                $this->stdout("Found $n new " . ($n === 1 ? 'migration' : 'migrations') . ":\n", Console::FG_YELLOW);
608
            }
609
610
            foreach ($migrations as $migration) {
611
                $this->stdout("\t" . $migration . "\n");
612
            }
613
        }
614
615
        return ExitCode::OK;
616
    }
617
618
    /**
619
     * Creates a new migration.
620
     *
621
     * This command creates a new migration using the available migration template.
622
     * After using this command, developers should modify the created migration
623
     * skeleton by filling up the actual migration logic.
624
     *
625
     * ```
626
     * yii migrate/create create_user_table
627
     * ```
628
     *
629
     * In order to generate a namespaced migration, you should specify a namespace before the migration's name.
630
     * Note that backslash (`\`) is usually considered a special character in the shell, so you need to escape it
631
     * properly to avoid shell errors or incorrect behavior.
632
     * For example:
633
     *
634
     * ```
635
     * yii migrate/create app\\migrations\\createUserTable
636
     * ```
637
     *
638
     * In case [[migrationPath]] is not set and no namespace is provided, the first entry of [[migrationNamespaces]] will be used.
639
     *
640
     * @param string $name the name of the new migration. This should only contain
641
     * letters, digits, underscores and/or backslashes.
642
     *
643
     * Note: If the migration name is of a special form, for example create_xxx or
644
     * drop_xxx, then the generated migration file will contain extra code,
645
     * in this case for creating/dropping tables.
646
     *
647
     * @throws Exception if the name argument is invalid.
648
     */
649
    public function actionCreate($name)
650
    {
651
        if (!preg_match('/^[\w\\\\]+$/', $name)) {
652
            throw new Exception('The migration name should contain letters, digits, underscore and/or backslash characters only.');
653
        }
654
655
        list($namespace, $className) = $this->generateClassName($name);
656
        // Abort if name is too long
657
        $nameLimit = $this->getMigrationNameLimit();
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $nameLimit is correct as $this->getMigrationNameLimit() targeting yii\console\controllers\...getMigrationNameLimit() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
658
        if ($nameLimit !== null && strlen($className) > $nameLimit) {
0 ignored issues
show
introduced by
The condition $nameLimit !== null is always false.
Loading history...
659
            throw new Exception('The migration name is too long.');
660
        }
661
662
        $migrationPath = $this->findMigrationPath($namespace);
663
664
        $file = $migrationPath . DIRECTORY_SEPARATOR . $className . '.php';
665
        if ($this->confirm("Create new migration '$file'?")) {
666
            $content = $this->generateMigrationSourceCode([
667
                'name' => $name,
668
                'className' => $className,
669
                'namespace' => $namespace,
670
            ]);
671
            FileHelper::createDirectory($migrationPath);
672
            if (file_put_contents($file, $content, LOCK_EX) === false) {
673
                $this->stdout("Failed to create new migration.\n", Console::FG_RED);
674
675
                return ExitCode::IOERR;
676
            }
677
678
            FileHelper::changeOwnership($file, $this->newFileOwnership, $this->newFileMode);
679
680
            $this->stdout("New migration created successfully.\n", Console::FG_GREEN);
681
        }
682
683
        return ExitCode::OK;
684
    }
685
686
    /**
687
     * Generates class base name and namespace from migration name from user input.
688
     * @param string $name migration name from user input.
689
     * @return array list of 2 elements: 'namespace' and 'class base name'
690
     * @since 2.0.10
691
     */
692
    private function generateClassName($name)
693
    {
694
        $namespace = null;
695
        $name = trim($name, '\\');
696
        if (strpos($name, '\\') !== false) {
697
            $namespace = substr($name, 0, strrpos($name, '\\'));
698
            $name = substr($name, strrpos($name, '\\') + 1);
699
        } elseif ($this->migrationPath === null) {
700
            $migrationNamespaces = $this->migrationNamespaces;
701
            $namespace = array_shift($migrationNamespaces);
702
        }
703
704
        if ($namespace === null) {
705
            $class = 'm' . gmdate('ymd_His') . '_' . $name;
706
        } else {
707
            $class = 'M' . gmdate('ymdHis') . Inflector::camelize($name);
708
        }
709
710
        return [$namespace, $class];
711
    }
712
713
    /**
714
     * Finds the file path for the specified migration namespace.
715
     * @param string|null $namespace migration namespace.
716
     * @return string migration file path.
717
     * @throws Exception on failure.
718
     * @since 2.0.10
719
     */
720
    private function findMigrationPath($namespace)
721
    {
722
        if (empty($namespace)) {
723
            return is_array($this->migrationPath) ? reset($this->migrationPath) : $this->migrationPath;
724
        }
725
726
        if (!in_array($namespace, $this->migrationNamespaces, true)) {
727
            throw new Exception("Namespace '{$namespace}' not found in `migrationNamespaces`");
728
        }
729
730
        return $this->getNamespacePath($namespace);
731
    }
732
733
    /**
734
     * Returns the file path matching the give namespace.
735
     * @param string $namespace namespace.
736
     * @return string file path.
737
     * @since 2.0.10
738
     */
739
    private function getNamespacePath($namespace)
740
    {
741
        return str_replace('/', DIRECTORY_SEPARATOR, Yii::getAlias('@' . str_replace('\\', '/', $namespace)));
742
    }
743
744
    /**
745
     * Upgrades with the specified migration class.
746
     * @param string $class the migration class name
747
     * @return bool whether the migration is successful
748
     */
749
    protected function migrateUp($class)
750
    {
751
        if ($class === self::BASE_MIGRATION) {
752
            return true;
753
        }
754
755
        $this->stdout("*** applying $class\n", Console::FG_YELLOW);
756
        $start = microtime(true);
757
        $migration = $this->createMigration($class);
758
        if ($migration->up() !== false) {
759
            $this->addMigrationHistory($class);
760
            $time = microtime(true) - $start;
761
            $this->stdout("*** applied $class (time: " . sprintf('%.3f', $time) . "s)\n\n", Console::FG_GREEN);
762
763
            return true;
764
        }
765
766
        $time = microtime(true) - $start;
767
        $this->stdout("*** failed to apply $class (time: " . sprintf('%.3f', $time) . "s)\n\n", Console::FG_RED);
768
769
        return false;
770
    }
771
772
    /**
773
     * Downgrades with the specified migration class.
774
     * @param string $class the migration class name
775
     * @return bool whether the migration is successful
776
     */
777
    protected function migrateDown($class)
778
    {
779
        if ($class === self::BASE_MIGRATION) {
780
            return true;
781
        }
782
783
        $this->stdout("*** reverting $class\n", Console::FG_YELLOW);
784
        $start = microtime(true);
785
        $migration = $this->createMigration($class);
786
        if ($migration->down() !== false) {
787
            $this->removeMigrationHistory($class);
788
            $time = microtime(true) - $start;
789
            $this->stdout("*** reverted $class (time: " . sprintf('%.3f', $time) . "s)\n\n", Console::FG_GREEN);
790
791
            return true;
792
        }
793
794
        $time = microtime(true) - $start;
795
        $this->stdout("*** failed to revert $class (time: " . sprintf('%.3f', $time) . "s)\n\n", Console::FG_RED);
796
797
        return false;
798
    }
799
800
    /**
801
     * Creates a new migration instance.
802
     * @param string $class the migration class name
803
     * @return \yii\db\MigrationInterface the migration instance
804
     */
805
    protected function createMigration($class)
806
    {
807
        $this->includeMigrationFile($class);
808
809
        /** @var MigrationInterface $migration */
810
        $migration = Yii::createObject($class);
811
        if ($migration instanceof BaseObject && $migration->canSetProperty('compact')) {
812
            $migration->compact = $this->compact;
0 ignored issues
show
Bug Best Practice introduced by
The property compact does not exist on yii\base\BaseObject. Since you implemented __set, consider adding a @property annotation.
Loading history...
813
        }
814
815
        return $migration;
816
    }
817
818
    /**
819
     * Includes the migration file for a given migration class name.
820
     *
821
     * This function will do nothing on namespaced migrations, which are loaded by
822
     * autoloading automatically. It will include the migration file, by searching
823
     * [[migrationPath]] for classes without namespace.
824
     * @param string $class the migration class name.
825
     * @since 2.0.12
826
     */
827
    protected function includeMigrationFile($class)
828
    {
829
        $class = trim($class, '\\');
830
        if (strpos($class, '\\') === false) {
831
            if (is_array($this->migrationPath)) {
832
                foreach ($this->migrationPath as $path) {
833
                    $file = $path . DIRECTORY_SEPARATOR . $class . '.php';
834
                    if (is_file($file)) {
835
                        require_once $file;
836
                        break;
837
                    }
838
                }
839
            } else {
840
                $file = $this->migrationPath . DIRECTORY_SEPARATOR . $class . '.php';
841
                require_once $file;
842
            }
843
        }
844
    }
845
846
    /**
847
     * Migrates to the specified apply time in the past.
848
     * @param int $time UNIX timestamp value.
849
     */
850
    protected function migrateToTime($time)
851
    {
852
        $count = 0;
853
        $migrations = array_values($this->getMigrationHistory(null));
854
        while ($count < count($migrations) && $migrations[$count] > $time) {
855
            ++$count;
856
        }
857
        if ($count === 0) {
858
            $this->stdout("Nothing needs to be done.\n", Console::FG_GREEN);
859
        } else {
860
            return $this->actionDown($count);
861
        }
862
863
        return ExitCode::OK;
864
    }
865
866
    /**
867
     * Migrates to the certain version.
868
     * @param string $version name in the full format.
869
     * @return int CLI exit code
870
     * @throws Exception if the provided version cannot be found.
871
     */
872
    protected function migrateToVersion($version)
873
    {
874
        $originalVersion = $version;
875
876
        // try migrate up
877
        $migrations = $this->getNewMigrations();
878
        foreach ($migrations as $i => $migration) {
879
            if (strpos($migration, $version) === 0) {
880
                return $this->actionUp($i + 1);
881
            }
882
        }
883
884
        // try migrate down
885
        $migrations = array_keys($this->getMigrationHistory(null));
886
        foreach ($migrations as $i => $migration) {
887
            if (strpos($migration, $version) === 0) {
888
                if ($i === 0) {
889
                    $this->stdout("Already at '$originalVersion'. Nothing needs to be done.\n", Console::FG_YELLOW);
890
                } else {
891
                    return $this->actionDown($i);
892
                }
893
894
                return ExitCode::OK;
895
            }
896
        }
897
898
        throw new Exception("Unable to find the version '$originalVersion'.");
899
    }
900
901
    /**
902
     * Returns the migrations that are not applied.
903
     * @return array list of new migrations
904
     */
905
    protected function getNewMigrations()
906
    {
907
        $applied = [];
908
        foreach ($this->getMigrationHistory(null) as $class => $time) {
909
            $applied[trim($class, '\\')] = true;
910
        }
911
912
        $migrationPaths = [];
913
        if (is_array($this->migrationPath)) {
914
            foreach ($this->migrationPath as $path) {
915
                $migrationPaths[] = [$path, ''];
916
            }
917
        } elseif (!empty($this->migrationPath)) {
918
            $migrationPaths[] = [$this->migrationPath, ''];
919
        }
920
        foreach ($this->migrationNamespaces as $namespace) {
921
            $migrationPaths[] = [$this->getNamespacePath($namespace), $namespace];
922
        }
923
924
        $migrations = [];
925
        foreach ($migrationPaths as $item) {
926
            list($migrationPath, $namespace) = $item;
927
            if (!file_exists($migrationPath)) {
928
                continue;
929
            }
930
            $handle = opendir($migrationPath);
931
            while (($file = readdir($handle)) !== false) {
932
                if ($file === '.' || $file === '..') {
933
                    continue;
934
                }
935
                $path = $migrationPath . DIRECTORY_SEPARATOR . $file;
936
                if (preg_match('/^(m(\d{6}_?\d{6})\D.*?)\.php$/is', $file, $matches) && is_file($path)) {
937
                    $class = $matches[1];
938
                    if (!empty($namespace)) {
939
                        $class = $namespace . '\\' . $class;
940
                    }
941
                    $time = str_replace('_', '', $matches[2]);
942
                    if (!isset($applied[$class])) {
943
                        $migrations[$time . '\\' . $class] = $class;
944
                    }
945
                }
946
            }
947
            closedir($handle);
948
        }
949
        ksort($migrations);
950
951
        return array_values($migrations);
952
    }
953
954
    /**
955
     * Generates new migration source PHP code.
956
     * Child class may override this method, adding extra logic or variation to the process.
957
     * @param array $params generation parameters, usually following parameters are present:
958
     *
959
     *  - name: string migration base name
960
     *  - className: string migration class name
961
     *
962
     * @return string generated PHP code.
963
     * @since 2.0.8
964
     */
965
    protected function generateMigrationSourceCode($params)
966
    {
967
        return $this->renderFile(Yii::getAlias($this->templateFile), $params);
0 ignored issues
show
Bug introduced by
It seems like Yii::getAlias($this->templateFile) can also be of type false; however, parameter $file of yii\base\Controller::renderFile() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

967
        return $this->renderFile(/** @scrutinizer ignore-type */ Yii::getAlias($this->templateFile), $params);
Loading history...
968
    }
969
970
    /**
971
     * Truncates the database.
972
     * This method should be overwritten in subclasses to implement the task of clearing the database.
973
     * @throws NotSupportedException if not overridden
974
     * @since 2.0.13
975
     */
976
    protected function truncateDatabase()
977
    {
978
        throw new NotSupportedException('This command is not implemented in ' . get_class($this));
979
    }
980
981
    /**
982
     * Return the maximum name length for a migration.
983
     *
984
     * Subclasses may override this method to define a limit.
985
     * @return int|null the maximum name length for a migration or `null` if no limit applies.
986
     * @since 2.0.13
987
     */
988
    protected function getMigrationNameLimit()
989
    {
990
        return null;
991
    }
992
993
    /**
994
     * Returns the migration history.
995
     * @param int|null $limit the maximum number of records in the history to be returned. `null` for "no limit".
996
     * @return array the migration history
997
     */
998
    abstract protected function getMigrationHistory($limit);
999
1000
    /**
1001
     * Adds new migration entry to the history.
1002
     * @param string $version migration version name.
1003
     */
1004
    abstract protected function addMigrationHistory($version);
1005
1006
    /**
1007
     * Removes existing migration from the history.
1008
     * @param string $version migration version name.
1009
     */
1010
    abstract protected function removeMigrationHistory($version);
1011
}
1012