-
Notifications
You must be signed in to change notification settings - Fork 641
/
Copy pathMigrateController.php
611 lines (537 loc) · 20.4 KB
/
MigrateController.php
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
<?php
/**
* @link https://craftcms.com/
* @copyright Copyright (c) Pixel & Tonic, Inc.
* @license https://craftcms.github.io/license/
*/
namespace craft\console\controllers;
use Craft;
use craft\base\PluginInterface;
use craft\console\ControllerTrait;
use craft\db\MigrationManager;
use craft\errors\InvalidPluginException;
use craft\errors\MigrateException;
use craft\events\RegisterMigratorEvent;
use craft\helpers\ArrayHelper;
use craft\helpers\FileHelper;
use yii\base\ErrorException;
use yii\base\InvalidArgumentException;
use yii\base\InvalidConfigException;
use yii\base\NotSupportedException;
use yii\console\controllers\BaseMigrateController;
use yii\console\Exception;
use yii\console\ExitCode;
use yii\db\MigrationInterface;
use yii\helpers\Console;
/**
* Manages Craft and plugin migrations.
*
* A migration means a set of persistent changes to the application environment that is shared among different
* developers. For example, in an application backed by a database, a migration may refer to a set of changes to
* the database, such as creating a new table, adding a new table column.
* This controller provides support for tracking the migration history, updating migrations, and creating new
* migration skeleton files.
* The migration history is stored in a database table named `migrations`. The table will be automatically
* created the first time this controller is executed, if it does not exist.
* Below are some common usages of this command:
* ~~~
* # creates a new migration named 'create_user_table' for a plugin with the handle pluginHandle.
* craft migrate/create create_user_table --plugin=pluginHandle
* # applies ALL new migrations for a plugin with the handle pluginHandle
* craft migrate up --plugin=pluginHandle
* ~~~
*
* @author Pixel & Tonic, Inc. <[email protected]>
* @since 3.0.0
*/
class MigrateController extends BaseMigrateController
{
use ControllerTrait {
ControllerTrait::init as private traitInit;
ControllerTrait::options as private traitOptions;
ControllerTrait::beforeAction as private traitBeforeAction;
}
use BackupTrait;
/**
* @event RegisterMigratorEvent The event that is triggered when resolving an unknown migration track.
*
* ```php
* use craft\console\controllers\MigrateController;
* use craft\db\MigrationManager;
* use craft\events\RegisterMigratorEvent;
* use yii\base\Event;
*
* Event::on(
* MigrateController::class,
* MigrateController::EVENT_REGISTER_MIGRATOR,
* function(RegisterMigratorEvent $event) {
* if ($event->track === 'myCustomTrack') {
* $event->migrator = Craft::createObject([
* 'class' => MigrationManager::class,
* 'track' => 'myCustomTrack',
* 'migrationNamespace' => 'my\migration\namespace',
* 'migrationPath' => '/path/to/migrations',
* ]);
* $event->handled = true;
* }
* }
* );
* ```
*
* @since 3.5.0
*/
public const EVENT_REGISTER_MIGRATOR = 'registerMigrator';
/**
* @var string|null The migration track to work with (e.g. `craft`, `content`, `plugin:commerce`, etc.)
*
* Defaults to `content`, or automatically set to the plugin’s track when `--plugin` is passed.
* @since 3.5.0
*/
public ?string $track = MigrationManager::TRACK_CONTENT;
/**
* @var string|null DEPRECATED. Use `--track` instead.
* @deprecated in 3.5.0. Use [[track]] instead.
*/
public ?string $type = null;
/**
* @var string|PluginInterface|null The handle of the plugin to use during migration operations, or the plugin itself.
*/
public PluginInterface|string|null $plugin = null;
/**
* @var bool Exclude pending content migrations.
*/
public bool $noContent = false;
/**
* @var bool Skip backing up the database.
* @since 3.4.3
*/
public bool $noBackup = false;
/**
* @var MigrationManager[] Migration managers that will be used in this request
*/
private array $_migrators;
/**
* @inheritdoc
*/
public function init(): void
{
$this->traitInit();
$this->templateFile = Craft::getAlias('@app/updates/migration.php.template');
}
/**
* Returns the names of valid options for the action (id)
* An option requires the existence of a public member variable whose
* name is the option name.
* Child classes may override this method to specify possible options.
*
* Note that the values setting via options are not available
* until [[beforeAction()]] is being called.
*
* @param string $actionID the action ID of the current request
* @return string[] the names of the options valid for the action
*/
public function options($actionID): array
{
$options = $this->traitOptions($actionID);
// Remove options we end up overriding
ArrayHelper::removeValue($options, 'migrationPath');
ArrayHelper::removeValue($options, 'migrationNamespaces');
ArrayHelper::removeValue($options, 'compact');
if ($actionID === 'all') {
$options[] = 'noBackup';
$options[] = 'noContent';
} else {
$options[] = 'track';
$options[] = 'plugin';
}
return $options;
}
/**
* @inheritdoc
*/
public function optionAliases(): array
{
$aliases = parent::optionAliases();
$aliases['p'] = 'plugin';
return $aliases;
}
/**
* @inheritdoc
*/
public function runAction($id, $params = []): ?int
{
// Make sure that the project config YAML exists in case any migrations need to check incoming YAML values
$projectConfig = Craft::$app->getProjectConfig();
if ($projectConfig->writeYamlAutomatically && !$projectConfig->getDoesExternalConfigExist()) {
$projectConfig->regenerateExternalConfig();
} elseif ($projectConfig->areChangesPending(force: true)) {
// allow project config changes, but don't overwrite the pending changes
$readOnly = $projectConfig->readOnly;
$writeYamlAutomatically = $projectConfig->writeYamlAutomatically;
$projectConfig->readOnly = false;
$projectConfig->writeYamlAutomatically = false;
}
try {
return parent::runAction($id, $params);
} finally {
if (isset($readOnly, $writeYamlAutomatically)) {
$projectConfig->readOnly = $readOnly;
$projectConfig->writeYamlAutomatically = $writeYamlAutomatically;
}
}
}
/**
* @inheritdoc
*/
public function beforeAction($action): bool
{
if ($action->id !== 'all') {
if ($this->plugin) {
$this->track = "plugin:$this->plugin";
} elseif ($this->track && preg_match('/^plugin:([\w\-]+)$/', $this->track, $match)) {
$this->plugin = $match[1];
}
// Validate $plugin
if ($this->plugin) {
// Make sure $this->plugin in set to a valid plugin handle
if (empty($this->plugin)) {
$this->stderr('You must specify the plugin handle using the --plugin option.' . PHP_EOL, Console::FG_RED);
return false;
}
try {
$this->plugin = $this->_plugin($this->plugin);
} catch (InvalidPluginException) {
$this->stderr("Invalid plugin handle: $this->plugin" . PHP_EOL, Console::FG_RED);
return false;
}
}
$this->migrationPath = $this->getMigrator()->migrationPath;
}
try {
if (!$this->traitBeforeAction($action)) {
return false;
}
} catch (InvalidConfigException $e) {
// migrations folder not created, but we don't mind.
}
return true;
}
/**
* 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.
*
* ```
* craft migrate/create create_news_section
* ```
*
* By default, the migration is created in the project’s `migrations/`
* folder (as a “content migration”).\
* Use `--plugin=<plugin-handle>` to create a new plugin migration.\
* Use `--type=app` to create a new Craft CMS app migration.
*
* @param string $name the name of the new migration. This should only contain
* letters, digits, and underscores.
* @return int
* @throws Exception if the name argument is invalid.
*/
public function actionCreate($name): int
{
if (!preg_match('/^\w+$/', $name)) {
throw new Exception('The migration name should contain letters, digits and/or underscore characters only.');
}
if ($isInstall = (strcasecmp($name, 'install') === 0)) {
$name = 'Install';
} else {
$name = 'm' . gmdate('ymd_His') . '_' . $name;
}
$file = $this->migrationPath . DIRECTORY_SEPARATOR . $name . '.php';
if (!$this->interactive || $this->confirm("Create new migration '$file'?", true)) {
$templateFile = Craft::getAlias($this->templateFile);
if ($templateFile === false) {
throw new Exception('There was a problem getting the template file path');
}
$content = $this->renderFile($templateFile, [
'isInstall' => $isInstall,
'namespace' => $this->getMigrator()->migrationNamespace,
'className' => $name,
]);
FileHelper::writeToFile($file, $content);
$this->stdout('New migration created successfully.' . PHP_EOL, Console::FG_GREEN);
}
return ExitCode::OK;
}
/**
* Runs all pending Craft, plugin, and content migrations.
*
* @return int
* @throws MigrateException
*/
public function actionAll(): int
{
if ($this->noContent) {
$this->stdout("Checking for pending Craft and plugin migrations ...\n");
} else {
$this->stdout("Checking for pending migrations ...\n");
}
$migrationsByTrack = [];
$updatesService = Craft::$app->getUpdates();
$craftMigrations = Craft::$app->getMigrator()->getNewMigrations();
if (!empty($craftMigrations) || $updatesService->getIsCraftUpdatePending()) {
$migrationsByTrack[MigrationManager::TRACK_CRAFT] = $craftMigrations;
}
$pluginsService = Craft::$app->getPlugins();
$plugins = $pluginsService->getAllPlugins();
foreach ($plugins as $plugin) {
$pluginMigrations = $plugin->getMigrator()->getNewMigrations();
if (!empty($pluginMigrations) || $pluginsService->isPluginUpdatePending($plugin)) {
$migrationsByTrack["plugin:$plugin->id"] = $pluginMigrations;
}
}
if (!$this->noContent) {
$contentMigrations = Craft::$app->getContentMigrator()->getNewMigrations();
if (!empty($contentMigrations)) {
$migrationsByTrack[MigrationManager::TRACK_CONTENT] = $contentMigrations;
}
}
if (empty($migrationsByTrack)) {
$this->stdout('No new migrations found. Your system is up to date.' . PHP_EOL, Console::FG_GREEN);
return ExitCode::OK;
}
$total = 0;
foreach ($migrationsByTrack as $track => $migrations) {
$n = count($migrations);
$which = match ($track) {
MigrationManager::TRACK_CRAFT => 'Craft',
MigrationManager::TRACK_CONTENT => 'content',
default => $plugins[substr($track, 7)]->name,
};
$this->stdout("Total $n new $which " . ($n === 1 ? 'migration' : 'migrations') . ' to be applied:' . PHP_EOL, Console::FG_YELLOW);
foreach ($migrations as $migration) {
$this->stdout(" - $migration" . PHP_EOL);
}
$this->stdout(PHP_EOL);
$total += $n;
}
if ($this->interactive && !$this->confirm('Apply the above ' . ($total === 1 ? 'migration' : 'migrations') . '?')) {
return ExitCode::OK;
}
// Enable maintenance mode
Craft::$app->enableMaintenanceMode();
// Backup the DB
if (!$this->noBackup && !$this->backup()) {
Craft::$app->disableMaintenanceMode();
return ExitCode::UNSPECIFIED_ERROR;
}
$applied = 0;
foreach ($migrationsByTrack as $track => $migrations) {
$this->track = $track;
foreach ($migrations as $migration) {
if (!$this->migrateUp($migration)) {
$this->stdout(PHP_EOL . "$applied from $total " . ($applied === 1 ? 'migration was' : 'migrations were') . ' applied.' . PHP_EOL, Console::FG_RED);
$this->stdout(PHP_EOL . 'Migration failed. The rest of the migrations are canceled.' . PHP_EOL, Console::FG_RED);
Craft::$app->disableMaintenanceMode();
Craft::$app->getProjectConfig()->reset();
if (!$this->restore()) {
$this->stdout("\nRestore a database backup before trying again.\n", Console::FG_RED);
}
return ExitCode::UNSPECIFIED_ERROR;
}
$applied++;
}
// Update version info
if ($track === MigrationManager::TRACK_CRAFT) {
Craft::$app->getUpdates()->updateCraftVersionInfo();
} elseif ($track !== MigrationManager::TRACK_CONTENT) {
Craft::$app->getPlugins()->updatePluginVersionInfo($plugins[substr($track, 7)]);
}
}
$this->stdout(PHP_EOL . "$total " . ($total === 1 ? 'migration was' : 'migrations were') . ' applied.' . PHP_EOL, Console::FG_GREEN);
$this->stdout(PHP_EOL . 'Migrated up successfully.' . PHP_EOL, Console::FG_GREEN);
Craft::$app->disableMaintenanceMode();
$this->_clearCompiledTemplates();
return ExitCode::OK;
}
/**
* Upgrades Craft by applying new migrations.
*
* Example:
* ```
* php craft migrate # apply all new migrations
* php craft migrate 3 # apply the first 3 new migrations
* ```
*
* @param int $limit The number of new migrations to be applied. If `0`, every new migration
* will be applied.
*
* @return int the status of the action execution. 0 means normal, other values mean abnormal.
*/
public function actionUp($limit = 0): int
{
switch ($this->track) {
case MigrationManager::TRACK_CRAFT:
$this->stdout("Checking for pending Craft migrations ...\n");
break;
case MigrationManager::TRACK_CONTENT:
$this->stdout("Checking for pending content migrations ...\n");
break;
default:
if ($this->plugin instanceof PluginInterface) {
$this->stdout("Checking for pending {$this->plugin->name} migrations ...\n");
}
}
$res = parent::actionUp($limit);
if ($res === ExitCode::UNSPECIFIED_ERROR) {
Craft::$app->getProjectConfig()->reset();
if (!$this->restore()) {
$this->stdout("\nRestore a database backup before trying again.\n", Console::FG_RED);
}
return $res;
}
if ($res === ExitCode::OK && empty($this->getNewMigrations())) {
// Update any schema versions.
if ($this->track === MigrationManager::TRACK_CRAFT) {
Craft::$app->getUpdates()->updateCraftVersionInfo();
} elseif ($this->plugin) {
Craft::$app->getPlugins()->updatePluginVersionInfo($this->plugin);
}
$this->_clearCompiledTemplates();
}
return $res;
}
/**
* Returns a plugin by its handle.
*
* @param string $handle
* @return PluginInterface
* @throws InvalidPluginException
*/
private function _plugin(string $handle): PluginInterface
{
$pluginsService = Craft::$app->getPlugins();
if ($plugin = $pluginsService->getPlugin($handle)) {
return $plugin;
}
return $pluginsService->createPlugin($handle);
}
/**
* Clears all compiled templates.
*/
private function _clearCompiledTemplates(): void
{
try {
FileHelper::clearDirectory(Craft::$app->getPath()->getCompiledTemplatesPath(false));
} catch (InvalidArgumentException) {
// the directory doesn't exist
} catch (ErrorException $e) {
Craft::error('Could not delete compiled templates: ' . $e->getMessage());
Craft::$app->getErrorHandler()->logException($e);
}
}
/**
* Returns a migration manager.
*
* @param string|null $track
* @return MigrationManager
* @throws InvalidPluginException
* @throws InvalidConfigException
*/
public function getMigrator(?string $track = null): MigrationManager
{
if ($track === null) {
$track = $this->track;
}
if (!isset($this->_migrators[$track])) {
if (preg_match('/^plugin:([\w\-]+)$/', $track, $match)) {
$this->_migrators[$track] = $this->_plugin($match[1])->getMigrator();
} else {
switch ($track) {
case MigrationManager::TRACK_CRAFT:
$this->_migrators[$track] = Craft::$app->getMigrator();
break;
case MigrationManager::TRACK_CONTENT:
$this->_migrators[$track] = Craft::$app->getContentMigrator();
break;
default:
// Fire a 'registerMigrator' event
if ($this->hasEventHandlers(self::EVENT_REGISTER_MIGRATOR)) {
$event = new RegisterMigratorEvent(['track' => $track]);
$this->trigger(self::EVENT_REGISTER_MIGRATOR, $event);
$migrator = $event->migrator;
} else {
$migrator = null;
}
if (!$migrator) {
throw new InvalidConfigException("Invalid migration track: $track");
}
$this->_migrators[$track] = $migrator;
}
}
}
return $this->_migrators[$track];
}
/**
* @inheritdoc
*/
protected function createMigration($class): MigrationInterface
{
return $this->getMigrator()->createMigration($class);
}
/**
* @inheritdoc
*/
protected function getNewMigrations(): array
{
return $this->getMigrator()->getNewMigrations();
}
/**
* @inheritdoc
*/
protected function getMigrationHistory($limit): array
{
$history = $this->getMigrator()->getMigrationHistory((int)$limit);
// Convert values to unix timestamps
return array_map('strtotime', $history);
}
/**
* @inheritdoc
*/
protected function addMigrationHistory($version): void
{
$this->getMigrator()->addMigrationHistory($version);
}
/**
* @inheritdoc
*/
protected function removeMigrationHistory($version): void
{
$this->getMigrator()->removeMigrationHistory($version);
}
/**
* Not supported.
*/
public function actionFresh(): int
{
$this->stderr('This command is not supported.' . PHP_EOL, Console::FG_RED);
return ExitCode::OK;
}
/**
* @inheritdoc
*/
protected function truncateDatabase(): void
{
throw new NotSupportedException('This command is not implemented in ' . get_class($this));
}
/**
* @inheritdoc
*/
public function stdout($string): bool|int
{
if (str_starts_with($string, 'Yii Migration Tool')) {
return false;
}
return parent::stdout(...func_get_args());
}
}