MigrateController.php 19.4 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;
Qiang Xue committed
14
use yii\console\Exception;
15
use yii\console\Controller;
Qiang Xue committed
16
use yii\db\Connection;
Qiang Xue committed
17 18
use yii\db\Query;
use yii\util\ArrayHelper;
19

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Qiang Xue committed
229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244
	/**
	 * 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
245
	{
Qiang Xue committed
246 247 248
		$limit = (int)$limit;
		if ($limit < 1) {
			throw new Exception("The step argument must be greater than 0.");
Qiang Xue committed
249
		}
Qiang Xue committed
250

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

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

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

Qiang Xue committed
281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296
	/**
	 * 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
297
	{
Qiang Xue committed
298
		$originalVersion = $version;
Qiang Xue committed
299
		if (preg_match('/^m?(\d{6}_\d{6})(_.*?)?$/', $version, $matches)) {
Qiang Xue committed
300
			$version = 'm' . $matches[1];
Qiang Xue committed
301
		} else {
Qiang Xue committed
302
			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
303
		}
Qiang Xue committed
304 305

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

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

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

Qiang Xue committed
330 331 332 333 334 335 336 337 338 339 340 341 342 343 344
	/**
	 * 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
345
	{
Qiang Xue committed
346
		$originalVersion = $version;
Qiang Xue committed
347
		if (preg_match('/^m?(\d{6}_\d{6})(_.*?)?$/', $version, $matches)) {
Qiang Xue committed
348
			$version = 'm' . $matches[1];
Qiang Xue committed
349
		} else {
Qiang Xue committed
350
			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
351
		}
Qiang Xue committed
352 353

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

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

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

Qiang Xue committed
395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410
	/**
	 * 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
411
	{
Qiang Xue committed
412
		$limit = (int)$limit;
Qiang Xue committed
413
		$migrations = $this->getMigrationHistory($limit);
Qiang Xue committed
414
		if ($migrations === array()) {
Qiang Xue committed
415
			echo "No migration has been done before.\n";
Qiang Xue committed
416
		} else {
Qiang Xue committed
417
			$n = count($migrations);
Qiang Xue committed
418
			if ($limit > 0) {
Qiang Xue committed
419
				echo "Showing the last $n applied " . ($n === 1 ? 'migration' : 'migrations') . ":\n";
Qiang Xue committed
420
			} else {
Qiang Xue committed
421
				echo "Total $n " . ($n === 1 ? 'migration has' : 'migrations have') . " been applied before:\n";
Qiang Xue committed
422 423
			}
			foreach ($migrations as $version => $time) {
Qiang Xue committed
424
				echo "    (" . date('Y-m-d H:i:s', $time) . ') ' . $version . "\n";
Qiang Xue committed
425
			}
Qiang Xue committed
426 427 428
		}
	}

Qiang Xue committed
429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444
	/**
	 * 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
445
	{
Qiang Xue committed
446
		$limit = (int)$limit;
Qiang Xue committed
447
		$migrations = $this->getNewMigrations();
Qiang Xue committed
448
		if ($migrations === array()) {
Qiang Xue committed
449
			echo "No new migrations found. Your system is up-to-date.\n";
Qiang Xue committed
450
		} else {
Qiang Xue committed
451 452 453 454
			$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
455
			} else {
Qiang Xue committed
456
				echo "Found $n new " . ($n === 1 ? 'migration' : 'migrations') . ":\n";
Qiang Xue committed
457
			}
Qiang Xue committed
458

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

Qiang Xue committed
465 466
	/**
	 * Creates a new migration.
Qiang Xue committed
467 468 469 470 471 472 473 474 475 476 477 478
	 *
	 * 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
479
	 */
Qiang Xue committed
480
	public function actionCreate($name)
Qiang Xue committed
481
	{
Qiang Xue committed
482
		if (!preg_match('/^\w+$/', $name)) {
Qiang Xue committed
483
			throw new Exception("The migration name should contain letters, digits and/or underscore characters only.");
Qiang Xue committed
484
		}
Qiang Xue committed
485

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

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

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

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

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

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

Qiang Xue committed
555 556 557 558 559 560
	/**
	 * 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
561
	{
Qiang Xue committed
562
		$file = $this->migrationPath . DIRECTORY_SEPARATOR . $class . '.php';
Qiang Xue committed
563
		require_once($file);
Qiang Xue committed
564 565 566
		return new $class(array(
			'db' => $this->db,
		));
Qiang Xue committed
567 568
	}

Qiang Xue committed
569

Qiang Xue committed
570
	/**
Qiang Xue committed
571 572
	 * @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
573
	 */
Qiang Xue committed
574
	protected function getDb()
Qiang Xue committed
575
	{
Qiang Xue committed
576 577
		if ($this->db !== null) {
			return $this->db;
Qiang Xue committed
578
		} else {
Qiang Xue committed
579
			$this->db = Yii::$app->getComponent($this->connectionID);
Qiang Xue committed
580 581
			if ($this->db instanceof Connection) {
				return $this->db;
Qiang Xue committed
582
			} else {
Qiang Xue committed
583
				throw new Exception("Invalid DB connection: {$this->connectionID}.");
Qiang Xue committed
584 585
			}
		}
Qiang Xue committed
586 587
	}

Qiang Xue committed
588 589 590 591 592
	/**
	 * 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
593 594
	protected function getMigrationHistory($limit)
	{
Qiang Xue committed
595
		if ($this->db->schema->getTableSchema($this->migrationTable) === null) {
Qiang Xue committed
596 597
			$this->createMigrationHistoryTable();
		}
Qiang Xue committed
598 599
		$query = new Query;
		$rows = $query->select(array('version', 'apply_time'))
Qiang Xue committed
600
			->from($this->migrationTable)
Qiang Xue committed
601
			->orderBy('version DESC')
Qiang Xue committed
602
			->limit($limit)
Qiang Xue committed
603 604 605 606 607
			->createCommand()
			->queryAll();
		$history = ArrayHelper::map($rows, 'version', 'apply_time');
		unset($history[self::BASE_MIGRATION]);
		return $history;
Qiang Xue committed
608 609
	}

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

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

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