Passed
Push — master ( 68691e...31ca0f )
by Alexander
03:27
created

BaseMigrateController::actionCreate()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 35
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 6.042

Importance

Changes 0
Metric Value
cc 6
eloc 20
nc 5
nop 1
dl 0
loc 35
ccs 17
cts 19
cp 0.8947
crap 6.042
rs 8.9777
c 0
b 0
f 0
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
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 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 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 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 149
     */
100
    public $newFileOwnership;
101 149
    /**
102 149
     * @var bool indicates whether the console output should be compacted.
103 149
     * If this is set to true, the individual commands ran within the migration will not be output to the console.
104 149
     * 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 160
        return array_merge(
116
            parent::options($actionID),
117 160
            ['migrationPath', 'migrationNamespaces', 'compact'], // global for all actions
118 160
            $actionID === 'create' ? ['templateFile'] : [] // action create
119
        );
120
    }
121
122 160
    /**
123
     * This method is invoked right before an action is to be executed (after all possible filters.)
124 160
     * It checks the existence of the [[migrationPath]].
125 88
     * @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 160
     */
129 7
    public function beforeAction($action)
130 7
    {
131
        if (parent::beforeAction($action)) {
132 153
            if (empty($this->migrationNamespaces) && empty($this->migrationPath)) {
133 147
                throw new InvalidConfigException('At least one of `migrationPath` or `migrationNamespaces` should be specified.');
134 147
            }
135
136
            $this->migrationNamespaces = (array) $this->migrationNamespaces;
137
138
            foreach ($this->migrationNamespaces as $key => $value) {
139
                $this->migrationNamespaces[$key] = trim($value, '\\');
140 147
            }
141
142
            if (is_array($this->migrationPath)) {
143 160
                foreach ($this->migrationPath as $i => $path) {
144 160
                    $this->migrationPath[$i] = Yii::getAlias($path);
145
                }
146 160
            } 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 boolean; 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);
153
                }
154
                $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 array|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 74
     * Upgrades the application by applying new migrations.
168
     *
169 74
     * For example,
170 74
     *
171 2
     * ```
172
     * yii migrate     # apply all new migrations
173 2
     * yii migrate 3   # apply the first 3 new migrations
174
     * ```
175
     *
176 72
     * @param int $limit the number of new migrations to be applied. If 0, it means
177 72
     * applying all available new migrations.
178 72
     *
179 4
     * @return int the status of the action execution. 0 means normal, other values mean abnormal.
180
     */
181
    public function actionUp($limit = 0)
182 72
    {
183 72
        $migrations = $this->getNewMigrations();
184 71
        if (empty($migrations)) {
185
            $this->stdout("No new migrations found. Your system is up-to-date.\n", Console::FG_GREEN);
186 2
187
            return ExitCode::OK;
188
        }
189 72
190 72
        $total = count($migrations);
191 72
        $limit = (int) $limit;
192 1
        if ($limit > 0) {
193 1
            $migrations = array_slice($migrations, 0, $limit);
194
        }
195 71
196
        $n = count($migrations);
197 71
        if ($n === $total) {
198
            $this->stdout("Total $n new " . ($n === 1 ? 'migration' : 'migrations') . " to be applied:\n", Console::FG_YELLOW);
199 71
        } else {
200 71
            $this->stdout("Total $n out of $total new " . ($total === 1 ? 'migration' : 'migrations') . " to be applied:\n", Console::FG_YELLOW);
201 71
        }
202 71
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 71
            }
209
            $this->stdout("\t$migration\n");
210
        }
211 71
        $this->stdout("\n");
212 71
213
        $applied = 0;
214
        if ($this->confirm('Apply the above ' . ($n === 1 ? 'migration' : 'migrations') . '?')) {
215 71
            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 60
     * For example,
236
     *
237 60
     * ```
238 51
     * yii migrate/down     # revert the last migration
239
     * yii migrate/down 3   # revert the last 3 migrations
240 14
     * yii migrate/down all # revert all migrations
241 14
     * ```
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 60
     *
247
     * @return int the status of the action execution. 0 means normal, other values mean abnormal.
248 60
     */
249 50
    public function actionDown($limit = 1)
250
    {
251 50
        if ($limit === 'all') {
252
            $limit = null;
253
        } else {
254 60
            $limit = (int) $limit;
255
            if ($limit < 1) {
256 60
                throw new Exception('The step argument must be greater than 0.');
257 60
            }
258 60
        }
259 60
260
        $migrations = $this->getMigrationHistory($limit);
261 60
262
        if (empty($migrations)) {
263 60
            $this->stdout("No migration has been done before.\n", Console::FG_YELLOW);
264 60
265 60
            return ExitCode::OK;
266 60
        }
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 60
        foreach ($migrations as $migration) {
273
            $this->stdout("\t$migration\n");
274 60
        }
275 60
        $this->stdout("\n");
276
277
        $reverted = 0;
278 60
        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 2
     * them again. For example,
300
     *
301 2
     * ```
302
     * yii migrate/redo     # redo the last applied migration
303
     * yii migrate/redo 3   # redo the last 3 applied migrations
304 2
     * yii migrate/redo all # redo all migrations
305 2
     * ```
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 2
     *
311
     * @return int the status of the action execution. 0 means normal, other values mean abnormal.
312 2
     */
313
    public function actionRedo($limit = 1)
314
    {
315
        if ($limit === 'all') {
316
            $limit = null;
317
        } else {
318 2
            $limit = (int) $limit;
319
            if ($limit < 1) {
320 2
                throw new Exception('The step argument must be greater than 0.');
321 2
            }
322 2
        }
323 2
324
        $migrations = $this->getMigrationHistory($limit);
325 2
326
        if (empty($migrations)) {
327 2
            $this->stdout("No migration has been done before.\n", Console::FG_YELLOW);
328 2
329 2
            return ExitCode::OK;
330
        }
331
332 2
        $migrations = array_keys($migrations);
333
334
        $n = count($migrations);
335 2
        $this->stdout("Total $n " . ($n === 1 ? 'migration' : 'migrations') . " to be redone:\n", Console::FG_YELLOW);
336 2
        foreach ($migrations as $migration) {
337
            $this->stdout("\t$migration\n");
338
        }
339 2
        $this->stdout("\n");
340
341
        if ($this->confirm('Redo the above ' . ($n === 1 ? 'migration' : 'migrations') . '?')) {
342 2
            foreach ($migrations as $migration) {
343 2
                if (!$this->migrateDown($migration)) {
344
                    $this->stdout("\nMigration failed. The rest of the migrations are canceled.\n", Console::FG_RED);
345
346 2
                    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 3
     * ```
374
     * yii migrate/to 101129_185401                          # using timestamp
375 3
     * yii migrate/to m101129_185401_create_user_table       # using full name
376 1
     * yii migrate/to 1392853618                             # using UNIX timestamp
377 2
     * yii migrate/to "2014-02-15 13:00:50"                  # using strtotime() parseable string
378 2
     * 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 4
     * ```
408
     * yii migrate/mark 101129_185401                        # using timestamp
409 4
     * yii migrate/mark m101129_185401_create_user_table     # using full name
410 4
     * yii migrate/mark app\migrations\M101129185401CreateUser # using full namespace name
411 1
     * yii migrate/mark m000000_000000_base # reset the complete migration history
412 3
     * ```
413 3
     *
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 4
     * @throws Exception if the version argument is invalid or the version cannot be found.
420 4
     */
421 3
    public function actionMark($version)
422 3
    {
423 3
        $originalVersion = $version;
424 3
        if (($namespaceVersion = $this->extractNamespaceMigrationVersion($version)) !== false) {
425
            $version = $namespaceVersion;
426 3
        } elseif (($migrationName = $this->extractMigrationVersion($version)) !== false) {
427
            $version = $migrationName;
428
        } elseif ($version !== static::BASE_MIGRATION) {
429 3
            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 1
        foreach ($migrations as $i => $migration) {
435 1
            if (strpos($migration, $version) === 0) {
436 1
                if ($this->confirm("Set migration history at $originalVersion?")) {
437 1
                    for ($j = 0; $j <= $i; ++$j) {
438 1
                        $this->addMigrationHistory($migrations[$j]);
439
                    }
440 1
                    $this->stdout("The migration history is set at $originalVersion.\nNo actual migration was performed.\n", Console::FG_GREEN);
441 1
                }
442 1
443
                return ExitCode::OK;
444 1
            }
445
        }
446
447 1
        // 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 2
        }
464
465 2
        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 2
     * ```
472 2
     * yii migrate/fresh
473
     * ```
474 2
     *
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 6
            return $this->actionUp();
489
        }
490 6
491 2
        $this->stdout('Action was cancelled by user. Nothing has been performed.');
492
493
        return ExitCode::OK;
494 4
    }
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 4
    {
504
        if (preg_match('/^\\\\?([\w_]+\\\\)+m(\d{6}_?\d{6})(\D.*)?$/is', $rawVersion, $matches)) {
505 4
            return trim($rawVersion, '\\');
506 4
        }
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 8
     *
529
     * This command will show the list of migrations that have been applied
530 8
     * so far. For example,
531
     *
532
     * ```
533 8
     * yii migrate/history     # showing the last 10 migrations
534 8
     * 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 8
     * If it is "all", the whole migration history will be displayed.
540
     * @throws \yii\console\Exception if invalid limit value passed
541 8
     */
542 8
    public function actionHistory($limit = 10)
543
    {
544 2
        if ($limit === 'all') {
545 2
            $limit = null;
546 2
        } else {
547
            $limit = (int) $limit;
548
            if ($limit < 1) {
549
                throw new Exception('The limit must be greater than 0.');
550 2
            }
551 2
        }
552
553
        $migrations = $this->getMigrationHistory($limit);
554
555 8
        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 1
     *
575
     * This command will show the new migrations that have not been applied.
576 1
     * For example,
577
     *
578
     * ```
579 1
     * yii migrate/new     # showing the first 10 new migrations
580 1
     * 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 1
     * If it is `all`, all available new migrations will be displayed.
586
     * @throws \yii\console\Exception if invalid limit value passed
587 1
     */
588 1
    public function actionNew($limit = 10)
589
    {
590 1
        if ($limit === 'all') {
591 1
            $limit = null;
592
        } else {
593
            $limit = (int) $limit;
594
            if ($limit < 1) {
595 1
                throw new Exception('The limit must be greater than 0.');
596
            }
597
        }
598 1
599 1
        $migrations = $this->getNewMigrations();
600
601
        if (empty($migrations)) {
602
            $this->stdout("No new migrations found. Your system is up-to-date.\n", Console::FG_GREEN);
603 1
        } else {
604
            $n = count($migrations);
605
            if ($limit && $n > $limit) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $limit of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. 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...
606
                $migrations = array_slice($migrations, 0, $limit);
607
                $this->stdout("Showing $limit out of $n new " . ($n === 1 ? 'migration' : 'migrations') . ":\n", Console::FG_YELLOW);
608
            } else {
609
                $this->stdout("Found $n new " . ($n === 1 ? 'migration' : 'migrations') . ":\n", Console::FG_YELLOW);
610
            }
611
612
            foreach ($migrations as $migration) {
613
                $this->stdout("\t" . $migration . "\n");
614
            }
615
        }
616
617
        return ExitCode::OK;
618
    }
619
620
    /**
621
     * Creates a new migration.
622
     *
623
     * This command creates a new migration using the available migration template.
624
     * After using this command, developers should modify the created migration
625
     * skeleton by filling up the actual migration logic.
626
     *
627
     * ```
628
     * yii migrate/create create_user_table
629
     * ```
630
     *
631
     * In order to generate a namespaced migration, you should specify a namespace before the migration's name.
632
     * Note that backslash (`\`) is usually considered a special character in the shell, so you need to escape it
633
     * properly to avoid shell errors or incorrect behavior.
634
     * For example:
635
     *
636
     * ```
637 84
     * yii migrate/create 'app\\migrations\\createUserTable'
638
     * ```
639 84
     *
640
     * In case [[migrationPath]] is not set and no namespace is provided, the first entry of [[migrationNamespaces]] will be used.
641
     *
642
     * @param string $name the name of the new migration. This should only contain
643 84
     * letters, digits, underscores and/or backslashes.
644
     *
645 84
     * Note: If the migration name is of a special form, for example create_xxx or
646 84
     * drop_xxx, then the generated migration file will contain extra code,
647 1
     * in this case for creating/dropping tables.
648
     *
649
     * @throws Exception if the name argument is invalid.
650 83
     */
651
    public function actionCreate($name)
652 83
    {
653 83
        if (!preg_match('/^[\w\\\\]+$/', $name)) {
654 83
            throw new Exception('The migration name should contain letters, digits, underscore and/or backslash characters only.');
655 83
        }
656 83
657 83
        list($namespace, $className) = $this->generateClassName($name);
658
        // Abort if name is too long
659 83
        $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...
660 83
        if ($nameLimit !== null && strlen($className) > $nameLimit) {
0 ignored issues
show
introduced by
The condition $nameLimit !== null is always false.
Loading history...
661
            throw new Exception('The migration name is too long.');
662
        }
663
664
        $migrationPath = $this->findMigrationPath($namespace);
665
666 83
        $file = $migrationPath . DIRECTORY_SEPARATOR . $className . '.php';
667
        if ($this->confirm("Create new migration '$file'?")) {
668
            $content = $this->generateMigrationSourceCode([
669 83
                'name' => $name,
670
                'className' => $className,
671
                'namespace' => $namespace,
672
            ]);
673
            FileHelper::createDirectory($migrationPath);
674
            if (file_put_contents($file, $content, LOCK_EX) === false) {
675
                $this->stdout("Failed to create new migration.\n", Console::FG_RED);
676
677
                return ExitCode::IOERR;
678 84
            }
679
680 84
            FileHelper::changeOwnership($file, $this->newFileOwnership, $this->newFileMode);
681 84
682 84
            $this->stdout("New migration created successfully.\n", Console::FG_GREEN);
683 81
        }
684 81
685 84
        return ExitCode::OK;
686 1
    }
687 1
688
    /**
689
     * Generates class base name and namespace from migration name from user input.
690 84
     * @param string $name migration name from user input.
691 84
     * @return array list of 2 elements: 'namespace' and 'class base name'
692
     * @since 2.0.10
693 81
     */
694
    private function generateClassName($name)
695
    {
696 84
        $namespace = null;
697
        $name = trim($name, '\\');
698
        if (strpos($name, '\\') !== false) {
699
            $namespace = substr($name, 0, strrpos($name, '\\'));
700
            $name = substr($name, strrpos($name, '\\') + 1);
701
        } elseif ($this->migrationPath === null) {
702
            $migrationNamespaces = $this->migrationNamespaces;
703
            $namespace = array_shift($migrationNamespaces);
704
        }
705
706 83
        if ($namespace === null) {
707
            $class = 'm' . gmdate('ymd_His') . '_' . $name;
708 83
        } else {
709 83
            $class = 'M' . gmdate('ymdHis') . Inflector::camelize($name);
710
        }
711
712 81
        return [$namespace, $class];
713
    }
714
715
    /**
716 81
     * Finds the file path for the specified migration namespace.
717
     * @param string|null $namespace migration namespace.
718
     * @return string migration file path.
719
     * @throws Exception on failure.
720
     * @since 2.0.10
721
     */
722
    private function findMigrationPath($namespace)
723
    {
724
        if (empty($namespace)) {
725 87
            return is_array($this->migrationPath) ? reset($this->migrationPath) : $this->migrationPath;
726
        }
727 87
728
        if (!in_array($namespace, $this->migrationNamespaces, true)) {
729
            throw new Exception("Namespace '{$namespace}' not found in `migrationNamespaces`");
730
        }
731
732
        return $this->getNamespacePath($namespace);
733
    }
734
735 71
    /**
736
     * Returns the file path matching the give namespace.
737 71
     * @param string $namespace namespace.
738
     * @return string file path.
739
     * @since 2.0.10
740
     */
741 71
    private function getNamespacePath($namespace)
742 71
    {
743 71
        return str_replace('/', DIRECTORY_SEPARATOR, Yii::getAlias('@' . str_replace('\\', '/', $namespace)));
744 71
    }
745 71
746 71
    /**
747 71
     * Upgrades with the specified migration class.
748
     * @param string $class the migration class name
749 71
     * @return bool whether the migration is successful
750
     */
751
    protected function migrateUp($class)
752
    {
753
        if ($class === self::BASE_MIGRATION) {
754
            return true;
755
        }
756
757
        $this->stdout("*** applying $class\n", Console::FG_YELLOW);
758
        $start = microtime(true);
759
        $migration = $this->createMigration($class);
760
        if ($migration->up() !== false) {
761
            $this->addMigrationHistory($class);
762
            $time = microtime(true) - $start;
763 61
            $this->stdout("*** applied $class (time: " . sprintf('%.3f', $time) . "s)\n\n", Console::FG_GREEN);
764
765 61
            return true;
766
        }
767
768
        $time = microtime(true) - $start;
769 61
        $this->stdout("*** failed to apply $class (time: " . sprintf('%.3f', $time) . "s)\n\n", Console::FG_RED);
770 61
771 61
        return false;
772 61
    }
773 61
774 61
    /**
775 61
     * Downgrades with the specified migration class.
776
     * @param string $class the migration class name
777 61
     * @return bool whether the migration is successful
778
     */
779
    protected function migrateDown($class)
780
    {
781
        if ($class === self::BASE_MIGRATION) {
782
            return true;
783
        }
784
785
        $this->stdout("*** reverting $class\n", Console::FG_YELLOW);
786
        $start = microtime(true);
787
        $migration = $this->createMigration($class);
788
        if ($migration->down() !== false) {
789
            $this->removeMigrationHistory($class);
790
            $time = microtime(true) - $start;
791
            $this->stdout("*** reverted $class (time: " . sprintf('%.3f', $time) . "s)\n\n", Console::FG_GREEN);
792
793
            return true;
794
        }
795
796
        $time = microtime(true) - $start;
797
        $this->stdout("*** failed to revert $class (time: " . sprintf('%.3f', $time) . "s)\n\n", Console::FG_RED);
798
799
        return false;
800
    }
801
802
    /**
803
     * Creates a new migration instance.
804
     * @param string $class the migration class name
805
     * @return \yii\db\MigrationInterface the migration instance
806
     */
807
    protected function createMigration($class)
808
    {
809
        $this->includeMigrationFile($class);
810
811
        /** @var MigrationInterface $migration */
812
        $migration = Yii::createObject($class);
813 71
        if ($migration instanceof BaseObject && $migration->canSetProperty('compact')) {
814
            $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...
815 71
        }
816 71
817 67
        return $migration;
818 7
    }
819 7
820 7
    /**
821 7
     * Includes the migration file for a given migration class name.
822 7
     *
823
     * This function will do nothing on namespaced migrations, which are loaded by
824
     * autoloading automatically. It will include the migration file, by searching
825
     * [[migrationPath]] for classes without namespace.
826 60
     * @param string $class the migration class name.
827 60
     * @since 2.0.12
828
     */
829
    protected function includeMigrationFile($class)
830 71
    {
831
        $class = trim($class, '\\');
832
        if (strpos($class, '\\') === false) {
833
            if (is_array($this->migrationPath)) {
834
                foreach ($this->migrationPath as $path) {
835
                    $file = $path . DIRECTORY_SEPARATOR . $class . '.php';
836
                    if (is_file($file)) {
837
                        require_once $file;
838
                        break;
839
                    }
840
                }
841
            } else {
842
                $file = $this->migrationPath . DIRECTORY_SEPARATOR . $class . '.php';
843
                require_once $file;
844
            }
845
        }
846
    }
847
848
    /**
849
     * Migrates to the specified apply time in the past.
850
     * @param int $time UNIX timestamp value.
851
     */
852
    protected function migrateToTime($time)
853
    {
854
        $count = 0;
855
        $migrations = array_values($this->getMigrationHistory(null));
856
        while ($count < count($migrations) && $migrations[$count] > $time) {
857
            ++$count;
858 3
        }
859
        if ($count === 0) {
860 3
            $this->stdout("Nothing needs to be done.\n", Console::FG_GREEN);
861
        } else {
862
            return $this->actionDown($count);
863 3
        }
864 3
865 2
        return ExitCode::OK;
866 2
    }
867
868
    /**
869
     * Migrates to the certain version.
870
     * @param string $version name in the full format.
871 1
     * @return int CLI exit code
872 1
     * @throws Exception if the provided version cannot be found.
873 1
     */
874 1
    protected function migrateToVersion($version)
875
    {
876
        $originalVersion = $version;
877 1
878
        // try migrate up
879
        $migrations = $this->getNewMigrations();
880 1
        foreach ($migrations as $i => $migration) {
881
            if (strpos($migration, $version) === 0) {
882
                return $this->actionUp($i + 1);
883
            }
884
        }
885
886
        // try migrate down
887
        $migrations = array_keys($this->getMigrationHistory(null));
888
        foreach ($migrations as $i => $migration) {
889
            if (strpos($migration, $version) === 0) {
890
                if ($i === 0) {
891 76
                    $this->stdout("Already at '$originalVersion'. Nothing needs to be done.\n", Console::FG_YELLOW);
892
                } else {
893 76
                    return $this->actionDown($i);
894 76
                }
895 3
896
                return ExitCode::OK;
897
            }
898 76
        }
899 76
900 7
        throw new Exception("Unable to find the version '$originalVersion'.");
901 7
    }
902
903 69
    /**
904 64
     * Returns the migrations that are not applied.
905
     * @return array list of new migrations
906 76
     */
907 6
    protected function getNewMigrations()
908
    {
909
        $applied = [];
910 76
        foreach ($this->getMigrationHistory(null) as $class => $time) {
911 76
            $applied[trim($class, '\\')] = true;
912 76
        }
913 76
914
        $migrationPaths = [];
915
        if (is_array($this->migrationPath)) {
916 76
            foreach ($this->migrationPath as $path) {
917 76
                $migrationPaths[] = [$path, ''];
918 76
            }
919 76
        } elseif (!empty($this->migrationPath)) {
920
            $migrationPaths[] = [$this->migrationPath, ''];
921 74
        }
922 74
        foreach ($this->migrationNamespaces as $namespace) {
923 74
            $migrationPaths[] = [$this->getNamespacePath($namespace), $namespace];
924 74
        }
925 6
926
        $migrations = [];
927 74
        foreach ($migrationPaths as $item) {
928 74
            list($migrationPath, $namespace) = $item;
929 74
            if (!file_exists($migrationPath)) {
930
                continue;
931
            }
932
            $handle = opendir($migrationPath);
933 76
            while (($file = readdir($handle)) !== false) {
934
                if ($file === '.' || $file === '..') {
935 76
                    continue;
936
                }
937 76
                $path = $migrationPath . DIRECTORY_SEPARATOR . $file;
938
                if (preg_match('/^(m(\d{6}_?\d{6})\D.*?)\.php$/is', $file, $matches) && is_file($path)) {
939
                    $class = $matches[1];
940
                    if (!empty($namespace)) {
941
                        $class = $namespace . '\\' . $class;
942
                    }
943
                    $time = str_replace('_', '', $matches[2]);
944
                    if (!isset($applied[$class])) {
945
                        $migrations[$time . '\\' . $class] = $class;
946
                    }
947
                }
948
            }
949
            closedir($handle);
950
        }
951
        ksort($migrations);
952
953
        return array_values($migrations);
954
    }
955
956
    /**
957
     * Generates new migration source PHP code.
958
     * Child class may override this method, adding extra logic or variation to the process.
959
     * @param array $params generation parameters, usually following parameters are present:
960
     *
961
     *  - name: string migration base name
962
     *  - className: string migration class name
963
     *
964
     * @return string generated PHP code.
965
     * @since 2.0.8
966
     */
967
    protected function generateMigrationSourceCode($params)
968
    {
969
        return $this->renderFile(Yii::getAlias($this->templateFile), $params);
970
    }
971
972
    /**
973
     * Truncates the database.
974
     * This method should be overwritten in subclasses to implement the task of clearing the database.
975
     * @throws NotSupportedException if not overridden
976
     * @since 2.0.13
977
     */
978
    protected function truncateDatabase()
979
    {
980
        throw new NotSupportedException('This command is not implemented in ' . get_class($this));
981
    }
982
983
    /**
984
     * Return the maximum name length for a migration.
985
     *
986
     * Subclasses may override this method to define a limit.
987
     * @return int|null the maximum name length for a migration or `null` if no limit applies.
988
     * @since 2.0.13
989
     */
990
    protected function getMigrationNameLimit()
991
    {
992
        return null;
993
    }
994
995
    /**
996
     * Returns the migration history.
997
     * @param int $limit the maximum number of records in the history to be returned. `null` for "no limit".
998
     * @return array the migration history
999
     */
1000
    abstract protected function getMigrationHistory($limit);
1001
1002
    /**
1003
     * Adds new migration entry to the history.
1004
     * @param string $version migration version name.
1005
     */
1006
    abstract protected function addMigrationHistory($version);
1007
1008
    /**
1009
     * Removes existing migration from the history.
1010
     * @param string $version migration version name.
1011
     */
1012
    abstract protected function removeMigrationHistory($version);
1013
}
1014