Completed
Push — 2.1 ( 83fa82...a136f4 )
by
unknown
12:35
created

BaseMigrateController::actionRedo()   C

Complexity

Conditions 13
Paths 35

Size

Total Lines 47
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 14.6632

Importance

Changes 0
Metric Value
dl 0
loc 47
ccs 22
cts 28
cp 0.7856
rs 5.0999
c 0
b 0
f 0
cc 13
eloc 28
nc 35
nop 1
crap 14.6632

How to fix   Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
137
            }
138
139 33
            $version = Yii::getVersion();
140 33
            $this->stdout("Yii Migration Tool (based on Yii v{$version})\n\n");
141
142 33
            return true;
143
        }
144
145
        return false;
146
    }
147
148
    /**
149
     * Upgrades the application by applying new migrations.
150
     *
151
     * For example,
152
     *
153
     * ```
154
     * yii migrate     # apply all new migrations
155
     * yii migrate 3   # apply the first 3 new migrations
156
     * ```
157
     *
158
     * @param int $limit the number of new migrations to be applied. If 0, it means
159
     * applying all available new migrations.
160
     *
161
     * @return int the status of the action execution. 0 means normal, other values mean abnormal.
162
     */
163 22
    public function actionUp($limit = 0)
164
    {
165 22
        $migrations = $this->getNewMigrations();
166 22
        if (empty($migrations)) {
167 1
            $this->stdout("No new migrations found. Your system is up-to-date.\n", Console::FG_GREEN);
168
169 1
            return ExitCode::OK;
170
        }
171
172 21
        $total = count($migrations);
173 21
        $limit = (int) $limit;
174 21
        if ($limit > 0) {
175 4
            $migrations = array_slice($migrations, 0, $limit);
176
        }
177
178 21
        $n = count($migrations);
179 21
        if ($n === $total) {
180 20
            $this->stdout("Total $n new " . ($n === 1 ? 'migration' : 'migrations') . " to be applied:\n", Console::FG_YELLOW);
181
        } else {
182 2
            $this->stdout("Total $n out of $total new " . ($total === 1 ? 'migration' : 'migrations') . " to be applied:\n", Console::FG_YELLOW);
183
        }
184
185 21
        foreach ($migrations as $migration) {
186 21
            $this->stdout("\t$migration\n");
187
        }
188 21
        $this->stdout("\n");
189
190 21
        $applied = 0;
191 21
        if ($this->confirm('Apply the above ' . ($n === 1 ? 'migration' : 'migrations') . '?')) {
192 21
            foreach ($migrations as $migration) {
193 21
                if (!$this->migrateUp($migration)) {
194
                    $this->stdout("\n$applied from $n " . ($applied === 1 ? 'migration was' : 'migrations were') . " applied.\n", Console::FG_RED);
195
                    $this->stdout("\nMigration failed. The rest of the migrations are canceled.\n", Console::FG_RED);
196
197
                    return ExitCode::UNSPECIFIED_ERROR;
198
                }
199 21
                $applied++;
200
            }
201
202 21
            $this->stdout("\n$n " . ($n === 1 ? 'migration was' : 'migrations were') . " applied.\n", Console::FG_GREEN);
203 21
            $this->stdout("\nMigrated up successfully.\n", Console::FG_GREEN);
204
        }
205 21
    }
206
207
    /**
208
     * Downgrades the application by reverting old migrations.
209
     *
210
     * For example,
211
     *
212
     * ```
213
     * yii migrate/down     # revert the last migration
214
     * yii migrate/down 3   # revert the last 3 migrations
215
     * yii migrate/down all # revert all migrations
216
     * ```
217
     *
218
     * @param int|string $limit the number of migrations to be reverted. Defaults to 1,
219
     * meaning the last applied migration will be reverted. When value is "all", all migrations will be reverted.
220
     * @throws Exception if the number of the steps specified is less than 1.
221
     *
222
     * @return int the status of the action execution. 0 means normal, other values mean abnormal.
223
     */
224 11
    public function actionDown($limit = 1)
225
    {
226 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...
227 1
            $limit = null;
228
        } else {
229 10
            $limit = (int) $limit;
230 10
            if ($limit < 1) {
231
                throw new Exception('The step argument must be greater than 0.');
232
            }
233
        }
234
235 11
        $migrations = $this->getMigrationHistory($limit);
236
237 11
        if (empty($migrations)) {
238
            $this->stdout("No migration has been done before.\n", Console::FG_YELLOW);
239
240
            return ExitCode::OK;
241
        }
242
243 11
        $migrations = array_keys($migrations);
244
245 11
        $n = count($migrations);
246 11
        $this->stdout("Total $n " . ($n === 1 ? 'migration' : 'migrations') . " to be reverted:\n", Console::FG_YELLOW);
247 11
        foreach ($migrations as $migration) {
248 11
            $this->stdout("\t$migration\n");
249
        }
250 11
        $this->stdout("\n");
251
252 11
        $reverted = 0;
253 11
        if ($this->confirm('Revert the above ' . ($n === 1 ? 'migration' : 'migrations') . '?')) {
254 11
            foreach ($migrations as $migration) {
255 11
                if (!$this->migrateDown($migration)) {
256
                    $this->stdout("\n$reverted from $n " . ($reverted === 1 ? 'migration was' : 'migrations were') . " reverted.\n", Console::FG_RED);
257
                    $this->stdout("\nMigration failed. The rest of the migrations are canceled.\n", Console::FG_RED);
258
259
                    return ExitCode::UNSPECIFIED_ERROR;
260
                }
261 11
                $reverted++;
262
            }
263 11
            $this->stdout("\n$n " . ($n === 1 ? 'migration was' : 'migrations were') . " reverted.\n", Console::FG_GREEN);
264 11
            $this->stdout("\nMigrated down successfully.\n", Console::FG_GREEN);
265
        }
266 11
    }
267
268
    /**
269
     * Redoes the last few migrations.
270
     *
271
     * This command will first revert the specified migrations, and then apply
272
     * them again. For example,
273
     *
274
     * ```
275
     * yii migrate/redo     # redo the last applied migration
276
     * yii migrate/redo 3   # redo the last 3 applied migrations
277
     * yii migrate/redo all # redo all migrations
278
     * ```
279
     *
280
     * @param int|string $limit the number of migrations to be redone. Defaults to 1,
281
     * meaning the last applied migration will be redone. When equals "all", all migrations will be redone.
282
     * @throws Exception if the number of the steps specified is less than 1.
283
     *
284
     * @return int the status of the action execution. 0 means normal, other values mean abnormal.
285
     */
286 2
    public function actionRedo($limit = 1)
287
    {
288 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...
289
            $limit = null;
290
        } else {
291 2
            $limit = (int) $limit;
292 2
            if ($limit < 1) {
293
                throw new Exception('The step argument must be greater than 0.');
294
            }
295
        }
296
297 2
        $migrations = $this->getMigrationHistory($limit);
298
299 2
        if (empty($migrations)) {
300
            $this->stdout("No migration has been done before.\n", Console::FG_YELLOW);
301
302
            return ExitCode::OK;
303
        }
304
305 2
        $migrations = array_keys($migrations);
306
307 2
        $n = count($migrations);
308 2
        $this->stdout("Total $n " . ($n === 1 ? 'migration' : 'migrations') . " to be redone:\n", Console::FG_YELLOW);
309 2
        foreach ($migrations as $migration) {
310 2
            $this->stdout("\t$migration\n");
311
        }
312 2
        $this->stdout("\n");
313
314 2
        if ($this->confirm('Redo the above ' . ($n === 1 ? 'migration' : 'migrations') . '?')) {
315 2
            foreach ($migrations as $migration) {
316 2
                if (!$this->migrateDown($migration)) {
317
                    $this->stdout("\nMigration failed. The rest of the migrations are canceled.\n", Console::FG_RED);
318
319 2
                    return ExitCode::UNSPECIFIED_ERROR;
320
                }
321
            }
322 2
            foreach (array_reverse($migrations) as $migration) {
323 2
                if (!$this->migrateUp($migration)) {
324
                    $this->stdout("\nMigration failed. The rest of the migrations are canceled.\n", Console::FG_RED);
325
326 2
                    return ExitCode::UNSPECIFIED_ERROR;
327
                }
328
            }
329 2
            $this->stdout("\n$n " . ($n === 1 ? 'migration was' : 'migrations were') . " redone.\n", Console::FG_GREEN);
330 2
            $this->stdout("\nMigration redone successfully.\n", Console::FG_GREEN);
331
        }
332 2
    }
333
334
    /**
335
     * Upgrades or downgrades till the specified version.
336
     *
337
     * Can also downgrade versions to the certain apply time in the past by providing
338
     * a UNIX timestamp or a string parseable by the strtotime() function. This means
339
     * that all the versions applied after the specified certain time would be reverted.
340
     *
341
     * This command will first revert the specified migrations, and then apply
342
     * them again. For example,
343
     *
344
     * ```
345
     * yii migrate/to 101129_185401                          # using timestamp
346
     * yii migrate/to m101129_185401_create_user_table       # using full name
347
     * yii migrate/to 1392853618                             # using UNIX timestamp
348
     * yii migrate/to "2014-02-15 13:00:50"                  # using strtotime() parseable string
349
     * yii migrate/to app\migrations\M101129185401CreateUser # using full namespace name
350
     * ```
351
     *
352
     * @param string $version either the version name or the certain time value in the past
353
     * that the application should be migrated to. This can be either the timestamp,
354
     * the full name of the migration, the UNIX timestamp, or the parseable datetime
355
     * string.
356
     * @throws Exception if the version argument is invalid.
357
     */
358 3
    public function actionTo($version)
359
    {
360 3
        if (($namespaceVersion = $this->extractNamespaceMigrationVersion($version)) !== false) {
361 1
            $this->migrateToVersion($namespaceVersion);
362 2
        } elseif (($migrationName = $this->extractMigrationVersion($version)) !== false) {
363 2
            $this->migrateToVersion($migrationName);
364
        } elseif ((string) (int) $version == $version) {
365
            $this->migrateToTime($version);
366
        } elseif (($time = strtotime($version)) !== false) {
367
            $this->migrateToTime($time);
368
        } else {
369
            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).");
370
        }
371 3
    }
372
373
    /**
374
     * Modifies the migration history to the specified version.
375
     *
376
     * No actual migration will be performed.
377
     *
378
     * ```
379
     * yii migrate/mark 101129_185401                        # using timestamp
380
     * yii migrate/mark m101129_185401_create_user_table     # using full name
381
     * yii migrate/mark app\migrations\M101129185401CreateUser # using full namespace name
382
     * yii migrate/mark m000000_000000_base # reset the complete migration history
383
     * ```
384
     *
385
     * @param string $version the version at which the migration history should be marked.
386
     * This can be either the timestamp or the full name of the migration.
387
     * You may specify the name `m000000_000000_base` to set the migration history to a
388
     * state where no migration has been applied.
389
     * @return int CLI exit code
390
     * @throws Exception if the version argument is invalid or the version cannot be found.
391
     */
392 4
    public function actionMark($version)
393
    {
394 4
        $originalVersion = $version;
395 4
        if (($namespaceVersion = $this->extractNamespaceMigrationVersion($version)) !== false) {
396 1
            $version = $namespaceVersion;
397 3
        } elseif (($migrationName = $this->extractMigrationVersion($version)) !== false) {
398 3
            $version = $migrationName;
399
        } elseif ($version !== static::BASE_MIGRATION) {
400
            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).");
401
        }
402
403
        // try mark up
404 4
        $migrations = $this->getNewMigrations();
405 4
        foreach ($migrations as $i => $migration) {
406 3
            if (strpos($migration, $version) === 0) {
407 3
                if ($this->confirm("Set migration history at $originalVersion?")) {
408 3
                    for ($j = 0; $j <= $i; ++$j) {
409 3
                        $this->addMigrationHistory($migrations[$j]);
410
                    }
411 3
                    $this->stdout("The migration history is set at $originalVersion.\nNo actual migration was performed.\n", Console::FG_GREEN);
412
                }
413
414 3
                return ExitCode::OK;
415
            }
416
        }
417
418
        // try mark down
419 1
        $migrations = array_keys($this->getMigrationHistory(null));
420 1
        $migrations[] = static::BASE_MIGRATION;
421 1
        foreach ($migrations as $i => $migration) {
422 1
            if (strpos($migration, $version) === 0) {
423 1
                if ($i === 0) {
424
                    $this->stdout("Already at '$originalVersion'. Nothing needs to be done.\n", Console::FG_YELLOW);
425
                } else {
426 1
                    if ($this->confirm("Set migration history at $originalVersion?")) {
427 1
                        for ($j = 0; $j < $i; ++$j) {
428 1
                            $this->removeMigrationHistory($migrations[$j]);
429
                        }
430 1
                        $this->stdout("The migration history is set at $originalVersion.\nNo actual migration was performed.\n", Console::FG_GREEN);
431
                    }
432
                }
433
434 1
                return ExitCode::OK;
435
            }
436
        }
437
438
        throw new Exception("Unable to find the version '$originalVersion'.");
439
    }
440
441
    /**
442
     * Truncates the whole database and starts the migration from the beginning.
443
     *
444
     * ```
445
     * yii migrate/fresh
446
     * ```
447
     *
448
     * @since 2.0.13
449
     */
450 1
    public function actionFresh()
451
    {
452 1
        if (YII_ENV_PROD) {
453
            $this->stdout("YII_ENV is set to 'prod'.\nRefreshing migrations is not possible on production systems.\n");
454
            return ExitCode::OK;
455
        }
456
457 1
        if ($this->confirm(
458 1
            "Are you sure you want to reset the database and start the migration from the beginning?\nAll data will be lost irreversibly!")) {
459 1
            $this->truncateDatabase();
460 1
            $this->actionUp();
461
        } else {
462
            $this->stdout('Action was cancelled by user. Nothing has been performed.');
463
        }
464 1
    }
465
466
    /**
467
     * Checks if given migration version specification matches namespaced migration name.
468
     * @param string $rawVersion raw version specification received from user input.
469
     * @return string|false actual migration version, `false` - if not match.
470
     * @since 2.0.10
471
     */
472 6
    private function extractNamespaceMigrationVersion($rawVersion)
473
    {
474 6
        if (preg_match('/^\\\\?([\w_]+\\\\)+m(\d{6}_?\d{6})(\D.*)?$/is', $rawVersion, $matches)) {
475 2
            return trim($rawVersion, '\\');
476
        }
477
478 4
        return false;
479
    }
480
481
    /**
482
     * Checks if given migration version specification matches migration base name.
483
     * @param string $rawVersion raw version specification received from user input.
484
     * @return string|false actual migration version, `false` - if not match.
485
     * @since 2.0.10
486
     */
487 4
    private function extractMigrationVersion($rawVersion)
488
    {
489 4
        if (preg_match('/^m?(\d{6}_?\d{6})(\D.*)?$/is', $rawVersion, $matches)) {
490 4
            return 'm' . $matches[1];
491
        }
492
493
        return false;
494
    }
495
496
    /**
497
     * Displays the migration history.
498
     *
499
     * This command will show the list of migrations that have been applied
500
     * so far. For example,
501
     *
502
     * ```
503
     * yii migrate/history     # showing the last 10 migrations
504
     * yii migrate/history 5   # showing the last 5 migrations
505
     * yii migrate/history all # showing the whole history
506
     * ```
507
     *
508
     * @param int|string $limit the maximum number of migrations to be displayed.
509
     * If it is "all", the whole migration history will be displayed.
510
     * @throws \yii\console\Exception if invalid limit value passed
511
     */
512 4
    public function actionHistory($limit = 10)
513
    {
514 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...
515
            $limit = null;
516
        } else {
517 4
            $limit = (int) $limit;
518 4
            if ($limit < 1) {
519
                throw new Exception('The limit must be greater than 0.');
520
            }
521
        }
522
523 4
        $migrations = $this->getMigrationHistory($limit);
524
525 4
        if (empty($migrations)) {
526 4
            $this->stdout("No migration has been done before.\n", Console::FG_YELLOW);
527
        } else {
528 2
            $n = count($migrations);
529 2
            if ($limit > 0) {
530 2
                $this->stdout("Showing the last $n applied " . ($n === 1 ? 'migration' : 'migrations') . ":\n", Console::FG_YELLOW);
531
            } else {
532
                $this->stdout("Total $n " . ($n === 1 ? 'migration has' : 'migrations have') . " been applied before:\n", Console::FG_YELLOW);
533
            }
534 2
            foreach ($migrations as $version => $time) {
535 2
                $this->stdout("\t(" . date('Y-m-d H:i:s', $time) . ') ' . $version . "\n");
536
            }
537
        }
538 4
    }
539
540
    /**
541
     * Displays the un-applied new migrations.
542
     *
543
     * This command will show the new migrations that have not been applied.
544
     * For example,
545
     *
546
     * ```
547
     * yii migrate/new     # showing the first 10 new migrations
548
     * yii migrate/new 5   # showing the first 5 new migrations
549
     * yii migrate/new all # showing all new migrations
550
     * ```
551
     *
552
     * @param int|string $limit the maximum number of new migrations to be displayed.
553
     * If it is `all`, all available new migrations will be displayed.
554
     * @throws \yii\console\Exception if invalid limit value passed
555
     */
556 1
    public function actionNew($limit = 10)
557
    {
558 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...
559
            $limit = null;
560
        } else {
561 1
            $limit = (int) $limit;
562 1
            if ($limit < 1) {
563
                throw new Exception('The limit must be greater than 0.');
564
            }
565
        }
566
567 1
        $migrations = $this->getNewMigrations();
568
569 1
        if (empty($migrations)) {
570 1
            $this->stdout("No new migrations found. Your system is up-to-date.\n", Console::FG_GREEN);
571
        } else {
572 1
            $n = count($migrations);
573 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...
574
                $migrations = array_slice($migrations, 0, $limit);
575
                $this->stdout("Showing $limit out of $n new " . ($n === 1 ? 'migration' : 'migrations') . ":\n", Console::FG_YELLOW);
576
            } else {
577 1
                $this->stdout("Found $n new " . ($n === 1 ? 'migration' : 'migrations') . ":\n", Console::FG_YELLOW);
578
            }
579
580 1
            foreach ($migrations as $migration) {
581 1
                $this->stdout("\t" . $migration . "\n");
582
            }
583
        }
584 1
    }
585
586
    /**
587
     * Creates a new migration.
588
     *
589
     * This command creates a new migration using the available migration template.
590
     * After using this command, developers should modify the created migration
591
     * skeleton by filling up the actual migration logic.
592
     *
593
     * ```
594
     * yii migrate/create create_user_table
595
     * ```
596
     *
597
     * In order to generate a namespaced migration, you should specify a namespace before the migration's name.
598
     * Note that backslash (`\`) is usually considered a special character in the shell, so you need to escape it
599
     * properly to avoid shell errors or incorrect behavior.
600
     * For example:
601
     *
602
     * ```
603
     * yii migrate/create 'app\\migrations\\createUserTable'
604
     * ```
605
     *
606
     * In case [[migrationPath]] is not set and no namespace is provided, the first entry of [[migrationNamespaces]] will be used.
607
     *
608
     * @param string $name the name of the new migration. This should only contain
609
     * letters, digits, underscores and/or backslashes.
610
     *
611
     * Note: If the migration name is of a special form, for example create_xxx or
612
     * drop_xxx, then the generated migration file will contain extra code,
613
     * in this case for creating/dropping tables.
614
     *
615
     * @throws Exception if the name argument is invalid.
616
     */
617 9
    public function actionCreate($name)
618
    {
619 9
        if (!preg_match('/^[\w\\\\]+$/', $name)) {
620
            throw new Exception('The migration name should contain letters, digits, underscore and/or backslash characters only.');
621
        }
622
623 9
        [$namespace, $className] = $this->generateClassName($name);
0 ignored issues
show
Bug introduced by
The variable $namespace does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $className does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
624 9
        $migrationPath = $this->findMigrationPath($namespace);
625
626 9
        $file = $migrationPath . DIRECTORY_SEPARATOR . $className . '.php';
627 9
        if ($this->confirm("Create new migration '$file'?")) {
628 9
            $content = $this->generateMigrationSourceCode([
629 9
                'name' => $name,
630 9
                'className' => $className,
631 9
                'namespace' => $namespace,
632
            ]);
633 9
            FileHelper::createDirectory($migrationPath);
634 9
            file_put_contents($file, $content);
635 9
            $this->stdout("New migration created successfully.\n", Console::FG_GREEN);
636
        }
637 9
    }
638
639
    /**
640
     * Generates class base name and namespace from migration name from user input.
641
     * @param string $name migration name from user input.
642
     * @return array list of 2 elements: 'namespace' and 'class base name'
643
     * @since 2.0.10
644
     */
645 9
    private function generateClassName($name)
646
    {
647 9
        $namespace = null;
648 9
        $name = trim($name, '\\');
649 9
        if (strpos($name, '\\') !== false) {
650 1
            $namespace = substr($name, 0, strrpos($name, '\\'));
651 1
            $name = substr($name, strrpos($name, '\\') + 1);
652
        } else {
653 9
            if ($this->migrationPath === null) {
654 1
                $migrationNamespaces = $this->migrationNamespaces;
655 1
                $namespace = array_shift($migrationNamespaces);
656
            }
657
        }
658
659 9
        if ($namespace === null) {
660 9
            $class = 'm' . gmdate('ymd_His') . '_' . $name;
661
        } else {
662 1
            $class = 'M' . gmdate('ymdHis') . ucfirst($name);
663
        }
664
665 9
        return [$namespace, $class];
666
    }
667
668
    /**
669
     * Finds the file path for the specified migration namespace.
670
     * @param string|null $namespace migration namespace.
671
     * @return string migration file path.
672
     * @throws Exception on failure.
673
     * @since 2.0.10
674
     */
675 9
    private function findMigrationPath($namespace)
676
    {
677 9
        if (empty($namespace)) {
678 9
            return is_array($this->migrationPath) ? reset($this->migrationPath) : $this->migrationPath;
679
        }
680
681 1
        if (!in_array($namespace, $this->migrationNamespaces, true)) {
682
            throw new Exception("Namespace '{$namespace}' not found in `migrationNamespaces`");
683
        }
684
685 1
        return $this->getNamespacePath($namespace);
686
    }
687
688
    /**
689
     * Returns the file path matching the give namespace.
690
     * @param string $namespace namespace.
691
     * @return string file path.
692
     * @since 2.0.10
693
     */
694 7
    private function getNamespacePath($namespace)
695
    {
696 7
        return str_replace('/', DIRECTORY_SEPARATOR, Yii::getAlias('@' . str_replace('\\', '/', $namespace)));
697
    }
698
699
    /**
700
     * Upgrades with the specified migration class.
701
     * @param string $class the migration class name
702
     * @return bool whether the migration is successful
703
     */
704 21
    protected function migrateUp($class)
705
    {
706 21
        if ($class === self::BASE_MIGRATION) {
707
            return true;
708
        }
709
710 21
        $this->stdout("*** applying $class\n", Console::FG_YELLOW);
711 21
        $start = microtime(true);
712 21
        $migration = $this->createMigration($class);
713 21
        if ($migration->up() !== false) {
714 21
            $this->addMigrationHistory($class);
715 21
            $time = microtime(true) - $start;
716 21
            $this->stdout("*** applied $class (time: " . sprintf('%.3f', $time) . "s)\n\n", Console::FG_GREEN);
717
718 21
            return true;
719
        }
720
721
        $time = microtime(true) - $start;
722
        $this->stdout("*** failed to apply $class (time: " . sprintf('%.3f', $time) . "s)\n\n", Console::FG_RED);
723
724
        return false;
725
    }
726
727
    /**
728
     * Downgrades with the specified migration class.
729
     * @param string $class the migration class name
730
     * @return bool whether the migration is successful
731
     */
732 12
    protected function migrateDown($class)
733
    {
734 12
        if ($class === self::BASE_MIGRATION) {
735
            return true;
736
        }
737
738 12
        $this->stdout("*** reverting $class\n", Console::FG_YELLOW);
739 12
        $start = microtime(true);
740 12
        $migration = $this->createMigration($class);
741 12
        if ($migration->down() !== false) {
742 12
            $this->removeMigrationHistory($class);
743 12
            $time = microtime(true) - $start;
744 12
            $this->stdout("*** reverted $class (time: " . sprintf('%.3f', $time) . "s)\n\n", Console::FG_GREEN);
745
746 12
            return true;
747
        }
748
749
        $time = microtime(true) - $start;
750
        $this->stdout("*** failed to revert $class (time: " . sprintf('%.3f', $time) . "s)\n\n", Console::FG_RED);
751
752
        return false;
753
    }
754
755
    /**
756
     * Creates a new migration instance.
757
     * @param string $class the migration class name
758
     * @return \yii\db\MigrationInterface the migration instance
759
     */
760
    protected function createMigration($class)
761
    {
762
        $this->includeMigrationFile($class);
763
        $migration = new $class();
764
        if ($migration instanceof BaseObject && $migration->canSetProperty('compact')) {
765
            $migration->compact = $this->compact;
0 ignored issues
show
Documentation introduced by
The property compact does not exist on object<yii\base\BaseObject>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
766
        }
767
        return $migration;
768
    }
769
770
    /**
771
     * Includes the migration file for a given migration class name.
772
     *
773
     * This function will do nothing on namespaced migrations, which are loaded by
774
     * autoloading automatically. It will include the migration file, by searching
775
     * [[migrationPath]] for classes without namespace.
776
     * @param string $class the migration class name.
777
     * @since 2.0.12
778
     */
779 21
    protected function includeMigrationFile($class)
780
    {
781 21
        $class = trim($class, '\\');
782 21
        if (strpos($class, '\\') === false) {
783 17
            if (is_array($this->migrationPath)) {
784 7
                foreach ($this->migrationPath as $path) {
785 7
                    $file = $path . DIRECTORY_SEPARATOR . $class . '.php';
786 7
                    if (is_file($file)) {
787 7
                        require_once $file;
788 7
                        break;
789
                    }
790
                }
791
            } else {
792 10
                $file = $this->migrationPath . DIRECTORY_SEPARATOR . $class . '.php';
793 10
                require_once $file;
794
            }
795
        }
796 21
    }
797
798
    /**
799
     * Migrates to the specified apply time in the past.
800
     * @param int $time UNIX timestamp value.
801
     */
802
    protected function migrateToTime($time)
803
    {
804
        $count = 0;
805
        $migrations = array_values($this->getMigrationHistory(null));
806
        while ($count < count($migrations) && $migrations[$count] > $time) {
807
            ++$count;
808
        }
809
        if ($count === 0) {
810
            $this->stdout("Nothing needs to be done.\n", Console::FG_GREEN);
811
        } else {
812
            $this->actionDown($count);
813
        }
814
    }
815
816
    /**
817
     * Migrates to the certain version.
818
     * @param string $version name in the full format.
819
     * @return int CLI exit code
820
     * @throws Exception if the provided version cannot be found.
821
     */
822 3
    protected function migrateToVersion($version)
823
    {
824 3
        $originalVersion = $version;
825
826
        // try migrate up
827 3
        $migrations = $this->getNewMigrations();
828 3
        foreach ($migrations as $i => $migration) {
829 2
            if (strpos($migration, $version) === 0) {
830 2
                $this->actionUp($i + 1);
831
832 2
                return ExitCode::OK;
833
            }
834
        }
835
836
        // try migrate down
837 1
        $migrations = array_keys($this->getMigrationHistory(null));
838 1
        foreach ($migrations as $i => $migration) {
839 1
            if (strpos($migration, $version) === 0) {
840 1
                if ($i === 0) {
841
                    $this->stdout("Already at '$originalVersion'. Nothing needs to be done.\n", Console::FG_YELLOW);
842
                } else {
843 1
                    $this->actionDown($i);
844
                }
845
846 1
                return ExitCode::OK;
847
            }
848
        }
849
850
        throw new Exception("Unable to find the version '$originalVersion'.");
851
    }
852
853
    /**
854
     * Returns the migrations that are not applied.
855
     * @return array list of new migrations
856
     */
857 24
    protected function getNewMigrations()
858
    {
859 24
        $applied = [];
860 24
        foreach ($this->getMigrationHistory(null) as $class => $time) {
861 3
            $applied[trim($class, '\\')] = true;
862
        }
863
864 24
        $migrationPaths = [];
865 24
        if (is_array($this->migrationPath)) {
866 7
            foreach ($this->migrationPath as $path) {
867 7
                $migrationPaths[] = [$path, ''];
868
            }
869 17
        } elseif (!empty($this->migrationPath)) {
870 12
            $migrationPaths[] = [$this->migrationPath, ''];
871
        }
872 24
        foreach ($this->migrationNamespaces as $namespace) {
873 6
            $migrationPaths[] = [$this->getNamespacePath($namespace), $namespace];
874
        }
875
876 24
        $migrations = [];
877 24
        foreach ($migrationPaths as $item) {
878 24
            list($migrationPath, $namespace) = $item;
879 24
            if (!file_exists($migrationPath)) {
880
                continue;
881
            }
882 24
            $handle = opendir($migrationPath);
883 24
            while (($file = readdir($handle)) !== false) {
884 24
                if ($file === '.' || $file === '..') {
885 24
                    continue;
886
                }
887 23
                $path = $migrationPath . DIRECTORY_SEPARATOR . $file;
888 23
                if (preg_match('/^(m(\d{6}_?\d{6})\D.*?)\.php$/is', $file, $matches) && is_file($path)) {
889 23
                    $class = $matches[1];
890 23
                    if (!empty($namespace)) {
891 6
                        $class = $namespace . '\\' . $class;
892
                    }
893 23
                    $time = str_replace('_', '', $matches[2]);
894 23
                    if (!isset($applied[$class])) {
895 23
                        $migrations[$time . '\\' . $class] = $class;
896
                    }
897
                }
898
            }
899 24
            closedir($handle);
900
        }
901 24
        ksort($migrations);
902
903 24
        return array_values($migrations);
904
    }
905
906
    /**
907
     * Generates new migration source PHP code.
908
     * Child class may override this method, adding extra logic or variation to the process.
909
     * @param array $params generation parameters, usually following parameters are present:
910
     *
911
     *  - name: string migration base name
912
     *  - className: string migration class name
913
     *
914
     * @return string generated PHP code.
915
     * @since 2.0.8
916
     */
917
    protected function generateMigrationSourceCode($params)
918
    {
919
        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...
920
    }
921
922
    /**
923
     * Truncates the database.
924
     * This method should be overwritten in subclasses to implement the task of clearing the database.
925
     * @throws NotSupportedException if not overridden
926
     * @since 2.0.13
927
     */
928
    protected function truncateDatabase()
929
    {
930
        throw new NotSupportedException('This command is not implemented in ' . get_class($this));
931
    }
932
933
    /**
934
     * Returns the migration history.
935
     * @param int $limit the maximum number of records in the history to be returned. `null` for "no limit".
936
     * @return array the migration history
937
     */
938
    abstract protected function getMigrationHistory($limit);
939
940
    /**
941
     * Adds new migration entry to the history.
942
     * @param string $version migration version name.
943
     */
944
    abstract protected function addMigrationHistory($version);
945
946
    /**
947
     * Removes existing migration from the history.
948
     * @param string $version migration version name.
949
     */
950
    abstract protected function removeMigrationHistory($version);
951
}
952