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