You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

847 lines
31KB

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