MigrateController.php 15.1 KB
Newer Older
Qiang Xue committed
1 2
<?php
/**
Alexander Makarov committed
3
 * MigrateController class file.
Qiang Xue committed
4 5 6
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @link http://www.yiiframework.com/
Qiang Xue committed
7
 * @copyright Copyright &copy; 2008 Yii Software LLC
Qiang Xue committed
8 9 10
 * @license http://www.yiiframework.com/license/
 */

11 12
namespace yii\console\controllers;

Qiang Xue committed
13
use Yii;
14 15
use yii\console\Controller;

Qiang Xue committed
16
/**
17
 * This command provides support for database migrations.
Qiang Xue committed
18 19 20 21 22
 *
 * The implementation of this command and other supporting classes referenced
 * the yii-dbmigrations extension ((https://github.com/pieterclaerhout/yii-dbmigrations),
 * authored by Pieter Claerhout.
 *
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
 * EXAMPLES
 *
 * - yiic migrate
 *   Applies ALL new migrations. This is equivalent to 'yiic migrate up'.
 *
 * - yiic migrate create create_user_table
 *    Creates a new migration named 'create_user_table'.
 *
 * - yiic migrate up 3
 *   Applies the next 3 new migrations.
 *
 * - yiic migrate down
 *   Reverts the last applied migration.
 *
 * - yiic migrate down 3
 *   Reverts the last 3 applied migrations.
 *
 * - yiic migrate to 101129_185401
 *   Migrates up or down to version 101129_185401.
 *
 * - yiic migrate mark 101129_185401
 *   Modifies the migration history up or down to version 101129_185401.
 *   No actual migration will be performed.
 *
 * - yiic migrate history
 * Shows all previously applied migration information.
 *
 * - yiic migrate history 10
 * Shows the last 10 applied migrations.
 *
 * - yiic migrate new
 * Shows all new migrations.
 *
 * - yiic migrate new 10
 * Shows the next 10 migrations that have not been applied.
 *
Qiang Xue committed
59
 * @author Qiang Xue <qiang.xue@gmail.com>
60
 * @since 2.0
Qiang Xue committed
61
 */
62
class MigrateController extends Controller
Qiang Xue committed
63
{
Qiang Xue committed
64
	const BASE_MIGRATION = 'm000000_000000_base';
Qiang Xue committed
65 66 67 68 69 70

	/**
	 * @var string the directory that stores the migrations. This must be specified
	 * in terms of a path alias, and the corresponding directory must exist.
	 * Defaults to 'application.migrations' (meaning 'protected/migrations').
	 */
Qiang Xue committed
71
	public $migrationPath = '@application/migrations';
Qiang Xue committed
72 73 74 75 76
	/**
	 * @var string the name of the table for keeping applied migration information.
	 * This table will be automatically created if not exists. Defaults to 'tbl_migration'.
	 * The table structure is: (version varchar(255) primary key, apply_time integer)
	 */
Qiang Xue committed
77
	public $migrationTable = 'tbl_migration';
Qiang Xue committed
78 79 80 81
	/**
	 * @var string the application component ID that specifies the database connection for
	 * storing migration information. Defaults to 'db'.
	 */
Qiang Xue committed
82
	public $connectionID = 'db';
Qiang Xue committed
83 84 85 86 87 88 89 90 91
	/**
	 * @var string the path of the template file for generating new migrations. This
	 * must be specified in terms of a path alias (e.g. application.migrations.template).
	 * If not set, an internal template will be used.
	 */
	public $templateFile;
	/**
	 * @var string the default command action. It defaults to 'up'.
	 */
Qiang Xue committed
92
	public $defaultAction = 'up';
Qiang Xue committed
93 94 95 96
	/**
	 * @var boolean whether to execute the migration in an interactive mode. Defaults to true.
	 * Set this to false when performing migration in a cron job or background process.
	 */
Qiang Xue committed
97 98
	public $interactive = true;

Qiang Xue committed
99

100
	public function beforeAction($action)
Qiang Xue committed
101
	{
Qiang Xue committed
102 103 104 105 106 107 108 109 110 111 112
		if (parent::beforeAction($action)) {
			$path = Yii::getAlias($this->migrationPath);
			if ($path === false || !is_dir($path)) {
				echo 'Error: the migration directory does not exist "' . $this->migrationPath . "\"\n";
				return false;
			}
			$this->migrationPath = $path;
			$version = Yii::getVersion();
			echo "\nYii Migration Tool v2.0 (based on Yii v{$version})\n\n";
			return true;
		} else {
Qiang Xue committed
113
			return false;
114
		}
Qiang Xue committed
115 116
	}

117 118 119
	/**
	 * @param array $args
	 */
Qiang Xue committed
120 121
	public function actionUp($args)
	{
Qiang Xue committed
122
		if (($migrations = $this->getNewMigrations()) === array()) {
Qiang Xue committed
123
			echo "No new migration found. Your system is up-to-date.\n";
Qiang Xue committed
124
			Yii::$application->end();
Qiang Xue committed
125 126
		}

Qiang Xue committed
127 128 129 130
		$total = count($migrations);
		$step = isset($args[0]) ? (int)$args[0] : 0;
		if ($step > 0) {
			$migrations = array_slice($migrations, 0, $step);
131
		}
Qiang Xue committed
132

Qiang Xue committed
133
		$n = count($migrations);
Qiang Xue committed
134
		if ($n === $total) {
Qiang Xue committed
135
			echo "Total $n new " . ($n === 1 ? 'migration' : 'migrations') . " to be applied:\n";
Qiang Xue committed
136 137 138
		} else {
			echo "Total $n out of $total new " . ($total === 1 ? 'migration' : 'migrations') . " to be applied:\n";
		}
Qiang Xue committed
139

Qiang Xue committed
140
		foreach ($migrations as $migration) {
Qiang Xue committed
141
			echo "    $migration\n";
Qiang Xue committed
142
		}
Qiang Xue committed
143 144
		echo "\n";

Qiang Xue committed
145 146 147
		if ($this->confirm('Apply the above ' . ($n === 1 ? 'migration' : 'migrations') . "?")) {
			foreach ($migrations as $migration) {
				if ($this->migrateUp($migration) === false) {
Qiang Xue committed
148 149 150 151 152 153 154 155 156 157
					echo "\nMigration failed. All later migrations are canceled.\n";
					return;
				}
			}
			echo "\nMigrated up successfully.\n";
		}
	}

	public function actionDown($args)
	{
Qiang Xue committed
158
		$step = isset($args[0]) ? (int)$args[0] : 1;
Qiang Xue committed
159
		if ($step < 1) {
Qiang Xue committed
160
			die("Error: The step parameter must be greater than 0.\n");
Qiang Xue committed
161
		}
Qiang Xue committed
162

Qiang Xue committed
163
		if (($migrations = $this->getMigrationHistory($step)) === array()) {
Qiang Xue committed
164 165 166
			echo "No migration has been done before.\n";
			return;
		}
Qiang Xue committed
167
		$migrations = array_keys($migrations);
Qiang Xue committed
168

Qiang Xue committed
169 170
		$n = count($migrations);
		echo "Total $n " . ($n === 1 ? 'migration' : 'migrations') . " to be reverted:\n";
Qiang Xue committed
171
		foreach ($migrations as $migration) {
Qiang Xue committed
172
			echo "    $migration\n";
Qiang Xue committed
173
		}
Qiang Xue committed
174 175
		echo "\n";

Qiang Xue committed
176 177 178
		if ($this->confirm('Revert the above ' . ($n === 1 ? 'migration' : 'migrations') . "?")) {
			foreach ($migrations as $migration) {
				if ($this->migrateDown($migration) === false) {
Qiang Xue committed
179 180 181 182 183 184 185 186 187 188
					echo "\nMigration failed. All later migrations are canceled.\n";
					return;
				}
			}
			echo "\nMigrated down successfully.\n";
		}
	}

	public function actionRedo($args)
	{
Qiang Xue committed
189
		$step = isset($args[0]) ? (int)$args[0] : 1;
Qiang Xue committed
190
		if ($step < 1) {
Qiang Xue committed
191
			die("Error: The step parameter must be greater than 0.\n");
Qiang Xue committed
192
		}
Qiang Xue committed
193

Qiang Xue committed
194
		if (($migrations = $this->getMigrationHistory($step)) === array()) {
Qiang Xue committed
195 196 197
			echo "No migration has been done before.\n";
			return;
		}
Qiang Xue committed
198
		$migrations = array_keys($migrations);
Qiang Xue committed
199

Qiang Xue committed
200 201
		$n = count($migrations);
		echo "Total $n " . ($n === 1 ? 'migration' : 'migrations') . " to be redone:\n";
Qiang Xue committed
202
		foreach ($migrations as $migration) {
Qiang Xue committed
203
			echo "    $migration\n";
Qiang Xue committed
204
		}
Qiang Xue committed
205 206
		echo "\n";

Qiang Xue committed
207 208 209
		if ($this->confirm('Redo the above ' . ($n === 1 ? 'migration' : 'migrations') . "?")) {
			foreach ($migrations as $migration) {
				if ($this->migrateDown($migration) === false) {
Qiang Xue committed
210 211 212 213
					echo "\nMigration failed. All later migrations are canceled.\n";
					return;
				}
			}
Qiang Xue committed
214 215
			foreach (array_reverse($migrations) as $migration) {
				if ($this->migrateUp($migration) === false) {
Qiang Xue committed
216 217 218 219 220 221 222 223 224 225
					echo "\nMigration failed. All later migrations are canceled.\n";
					return;
				}
			}
			echo "\nMigration redone successfully.\n";
		}
	}

	public function actionTo($args)
	{
Qiang Xue committed
226
		if (isset($args[0])) {
Qiang Xue committed
227
			$version = $args[0];
Qiang Xue committed
228
		} else {
Qiang Xue committed
229
			$this->usageError('Please specify which version to migrate to.');
Qiang Xue committed
230
		}
Qiang Xue committed
231

Qiang Xue committed
232
		$originalVersion = $version;
Qiang Xue committed
233
		if (preg_match('/^m?(\d{6}_\d{6})(_.*?)?$/', $version, $matches)) {
Qiang Xue committed
234
			$version = 'm' . $matches[1];
Qiang Xue committed
235
		} else {
Qiang Xue committed
236
			die("Error: The version option must be either a timestamp (e.g. 101129_185401)\nor the full name of a migration (e.g. m101129_185401_create_user_table).\n");
Qiang Xue committed
237
		}
Qiang Xue committed
238 239

		// try migrate up
Qiang Xue committed
240 241 242 243
		$migrations = $this->getNewMigrations();
		foreach ($migrations as $i => $migration) {
			if (strpos($migration, $version . '_') === 0) {
				$this->actionUp(array($i + 1));
Qiang Xue committed
244 245 246 247 248
				return;
			}
		}

		// try migrate down
Qiang Xue committed
249 250 251
		$migrations = array_keys($this->getMigrationHistory(-1));
		foreach ($migrations as $i => $migration) {
			if (strpos($migration, $version . '_') === 0) {
Qiang Xue committed
252
				if ($i === 0) {
Qiang Xue committed
253
					echo "Already at '$originalVersion'. Nothing needs to be done.\n";
Qiang Xue committed
254
				} else {
Qiang Xue committed
255
					$this->actionDown(array($i));
Qiang Xue committed
256
				}
Qiang Xue committed
257 258 259 260 261 262 263 264 265
				return;
			}
		}

		die("Error: Unable to find the version '$originalVersion'.\n");
	}

	public function actionMark($args)
	{
Qiang Xue committed
266
		if (isset($args[0])) {
Qiang Xue committed
267
			$version = $args[0];
Qiang Xue committed
268
		} else {
Qiang Xue committed
269
			$this->usageError('Please specify which version to mark to.');
Qiang Xue committed
270
		}
Qiang Xue committed
271
		$originalVersion = $version;
Qiang Xue committed
272
		if (preg_match('/^m?(\d{6}_\d{6})(_.*?)?$/', $version, $matches)) {
Qiang Xue committed
273
			$version = 'm' . $matches[1];
Qiang Xue committed
274
		} else {
Qiang Xue committed
275
			die("Error: The version option must be either a timestamp (e.g. 101129_185401)\nor the full name of a migration (e.g. m101129_185401_create_user_table).\n");
Qiang Xue committed
276
		}
Qiang Xue committed
277

Qiang Xue committed
278
		$db = $this->getDb();
Qiang Xue committed
279 280

		// try mark up
Qiang Xue committed
281 282 283 284 285 286
		$migrations = $this->getNewMigrations();
		foreach ($migrations as $i => $migration) {
			if (strpos($migration, $version . '_') === 0) {
				if ($this->confirm("Set migration history at $originalVersion?")) {
					$command = $db->createCommand();
					for ($j = 0; $j <= $i; ++$j) {
Qiang Xue committed
287
						$command->insert($this->migrationTable, array(
Qiang Xue committed
288 289
							'version' => $migrations[$j],
							'apply_time' => time(),
Qiang Xue committed
290 291 292 293 294 295 296 297 298
						));
					}
					echo "The migration history is set at $originalVersion.\nNo actual migration was performed.\n";
				}
				return;
			}
		}

		// try mark down
Qiang Xue committed
299 300 301
		$migrations = array_keys($this->getMigrationHistory(-1));
		foreach ($migrations as $i => $migration) {
			if (strpos($migration, $version . '_') === 0) {
Qiang Xue committed
302
				if ($i === 0) {
Qiang Xue committed
303
					echo "Already at '$originalVersion'. Nothing needs to be done.\n";
Qiang Xue committed
304
				} else {
Qiang Xue committed
305 306
					if ($this->confirm("Set migration history at $originalVersion?")) {
						$command = $db->createCommand();
Qiang Xue committed
307
						for ($j = 0; $j < $i; ++$j) {
Qiang Xue committed
308
							$command->delete($this->migrationTable, $db->quoteColumnName('version') . '=:version', array(':version' => $migrations[$j]));
Qiang Xue committed
309
						}
Qiang Xue committed
310 311 312 313 314 315 316 317 318 319 320 321
						echo "The migration history is set at $originalVersion.\nNo actual migration was performed.\n";
					}
				}
				return;
			}
		}

		die("Error: Unable to find the version '$originalVersion'.\n");
	}

	public function actionHistory($args)
	{
Qiang Xue committed
322 323
		$limit = isset($args[0]) ? (int)$args[0] : -1;
		$migrations = $this->getMigrationHistory($limit);
Qiang Xue committed
324
		if ($migrations === array()) {
Qiang Xue committed
325
			echo "No migration has been done before.\n";
Qiang Xue committed
326
		} else {
Qiang Xue committed
327
			$n = count($migrations);
Qiang Xue committed
328
			if ($limit > 0) {
Qiang Xue committed
329
				echo "Showing the last $n applied " . ($n === 1 ? 'migration' : 'migrations') . ":\n";
Qiang Xue committed
330
			} else {
Qiang Xue committed
331
				echo "Total $n " . ($n === 1 ? 'migration has' : 'migrations have') . " been applied before:\n";
Qiang Xue committed
332 333
			}
			foreach ($migrations as $version => $time) {
Qiang Xue committed
334
				echo "    (" . date('Y-m-d H:i:s', $time) . ') ' . $version . "\n";
Qiang Xue committed
335
			}
Qiang Xue committed
336 337 338 339 340
		}
	}

	public function actionNew($args)
	{
Qiang Xue committed
341 342
		$limit = isset($args[0]) ? (int)$args[0] : -1;
		$migrations = $this->getNewMigrations();
Qiang Xue committed
343
		if ($migrations === array()) {
Qiang Xue committed
344
			echo "No new migrations found. Your system is up-to-date.\n";
Qiang Xue committed
345
		} else {
Qiang Xue committed
346 347 348 349
			$n = count($migrations);
			if ($limit > 0 && $n > $limit) {
				$migrations = array_slice($migrations, 0, $limit);
				echo "Showing $limit out of $n new " . ($n === 1 ? 'migration' : 'migrations') . ":\n";
Qiang Xue committed
350
			} else {
Qiang Xue committed
351
				echo "Found $n new " . ($n === 1 ? 'migration' : 'migrations') . ":\n";
Qiang Xue committed
352
			}
Qiang Xue committed
353

Qiang Xue committed
354
			foreach ($migrations as $migration) {
Qiang Xue committed
355
				echo "    " . $migration . "\n";
Qiang Xue committed
356
			}
Qiang Xue committed
357 358 359 360 361
		}
	}

	public function actionCreate($args)
	{
Qiang Xue committed
362
		if (isset($args[0])) {
Qiang Xue committed
363
			$name = $args[0];
Qiang Xue committed
364
		} else {
Qiang Xue committed
365
			$this->usageError('Please provide the name of the new migration.');
Qiang Xue committed
366
		}
Qiang Xue committed
367

Qiang Xue committed
368
		if (!preg_match('/^\w+$/', $name)) {
Qiang Xue committed
369
			die("Error: The name of the migration must contain letters, digits and/or underscore characters only.\n");
Qiang Xue committed
370
		}
Qiang Xue committed
371

Qiang Xue committed
372 373 374
		$name = 'm' . gmdate('ymd_His') . '_' . $name;
		$content = strtr($this->getTemplate(), array('{ClassName}' => $name));
		$file = $this->migrationPath . DIRECTORY_SEPARATOR . $name . '.php';
Qiang Xue committed
375

Qiang Xue committed
376
		if ($this->confirm("Create new migration '$file'?")) {
Qiang Xue committed
377 378 379 380 381 382 383
			file_put_contents($file, $content);
			echo "New migration created successfully.\n";
		}
	}

	protected function migrateUp($class)
	{
Qiang Xue committed
384
		if ($class === self::BASE_MIGRATION) {
Qiang Xue committed
385
			return;
Qiang Xue committed
386
		}
Qiang Xue committed
387 388

		echo "*** applying $class\n";
Qiang Xue committed
389 390 391
		$start = microtime(true);
		$migration = $this->instantiateMigration($class);
		if ($migration->up() !== false) {
Qiang Xue committed
392
			$this->getDb()->createCommand()->insert($this->migrationTable, array(
Qiang Xue committed
393 394
				'version' => $class,
				'apply_time' => time(),
Qiang Xue committed
395
			));
Qiang Xue committed
396 397 398 399 400
			$time = microtime(true) - $start;
			echo "*** applied $class (time: " . sprintf("%.3f", $time) . "s)\n\n";
		} else {
			$time = microtime(true) - $start;
			echo "*** failed to apply $class (time: " . sprintf("%.3f", $time) . "s)\n\n";
Qiang Xue committed
401 402 403 404 405 406
			return false;
		}
	}

	protected function migrateDown($class)
	{
Qiang Xue committed
407
		if ($class === self::BASE_MIGRATION) {
Qiang Xue committed
408
			return;
Qiang Xue committed
409
		}
Qiang Xue committed
410 411

		echo "*** reverting $class\n";
Qiang Xue committed
412 413 414 415 416 417 418 419 420 421
		$start = microtime(true);
		$migration = $this->instantiateMigration($class);
		if ($migration->down() !== false) {
			$db = $this->getDb();
			$db->createCommand()->delete($this->migrationTable, $db->quoteColumnName('version') . '=:version', array(':version' => $class));
			$time = microtime(true) - $start;
			echo "*** reverted $class (time: " . sprintf("%.3f", $time) . "s)\n\n";
		} else {
			$time = microtime(true) - $start;
			echo "*** failed to revert $class (time: " . sprintf("%.3f", $time) . "s)\n\n";
Qiang Xue committed
422 423 424 425 426 427
			return false;
		}
	}

	protected function instantiateMigration($class)
	{
Qiang Xue committed
428
		$file = $this->migrationPath . DIRECTORY_SEPARATOR . $class . '.php';
Qiang Xue committed
429
		require_once($file);
Qiang Xue committed
430
		$migration = new $class;
Qiang Xue committed
431
		$migration->setDb($this->getDb());
Qiang Xue committed
432 433 434 435 436 437 438
		return $migration;
	}

	/**
	 * @var CDbConnection
	 */
	private $_db;
Qiang Xue committed
439

Qiang Xue committed
440
	protected function getDb()
Qiang Xue committed
441
	{
Qiang Xue committed
442
		if ($this->_db !== null) {
Qiang Xue committed
443
			return $this->_db;
Qiang Xue committed
444 445 446 447 448 449 450
		} else {
			if (($this->_db = Yii::$application->getComponent($this->connectionID)) instanceof CDbConnection) {
				return $this->_db;
			} else {
				die("Error: CMigrationCommand.connectionID '{$this->connectionID}' is invalid. Please make sure it refers to the ID of a CDbConnection application component.\n");
			}
		}
Qiang Xue committed
451 452 453 454
	}

	protected function getMigrationHistory($limit)
	{
Qiang Xue committed
455 456
		$db = $this->getDb();
		if ($db->schema->getTable($this->migrationTable) === null) {
Qiang Xue committed
457 458 459 460 461 462 463 464 465 466 467 468
			$this->createMigrationHistoryTable();
		}
		return CHtml::listData($db->createCommand()
			->select('version, apply_time')
			->from($this->migrationTable)
			->order('version DESC')
			->limit($limit)
			->queryAll(), 'version', 'apply_time');
	}

	protected function createMigrationHistoryTable()
	{
Qiang Xue committed
469 470 471 472 473
		$db = $this->getDb();
		echo 'Creating migration history table "' . $this->migrationTable . '"...';
		$db->createCommand()->createTable($this->migrationTable, array(
			'version' => 'string NOT NULL PRIMARY KEY',
			'apply_time' => 'integer',
Qiang Xue committed
474
		));
Qiang Xue committed
475 476 477
		$db->createCommand()->insert($this->migrationTable, array(
			'version' => self::BASE_MIGRATION,
			'apply_time' => time(),
Qiang Xue committed
478 479 480 481 482 483
		));
		echo "done.\n";
	}

	protected function getNewMigrations()
	{
Qiang Xue committed
484
		$applied = array();
Qiang Xue committed
485
		foreach ($this->getMigrationHistory(-1) as $version => $time) {
Qiang Xue committed
486
			$applied[substr($version, 1, 13)] = true;
Qiang Xue committed
487
		}
Qiang Xue committed
488 489 490 491

		$migrations = array();
		$handle = opendir($this->migrationPath);
		while (($file = readdir($handle)) !== false) {
Qiang Xue committed
492
			if ($file === '.' || $file === '..') {
Qiang Xue committed
493
				continue;
Qiang Xue committed
494
			}
Qiang Xue committed
495
			$path = $this->migrationPath . DIRECTORY_SEPARATOR . $file;
Qiang Xue committed
496
			if (preg_match('/^(m(\d{6}_\d{6})_.*?)\.php$/', $file, $matches) && is_file($path) && !isset($applied[$matches[2]])) {
Qiang Xue committed
497
				$migrations[] = $matches[1];
Qiang Xue committed
498
			}
Qiang Xue committed
499 500 501 502 503 504 505 506
		}
		closedir($handle);
		sort($migrations);
		return $migrations;
	}

	protected function getTemplate()
	{
Qiang Xue committed
507
		if ($this->templateFile !== null) {
Qiang Xue committed
508
			return file_get_contents(Yii::getPathOfAlias($this->templateFile) . '.php');
Qiang Xue committed
509
		} else {
Qiang Xue committed
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
			return <<<EOD
<?php

class {ClassName} extends CDbMigration
{
	public function up()
	{
	}

	public function down()
	{
		echo "{ClassName} does not support migration down.\\n";
		return false;
	}

	/*
	// Use safeUp/safeDown to do migration with transaction
	public function safeUp()
	{
	}

	public function safeDown()
	{
	}
	*/
}
EOD;
Qiang Xue committed
537
		}
Qiang Xue committed
538 539
	}
}