Completed
Push — master ( 7e7fae...614fb5 )
by Alexander
17:22 queued 14:22
created

BaseMigrateController::actionCreate()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 26
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 4.0032

Importance

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