MigrateController.php 19.3 KB
Newer Older
Qiang Xue committed
1 2 3 4
<?php
/**
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @link http://www.yiiframework.com/
Qiang Xue committed
5
 * @copyright Copyright (c) 2008 Yii Software LLC
Qiang Xue committed
6 7 8
 * @license http://www.yiiframework.com/license/
 */

9 10
namespace yii\console\controllers;

Qiang Xue committed
11
use Yii;
Qiang Xue committed
12
use yii\console\Exception;
13
use yii\console\Controller;
Qiang Xue committed
14
use yii\db\Connection;
Qiang Xue committed
15 16
use yii\db\Query;
use yii\util\ArrayHelper;
17

Qiang Xue committed
18
/**
Qiang Xue committed
19
 * This command manages application migrations.
Qiang Xue committed
20
 *
Qiang Xue committed
21 22
 * A migration means a set of persistent changes to the application environment
 * that is shared among different developers. For example, in an application
23 24
 * 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.
25
 *
Qiang Xue committed
26 27
 * This command provides support for tracking the migration history, upgrading
 * or downloading with migrations, and creating new migration skeletons.
28
 *
Qiang Xue committed
29 30 31
 * The migration history is stored in a database table named as [[migrationTable]].
 * The table will be automatically created the first this command is executed.
 * You may also manually create it with the following structure:
32
 *
Qiang Xue committed
33 34 35 36 37 38
 * ~~~
 * CREATE TABLE tbl_migration (
 *     version varchar(255) PRIMARY KEY,
 *     apply_time integer
 * )
 * ~~~
39
 *
Qiang Xue committed
40
 * Below are some common usages of this command:
41
 *
Qiang Xue committed
42 43 44
 * ~~~
 * # creates a new migration named 'create_user_table'
 * yiic migrate/create create_user_table
45
 *
Qiang Xue committed
46 47
 * # applies ALL new migrations
 * yiic migrate
48
 *
Qiang Xue committed
49 50 51
 * # reverts the last applied migration
 * yiic migrate/down
 * ~~~
52
 *
Qiang Xue committed
53
 * @author Qiang Xue <qiang.xue@gmail.com>
54
 * @since 2.0
Qiang Xue committed
55
 */
56
class MigrateController extends Controller
Qiang Xue committed
57
{
Qiang Xue committed
58 59 60
	/**
	 * The name of the dummy migration that marks the beginning of the whole migration history.
	 */
Qiang Xue committed
61
	const BASE_MIGRATION = 'm000000_000000_base';
Qiang Xue committed
62 63

	/**
Qiang Xue committed
64 65 66 67 68 69
	 * @var string the default command action.
	 */
	public $defaultAction = 'up';
	/**
	 * @var string the directory storing the migration classes. This can be either
	 * a path alias or a directory.
Qiang Xue committed
70
	 */
Qiang Xue committed
71
	public $migrationPath = '@app/migrations';
Qiang Xue committed
72 73 74
	/**
	 * @var string the name of the table for keeping applied migration information.
	 */
Qiang Xue committed
75
	public $migrationTable = 'tbl_migration';
Qiang Xue committed
76
	/**
Qiang Xue committed
77 78
	 * @var string the component ID that specifies the database connection for
	 * storing migration information.
Qiang Xue committed
79
	 */
Qiang Xue committed
80
	public $connectionID = 'db';
Qiang Xue committed
81
	/**
Qiang Xue committed
82
	 * @var string the template file for generating new migrations.
Qiang Xue committed
83
	 * This can be either a path alias (e.g. "@app/migrations/template.php")
Qiang Xue committed
84
	 * or a file path.
Qiang Xue committed
85
	 */
Qiang Xue committed
86
	public $templateFile = '@yii/views/migration.php';
Qiang Xue committed
87
	/**
Qiang Xue committed
88
	 * @var boolean whether to execute the migration in an interactive mode.
Qiang Xue committed
89
	 */
Qiang Xue committed
90
	public $interactive = true;
Qiang Xue committed
91 92 93 94 95
	/**
	 * @var Connection the DB connection used for storing migration history.
	 * @see connectionID
	 */
	public $db;
Qiang Xue committed
96

Qiang Xue committed
97 98 99 100
	/**
	 * Returns the names of the global options for this command.
	 * @return array the names of the global options for this command.
	 */
Qiang Xue committed
101 102 103 104
	public function globalOptions()
	{
		return array('migrationPath', 'migrationTable', 'connectionID', 'templateFile', 'interactive');
	}
Qiang Xue committed
105

Qiang Xue committed
106 107 108 109 110 111 112
	/**
	 * 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.
	 * @return boolean whether the action should continue to be executed.
	 * @throws Exception if the migration directory does not exist.
	 */
113
	public function beforeAction($action)
Qiang Xue committed
114
	{
Qiang Xue committed
115 116 117
		if (parent::beforeAction($action)) {
			$path = Yii::getAlias($this->migrationPath);
			if ($path === false || !is_dir($path)) {
Qiang Xue committed
118
				throw new Exception("The migration directory \"{$this->migrationPath}\" does not exist.");
Qiang Xue committed
119 120
			}
			$this->migrationPath = $path;
Qiang Xue committed
121

Qiang Xue committed
122
			$this->db = Yii::$app->getComponent($this->connectionID);
Qiang Xue committed
123 124 125 126
			if (!$this->db instanceof Connection) {
				throw new Exception("Invalid DB connection \"{$this->connectionID}\".");
			}

Qiang Xue committed
127
			$version = Yii::getVersion();
Qiang Xue committed
128
			echo "Yii Migration Tool (based on Yii v{$version})\n\n";
Qiang Xue committed
129 130
			return true;
		} else {
Qiang Xue committed
131
			return false;
132
		}
Qiang Xue committed
133 134
	}

135
	/**
Qiang Xue committed
136 137
	 * Upgrades the application by applying new migrations.
	 * For example,
Qiang Xue committed
138 139 140 141 142 143
	 *
	 * ~~~
	 * yiic migrate     # apply all new migrations
	 * yiic migrate 3   # apply the first 3 new migrations
	 * ~~~
	 *
Qiang Xue committed
144 145
	 * @param integer $limit the number of new migrations to be applied. If 0, it means
	 * applying all available new migrations.
146
	 */
Qiang Xue committed
147
	public function actionUp($limit = 0)
Qiang Xue committed
148
	{
Qiang Xue committed
149
		if (($migrations = $this->getNewMigrations()) === array()) {
Qiang Xue committed
150
			echo "No new migration found. Your system is up-to-date.\n";
Qiang Xue committed
151
			Yii::$app->end();
Qiang Xue committed
152 153
		}

Qiang Xue committed
154
		$total = count($migrations);
Qiang Xue committed
155 156 157
		$limit = (int)$limit;
		if ($limit > 0) {
			$migrations = array_slice($migrations, 0, $limit);
158
		}
Qiang Xue committed
159

Qiang Xue committed
160
		$n = count($migrations);
Qiang Xue committed
161
		if ($n === $total) {
Qiang Xue committed
162
			echo "Total $n new " . ($n === 1 ? 'migration' : 'migrations') . " to be applied:\n";
Qiang Xue committed
163 164 165
		} else {
			echo "Total $n out of $total new " . ($total === 1 ? 'migration' : 'migrations') . " to be applied:\n";
		}
Qiang Xue committed
166

Qiang Xue committed
167
		foreach ($migrations as $migration) {
Qiang Xue committed
168
			echo "    $migration\n";
Qiang Xue committed
169
		}
Qiang Xue committed
170 171
		echo "\n";

Qiang Xue committed
172 173
		if ($this->confirm('Apply the above ' . ($n === 1 ? 'migration' : 'migrations') . "?")) {
			foreach ($migrations as $migration) {
Qiang Xue committed
174 175
				if (!$this->migrateUp($migration)) {
					echo "\nMigration failed. The rest of the migrations are canceled.\n";
Qiang Xue committed
176 177 178 179 180 181 182
					return;
				}
			}
			echo "\nMigrated up successfully.\n";
		}
	}

Qiang Xue committed
183
	/**
Qiang Xue committed
184 185
	 * Downgrades the application by reverting old migrations.
	 * For example,
Qiang Xue committed
186 187 188 189 190 191
	 *
	 * ~~~
	 * yiic migrate/down     # revert the last migration
	 * yiic migrate/down 3   # revert the last 3 migrations
	 * ~~~
	 *
Qiang Xue committed
192 193 194
	 * @param integer $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.
Qiang Xue committed
195
	 */
Qiang Xue committed
196
	public function actionDown($limit = 1)
Qiang Xue committed
197
	{
Qiang Xue committed
198 199 200
		$limit = (int)$limit;
		if ($limit < 1) {
			throw new Exception("The step argument must be greater than 0.");
Qiang Xue committed
201
		}
Qiang Xue committed
202

Qiang Xue committed
203
		if (($migrations = $this->getMigrationHistory($limit)) === array()) {
Qiang Xue committed
204 205 206
			echo "No migration has been done before.\n";
			return;
		}
Qiang Xue committed
207
		$migrations = array_keys($migrations);
Qiang Xue committed
208

Qiang Xue committed
209 210
		$n = count($migrations);
		echo "Total $n " . ($n === 1 ? 'migration' : 'migrations') . " to be reverted:\n";
Qiang Xue committed
211
		foreach ($migrations as $migration) {
Qiang Xue committed
212
			echo "    $migration\n";
Qiang Xue committed
213
		}
Qiang Xue committed
214 215
		echo "\n";

Qiang Xue committed
216 217
		if ($this->confirm('Revert the above ' . ($n === 1 ? 'migration' : 'migrations') . "?")) {
			foreach ($migrations as $migration) {
Qiang Xue committed
218 219
				if (!$this->migrateDown($migration)) {
					echo "\nMigration failed. The rest of the migrations are canceled.\n";
Qiang Xue committed
220 221 222 223 224 225 226
					return;
				}
			}
			echo "\nMigrated down successfully.\n";
		}
	}

Qiang Xue committed
227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242
	/**
	 * Redoes the last few migrations.
	 *
	 * This command will first revert the specified migrations, and then apply
	 * them again. For example,
	 *
	 * ~~~
	 * yiic migrate/redo     # redo the last applied migration
	 * yiic migrate/redo 3   # redo the last 3 applied migrations
	 * ~~~
	 *
	 * @param integer $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.
	 */
	public function actionRedo($limit = 1)
Qiang Xue committed
243
	{
Qiang Xue committed
244 245 246
		$limit = (int)$limit;
		if ($limit < 1) {
			throw new Exception("The step argument must be greater than 0.");
Qiang Xue committed
247
		}
Qiang Xue committed
248

Qiang Xue committed
249
		if (($migrations = $this->getMigrationHistory($limit)) === array()) {
Qiang Xue committed
250 251 252
			echo "No migration has been done before.\n";
			return;
		}
Qiang Xue committed
253
		$migrations = array_keys($migrations);
Qiang Xue committed
254

Qiang Xue committed
255 256
		$n = count($migrations);
		echo "Total $n " . ($n === 1 ? 'migration' : 'migrations') . " to be redone:\n";
Qiang Xue committed
257
		foreach ($migrations as $migration) {
Qiang Xue committed
258
			echo "    $migration\n";
Qiang Xue committed
259
		}
Qiang Xue committed
260 261
		echo "\n";

Qiang Xue committed
262 263
		if ($this->confirm('Redo the above ' . ($n === 1 ? 'migration' : 'migrations') . "?")) {
			foreach ($migrations as $migration) {
Qiang Xue committed
264 265
				if (!$this->migrateDown($migration)) {
					echo "\nMigration failed. The rest of the migrations are canceled.\n";
Qiang Xue committed
266 267 268
					return;
				}
			}
Qiang Xue committed
269
			foreach (array_reverse($migrations) as $migration) {
Qiang Xue committed
270 271
				if (!$this->migrateUp($migration)) {
					echo "\nMigration failed. The rest of the migrations migrations are canceled.\n";
Qiang Xue committed
272 273 274 275 276 277 278
					return;
				}
			}
			echo "\nMigration redone successfully.\n";
		}
	}

Qiang Xue committed
279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294
	/**
	 * Upgrades or downgrades till the specified version of migration.
	 *
	 * This command will first revert the specified migrations, and then apply
	 * them again. For example,
	 *
	 * ~~~
	 * yiic migrate/to 101129_185401                      # using timestamp
	 * yiic migrate/to m101129_185401_create_user_table   # using full name
	 * ~~~
	 *
	 * @param string $version the version name that the application should be migrated to.
	 * This can be either the timestamp or the full name of the migration.
	 * @throws Exception if the version argument is invalid
	 */
	public function actionTo($version)
Qiang Xue committed
295
	{
Qiang Xue committed
296
		$originalVersion = $version;
Qiang Xue committed
297
		if (preg_match('/^m?(\d{6}_\d{6})(_.*?)?$/', $version, $matches)) {
Qiang Xue committed
298
			$version = 'm' . $matches[1];
Qiang Xue committed
299
		} else {
Qiang Xue committed
300
			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).");
Qiang Xue committed
301
		}
Qiang Xue committed
302 303

		// try migrate up
Qiang Xue committed
304 305 306
		$migrations = $this->getNewMigrations();
		foreach ($migrations as $i => $migration) {
			if (strpos($migration, $version . '_') === 0) {
Qiang Xue committed
307
				$this->actionUp($i + 1);
Qiang Xue committed
308 309 310 311 312
				return;
			}
		}

		// try migrate down
Qiang Xue committed
313 314 315
		$migrations = array_keys($this->getMigrationHistory(-1));
		foreach ($migrations as $i => $migration) {
			if (strpos($migration, $version . '_') === 0) {
Qiang Xue committed
316
				if ($i === 0) {
Qiang Xue committed
317
					echo "Already at '$originalVersion'. Nothing needs to be done.\n";
Qiang Xue committed
318
				} else {
Qiang Xue committed
319
					$this->actionDown($i);
Qiang Xue committed
320
				}
Qiang Xue committed
321 322 323 324
				return;
			}
		}

Qiang Xue committed
325
		throw new Exception("Unable to find the version '$originalVersion'.");
Qiang Xue committed
326 327
	}

Qiang Xue committed
328 329 330 331 332 333 334 335 336 337 338 339 340 341 342
	/**
	 * Modifies the migration history to the specified version.
	 *
	 * No actual migration will be performed.
	 *
	 * ~~~
	 * yiic migrate/mark 101129_185401                      # using timestamp
	 * yiic migrate/mark m101129_185401_create_user_table   # using full 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.
	 * @throws Exception if the version argument is invalid or the version cannot be found.
	 */
	public function actionMark($version)
Qiang Xue committed
343
	{
Qiang Xue committed
344
		$originalVersion = $version;
Qiang Xue committed
345
		if (preg_match('/^m?(\d{6}_\d{6})(_.*?)?$/', $version, $matches)) {
Qiang Xue committed
346
			$version = 'm' . $matches[1];
Qiang Xue committed
347
		} else {
Qiang Xue committed
348
			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).");
Qiang Xue committed
349
		}
Qiang Xue committed
350 351

		// try mark up
Qiang Xue committed
352 353 354 355
		$migrations = $this->getNewMigrations();
		foreach ($migrations as $i => $migration) {
			if (strpos($migration, $version . '_') === 0) {
				if ($this->confirm("Set migration history at $originalVersion?")) {
Qiang Xue committed
356
					$command = $this->db->createCommand();
Qiang Xue committed
357
					for ($j = 0; $j <= $i; ++$j) {
Qiang Xue committed
358
						$command->insert($this->migrationTable, array(
Qiang Xue committed
359 360
							'version' => $migrations[$j],
							'apply_time' => time(),
Qiang Xue committed
361
						))->execute();
Qiang Xue committed
362 363 364 365 366 367 368 369
					}
					echo "The migration history is set at $originalVersion.\nNo actual migration was performed.\n";
				}
				return;
			}
		}

		// try mark down
Qiang Xue committed
370 371 372
		$migrations = array_keys($this->getMigrationHistory(-1));
		foreach ($migrations as $i => $migration) {
			if (strpos($migration, $version . '_') === 0) {
Qiang Xue committed
373
				if ($i === 0) {
Qiang Xue committed
374
					echo "Already at '$originalVersion'. Nothing needs to be done.\n";
Qiang Xue committed
375
				} else {
Qiang Xue committed
376
					if ($this->confirm("Set migration history at $originalVersion?")) {
Qiang Xue committed
377
						$command = $this->db->createCommand();
Qiang Xue committed
378
						for ($j = 0; $j < $i; ++$j) {
Qiang Xue committed
379 380 381
							$command->delete($this->migrationTable, array(
								'version' => $migrations[$j],
							))->execute();
Qiang Xue committed
382
						}
Qiang Xue committed
383 384 385 386 387 388 389
						echo "The migration history is set at $originalVersion.\nNo actual migration was performed.\n";
					}
				}
				return;
			}
		}

Qiang Xue committed
390
		throw new Exception("Unable to find the version '$originalVersion'.");
Qiang Xue committed
391 392
	}

Qiang Xue committed
393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408
	/**
	 * Displays the migration history.
	 *
	 * This command will show the list of migrations that have been applied
	 * so far. For example,
	 *
	 * ~~~
	 * yiic migrate/history     # showing the last 10 migrations
	 * yiic migrate/history 5   # showing the last 5 migrations
	 * yiic migrate/history 0   # showing the whole history
	 * ~~~
	 *
	 * @param integer $limit the maximum number of migrations to be displayed.
	 * If it is 0, the whole migration history will be displayed.
	 */
	public function actionHistory($limit = 10)
Qiang Xue committed
409
	{
Qiang Xue committed
410
		$limit = (int)$limit;
Qiang Xue committed
411
		$migrations = $this->getMigrationHistory($limit);
Qiang Xue committed
412
		if ($migrations === array()) {
Qiang Xue committed
413
			echo "No migration has been done before.\n";
Qiang Xue committed
414
		} else {
Qiang Xue committed
415
			$n = count($migrations);
Qiang Xue committed
416
			if ($limit > 0) {
Qiang Xue committed
417
				echo "Showing the last $n applied " . ($n === 1 ? 'migration' : 'migrations') . ":\n";
Qiang Xue committed
418
			} else {
Qiang Xue committed
419
				echo "Total $n " . ($n === 1 ? 'migration has' : 'migrations have') . " been applied before:\n";
Qiang Xue committed
420 421
			}
			foreach ($migrations as $version => $time) {
Qiang Xue committed
422
				echo "    (" . date('Y-m-d H:i:s', $time) . ') ' . $version . "\n";
Qiang Xue committed
423
			}
Qiang Xue committed
424 425 426
		}
	}

Qiang Xue committed
427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442
	/**
	 * Displays the un-applied new migrations.
	 *
	 * This command will show the new migrations that have not been applied.
	 * For example,
	 *
	 * ~~~
	 * yiic migrate/new     # showing the first 10 new migrations
	 * yiic migrate/new 5   # showing the first 5 new migrations
	 * yiic migrate/new 0   # showing all new migrations
	 * ~~~
	 *
	 * @param integer $limit the maximum number of new migrations to be displayed.
	 * If it is 0, all available new migrations will be displayed.
	 */
	public function actionNew($limit = 10)
Qiang Xue committed
443
	{
Qiang Xue committed
444
		$limit = (int)$limit;
Qiang Xue committed
445
		$migrations = $this->getNewMigrations();
Qiang Xue committed
446
		if ($migrations === array()) {
Qiang Xue committed
447
			echo "No new migrations found. Your system is up-to-date.\n";
Qiang Xue committed
448
		} else {
Qiang Xue committed
449 450 451 452
			$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
453
			} else {
Qiang Xue committed
454
				echo "Found $n new " . ($n === 1 ? 'migration' : 'migrations') . ":\n";
Qiang Xue committed
455
			}
Qiang Xue committed
456

Qiang Xue committed
457
			foreach ($migrations as $migration) {
Qiang Xue committed
458
				echo "    " . $migration . "\n";
Qiang Xue committed
459
			}
Qiang Xue committed
460 461 462
		}
	}

Qiang Xue committed
463 464
	/**
	 * Creates a new migration.
Qiang Xue committed
465 466 467 468 469 470 471 472 473 474 475 476
	 *
	 * 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.
	 *
	 * ~~~
	 * yiic migrate/create create_user_table
	 * ~~~
	 *
	 * @param string $name the name of the new migration. This should only contain
	 * letters, digits and/or underscores.
	 * @throws Exception if the name argument is invalid.
Qiang Xue committed
477
	 */
Qiang Xue committed
478
	public function actionCreate($name)
Qiang Xue committed
479
	{
Qiang Xue committed
480
		if (!preg_match('/^\w+$/', $name)) {
Qiang Xue committed
481
			throw new Exception("The migration name should contain letters, digits and/or underscore characters only.");
Qiang Xue committed
482
		}
Qiang Xue committed
483

Qiang Xue committed
484 485
		$name = 'm' . gmdate('ymd_His') . '_' . $name;
		$file = $this->migrationPath . DIRECTORY_SEPARATOR . $name . '.php';
Qiang Xue committed
486

Qiang Xue committed
487
		if ($this->confirm("Create new migration '$file'?")) {
Qiang Xue committed
488 489 490
			$content = $this->renderFile(Yii::getAlias($this->templateFile), array(
				'className' => $name,
			));
Qiang Xue committed
491 492 493 494 495
			file_put_contents($file, $content);
			echo "New migration created successfully.\n";
		}
	}

Qiang Xue committed
496 497 498 499 500
	/**
	 * Upgrades with the specified migration class.
	 * @param string $class the migration class name
	 * @return boolean whether the migration is successful
	 */
Qiang Xue committed
501 502
	protected function migrateUp($class)
	{
Qiang Xue committed
503
		if ($class === self::BASE_MIGRATION) {
Qiang Xue committed
504
			return true;
Qiang Xue committed
505
		}
Qiang Xue committed
506 507

		echo "*** applying $class\n";
Qiang Xue committed
508
		$start = microtime(true);
Qiang Xue committed
509
		$migration = $this->createMigration($class);
Qiang Xue committed
510
		if ($migration->up() !== false) {
Qiang Xue committed
511
			$this->db->createCommand()->insert($this->migrationTable, array(
Qiang Xue committed
512 513
				'version' => $class,
				'apply_time' => time(),
Qiang Xue committed
514
			))->execute();
Qiang Xue committed
515 516
			$time = microtime(true) - $start;
			echo "*** applied $class (time: " . sprintf("%.3f", $time) . "s)\n\n";
Qiang Xue committed
517
			return true;
Qiang Xue committed
518 519 520
		} else {
			$time = microtime(true) - $start;
			echo "*** failed to apply $class (time: " . sprintf("%.3f", $time) . "s)\n\n";
Qiang Xue committed
521 522 523 524
			return false;
		}
	}

Qiang Xue committed
525 526 527 528 529
	/**
	 * Downgrades with the specified migration class.
	 * @param string $class the migration class name
	 * @return boolean whether the migration is successful
	 */
Qiang Xue committed
530 531
	protected function migrateDown($class)
	{
Qiang Xue committed
532
		if ($class === self::BASE_MIGRATION) {
Qiang Xue committed
533
			return true;
Qiang Xue committed
534
		}
Qiang Xue committed
535 536

		echo "*** reverting $class\n";
Qiang Xue committed
537
		$start = microtime(true);
Qiang Xue committed
538
		$migration = $this->createMigration($class);
Qiang Xue committed
539
		if ($migration->down() !== false) {
Qiang Xue committed
540 541 542
			$this->db->createCommand()->delete($this->migrationTable, array(
				'version' => $class,
			))->execute();
Qiang Xue committed
543 544
			$time = microtime(true) - $start;
			echo "*** reverted $class (time: " . sprintf("%.3f", $time) . "s)\n\n";
Qiang Xue committed
545
			return true;
Qiang Xue committed
546 547 548
		} else {
			$time = microtime(true) - $start;
			echo "*** failed to revert $class (time: " . sprintf("%.3f", $time) . "s)\n\n";
Qiang Xue committed
549 550 551 552
			return false;
		}
	}

Qiang Xue committed
553 554 555 556 557 558
	/**
	 * Creates a new migration instance.
	 * @param string $class the migration class name
	 * @return \yii\db\Migration the migration instance
	 */
	protected function createMigration($class)
Qiang Xue committed
559
	{
Qiang Xue committed
560
		$file = $this->migrationPath . DIRECTORY_SEPARATOR . $class . '.php';
Qiang Xue committed
561
		require_once($file);
Qiang Xue committed
562 563 564
		return new $class(array(
			'db' => $this->db,
		));
Qiang Xue committed
565 566
	}

Qiang Xue committed
567

Qiang Xue committed
568
	/**
Qiang Xue committed
569 570
	 * @return Connection the database connection that is used to store the migration history.
	 * @throws Exception if the database connection ID is invalid.
Qiang Xue committed
571
	 */
Qiang Xue committed
572
	protected function getDb()
Qiang Xue committed
573
	{
Qiang Xue committed
574 575
		if ($this->db !== null) {
			return $this->db;
Qiang Xue committed
576
		} else {
Qiang Xue committed
577
			$this->db = Yii::$app->getComponent($this->connectionID);
Qiang Xue committed
578 579
			if ($this->db instanceof Connection) {
				return $this->db;
Qiang Xue committed
580
			} else {
Qiang Xue committed
581
				throw new Exception("Invalid DB connection: {$this->connectionID}.");
Qiang Xue committed
582 583
			}
		}
Qiang Xue committed
584 585
	}

Qiang Xue committed
586 587 588 589 590
	/**
	 * Returns the migration history.
	 * @param integer $limit the maximum number of records in the history to be returned
	 * @return array the migration history
	 */
Qiang Xue committed
591 592
	protected function getMigrationHistory($limit)
	{
Qiang Xue committed
593
		if ($this->db->schema->getTableSchema($this->migrationTable) === null) {
Qiang Xue committed
594 595
			$this->createMigrationHistoryTable();
		}
Qiang Xue committed
596 597
		$query = new Query;
		$rows = $query->select(array('version', 'apply_time'))
Qiang Xue committed
598
			->from($this->migrationTable)
Qiang Xue committed
599
			->orderBy('version DESC')
Qiang Xue committed
600
			->limit($limit)
Qiang Xue committed
601 602 603 604 605
			->createCommand()
			->queryAll();
		$history = ArrayHelper::map($rows, 'version', 'apply_time');
		unset($history[self::BASE_MIGRATION]);
		return $history;
Qiang Xue committed
606 607
	}

Qiang Xue committed
608 609 610
	/**
	 * Creates the migration history table.
	 */
Qiang Xue committed
611 612
	protected function createMigrationHistoryTable()
	{
Qiang Xue committed
613
		echo 'Creating migration history table "' . $this->migrationTable . '"...';
Qiang Xue committed
614 615
		$this->db->createCommand()->createTable($this->migrationTable, array(
			'version' => 'varchar(255) NOT NULL PRIMARY KEY',
Qiang Xue committed
616
			'apply_time' => 'integer',
Qiang Xue committed
617 618
		))->execute();
		$this->db->createCommand()->insert($this->migrationTable, array(
Qiang Xue committed
619 620
			'version' => self::BASE_MIGRATION,
			'apply_time' => time(),
Qiang Xue committed
621
		))->execute();
Qiang Xue committed
622 623 624
		echo "done.\n";
	}

Qiang Xue committed
625 626 627 628
	/**
	 * Returns the migrations that are not applied.
	 * @return array list of new migrations
	 */
Qiang Xue committed
629 630
	protected function getNewMigrations()
	{
Qiang Xue committed
631
		$applied = array();
Qiang Xue committed
632
		foreach ($this->getMigrationHistory(-1) as $version => $time) {
Qiang Xue committed
633
			$applied[substr($version, 1, 13)] = true;
Qiang Xue committed
634
		}
Qiang Xue committed
635 636 637 638

		$migrations = array();
		$handle = opendir($this->migrationPath);
		while (($file = readdir($handle)) !== false) {
Qiang Xue committed
639
			if ($file === '.' || $file === '..') {
Qiang Xue committed
640
				continue;
Qiang Xue committed
641
			}
Qiang Xue committed
642
			$path = $this->migrationPath . DIRECTORY_SEPARATOR . $file;
Qiang Xue committed
643
			if (preg_match('/^(m(\d{6}_\d{6})_.*?)\.php$/', $file, $matches) && is_file($path) && !isset($applied[$matches[2]])) {
Qiang Xue committed
644
				$migrations[] = $matches[1];
Qiang Xue committed
645
			}
Qiang Xue committed
646 647 648 649 650 651
		}
		closedir($handle);
		sort($migrations);
		return $migrations;
	}
}