Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

650 lines
23KB

  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\console\Exception;
  10. use yii\console\Controller;
  11. use yii\helpers\Console;
  12. use yii\helpers\FileHelper;
  13. /**
  14. * BaseMigrateController is base class for migrate controllers.
  15. *
  16. * @author Qiang Xue <qiang.xue@gmail.com>
  17. * @since 2.0
  18. */
  19. abstract class BaseMigrateController extends Controller
  20. {
  21. /**
  22. * The name of the dummy migration that marks the beginning of the whole migration history.
  23. */
  24. const BASE_MIGRATION = 'm000000_000000_base';
  25. /**
  26. * @var string the default command action.
  27. */
  28. public $defaultAction = 'up';
  29. /**
  30. * @var string the directory storing the migration classes. This can be either
  31. * a path alias or a directory.
  32. */
  33. public $migrationPath = '@app/migrations';
  34. /**
  35. * @var string the template file for generating new migrations.
  36. * This can be either a path alias (e.g. "@app/migrations/template.php")
  37. * or a file path.
  38. */
  39. public $templateFile;
  40. /**
  41. * @inheritdoc
  42. */
  43. public function options($actionID)
  44. {
  45. return array_merge(
  46. parent::options($actionID),
  47. ['migrationPath'], // global for all actions
  48. ($actionID == 'create') ? ['templateFile'] : [] // action create
  49. );
  50. }
  51. /**
  52. * This method is invoked right before an action is to be executed (after all possible filters.)
  53. * It checks the existence of the [[migrationPath]].
  54. * @param \yii\base\Action $action the action to be executed.
  55. * @throws Exception if directory specified in migrationPath doesn't exist and action isn't "create".
  56. * @return boolean whether the action should continue to be executed.
  57. */
  58. public function beforeAction($action)
  59. {
  60. if (parent::beforeAction($action)) {
  61. $path = Yii::getAlias($this->migrationPath);
  62. if (!is_dir($path)) {
  63. if ($action->id !== 'create') {
  64. throw new Exception('Migration failed. Directory specified in migrationPath doesn\'t exist.');
  65. }
  66. FileHelper::createDirectory($path);
  67. }
  68. $this->migrationPath = $path;
  69. $version = Yii::getVersion();
  70. $this->stdout("Yii Migration Tool (based on Yii v{$version})\n\n");
  71. return true;
  72. } else {
  73. return false;
  74. }
  75. }
  76. /**
  77. * Upgrades the application by applying new migrations.
  78. * For example,
  79. *
  80. * ~~~
  81. * yii migrate # apply all new migrations
  82. * yii migrate 3 # apply the first 3 new migrations
  83. * ~~~
  84. *
  85. * @param integer $limit the number of new migrations to be applied. If 0, it means
  86. * applying all available new migrations.
  87. *
  88. * @return integer the status of the action execution. 0 means normal, other values mean abnormal.
  89. */
  90. public function actionUp($limit = 0)
  91. {
  92. $migrations = $this->getNewMigrations();
  93. if (empty($migrations)) {
  94. $this->stdout("No new migration found. Your system is up-to-date.\n", Console::FG_GREEN);
  95. return self::EXIT_CODE_NORMAL;
  96. }
  97. $total = count($migrations);
  98. $limit = (int) $limit;
  99. if ($limit > 0) {
  100. $migrations = array_slice($migrations, 0, $limit);
  101. }
  102. $n = count($migrations);
  103. if ($n === $total) {
  104. $this->stdout("Total $n new " . ($n === 1 ? 'migration' : 'migrations') . " to be applied:\n", Console::FG_YELLOW);
  105. } else {
  106. $this->stdout("Total $n out of $total new " . ($total === 1 ? 'migration' : 'migrations') . " to be applied:\n", Console::FG_YELLOW);
  107. }
  108. foreach ($migrations as $migration) {
  109. $this->stdout("\t$migration\n");
  110. }
  111. $this->stdout("\n");
  112. if ($this->confirm('Apply the above ' . ($n === 1 ? 'migration' : 'migrations') . "?")) {
  113. foreach ($migrations as $migration) {
  114. if (!$this->migrateUp($migration)) {
  115. $this->stdout("\nMigration failed. The rest of the migrations are canceled.\n", Console::FG_RED);
  116. return self::EXIT_CODE_ERROR;
  117. }
  118. }
  119. $this->stdout("\nMigrated up successfully.\n", Console::FG_GREEN);
  120. }
  121. }
  122. /**
  123. * Downgrades the application by reverting old migrations.
  124. * For example,
  125. *
  126. * ~~~
  127. * yii migrate/down # revert the last migration
  128. * yii migrate/down 3 # revert the last 3 migrations
  129. * yii migrate/down all # revert all migrations
  130. * ~~~
  131. *
  132. * @param integer $limit the number of migrations to be reverted. Defaults to 1,
  133. * meaning the last applied migration will be reverted.
  134. * @throws Exception if the number of the steps specified is less than 1.
  135. *
  136. * @return integer the status of the action execution. 0 means normal, other values mean abnormal.
  137. */
  138. public function actionDown($limit = 1)
  139. {
  140. if ($limit === 'all') {
  141. $limit = null;
  142. } else {
  143. $limit = (int) $limit;
  144. if ($limit < 1) {
  145. throw new Exception("The step argument must be greater than 0.");
  146. }
  147. }
  148. $migrations = $this->getMigrationHistory($limit);
  149. if (empty($migrations)) {
  150. $this->stdout("No migration has been done before.\n", Console::FG_YELLOW);
  151. return self::EXIT_CODE_NORMAL;
  152. }
  153. $migrations = array_keys($migrations);
  154. $n = count($migrations);
  155. $this->stdout("Total $n " . ($n === 1 ? 'migration' : 'migrations') . " to be reverted:\n", Console::FG_YELLOW);
  156. foreach ($migrations as $migration) {
  157. $this->stdout("\t$migration\n");
  158. }
  159. $this->stdout("\n");
  160. if ($this->confirm('Revert the above ' . ($n === 1 ? 'migration' : 'migrations') . "?")) {
  161. foreach ($migrations as $migration) {
  162. if (!$this->migrateDown($migration)) {
  163. $this->stdout("\nMigration failed. The rest of the migrations are canceled.\n", Console::FG_RED);
  164. return self::EXIT_CODE_ERROR;
  165. }
  166. }
  167. $this->stdout("\nMigrated down successfully.\n", Console::FG_GREEN);
  168. }
  169. }
  170. /**
  171. * Redoes the last few migrations.
  172. *
  173. * This command will first revert the specified migrations, and then apply
  174. * them again. For example,
  175. *
  176. * ~~~
  177. * yii migrate/redo # redo the last applied migration
  178. * yii migrate/redo 3 # redo the last 3 applied migrations
  179. * yii migrate/redo all # redo all migrations
  180. * ~~~
  181. *
  182. * @param integer $limit the number of migrations to be redone. Defaults to 1,
  183. * meaning the last applied migration will be redone.
  184. * @throws Exception if the number of the steps specified is less than 1.
  185. *
  186. * @return integer the status of the action execution. 0 means normal, other values mean abnormal.
  187. */
  188. public function actionRedo($limit = 1)
  189. {
  190. if ($limit === 'all') {
  191. $limit = null;
  192. } else {
  193. $limit = (int) $limit;
  194. if ($limit < 1) {
  195. throw new Exception("The step argument must be greater than 0.");
  196. }
  197. }
  198. $migrations = $this->getMigrationHistory($limit);
  199. if (empty($migrations)) {
  200. $this->stdout("No migration has been done before.\n", Console::FG_YELLOW);
  201. return self::EXIT_CODE_NORMAL;
  202. }
  203. $migrations = array_keys($migrations);
  204. $n = count($migrations);
  205. $this->stdout("Total $n " . ($n === 1 ? 'migration' : 'migrations') . " to be redone:\n", Console::FG_YELLOW);
  206. foreach ($migrations as $migration) {
  207. $this->stdout("\t$migration\n");
  208. }
  209. $this->stdout("\n");
  210. if ($this->confirm('Redo the above ' . ($n === 1 ? 'migration' : 'migrations') . "?")) {
  211. foreach ($migrations as $migration) {
  212. if (!$this->migrateDown($migration)) {
  213. $this->stdout("\nMigration failed. The rest of the migrations are canceled.\n", Console::FG_RED);
  214. return self::EXIT_CODE_ERROR;
  215. }
  216. }
  217. foreach (array_reverse($migrations) as $migration) {
  218. if (!$this->migrateUp($migration)) {
  219. $this->stdout("\nMigration failed. The rest of the migrations migrations are canceled.\n", Console::FG_RED);
  220. return self::EXIT_CODE_ERROR;
  221. }
  222. }
  223. $this->stdout("\nMigration redone successfully.\n", Console::FG_GREEN);
  224. }
  225. }
  226. /**
  227. * Upgrades or downgrades till the specified version.
  228. *
  229. * Can also downgrade versions to the certain apply time in the past by providing
  230. * a UNIX timestamp or a string parseable by the strtotime() function. This means
  231. * that all the versions applied after the specified certain time would be reverted.
  232. *
  233. * This command will first revert the specified migrations, and then apply
  234. * them again. For example,
  235. *
  236. * ~~~
  237. * yii migrate/to 101129_185401 # using timestamp
  238. * yii migrate/to m101129_185401_create_user_table # using full name
  239. * yii migrate/to 1392853618 # using UNIX timestamp
  240. * yii migrate/to "2014-02-15 13:00:50" # using strtotime() parseable string
  241. * ~~~
  242. *
  243. * @param string $version either the version name or the certain time value in the past
  244. * that the application should be migrated to. This can be either the timestamp,
  245. * the full name of the migration, the UNIX timestamp, or the parseable datetime
  246. * string.
  247. * @throws Exception if the version argument is invalid.
  248. */
  249. public function actionTo($version)
  250. {
  251. if (preg_match('/^m?(\d{6}_\d{6})(_.*?)?$/', $version, $matches)) {
  252. $this->migrateToVersion('m' . $matches[1]);
  253. } elseif ((string) (int) $version == $version) {
  254. $this->migrateToTime($version);
  255. } elseif (($time = strtotime($version)) !== false) {
  256. $this->migrateToTime($time);
  257. } else {
  258. 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 a UNIX timestamp (e.g. 1392853000), or a datetime string parseable\nby the strtotime() function (e.g. 2014-02-15 13:00:50).");
  259. }
  260. }
  261. /**
  262. * Modifies the migration history to the specified version.
  263. *
  264. * No actual migration will be performed.
  265. *
  266. * ~~~
  267. * yii migrate/mark 101129_185401 # using timestamp
  268. * yii migrate/mark m101129_185401_create_user_table # using full name
  269. * ~~~
  270. *
  271. * @param string $version the version at which the migration history should be marked.
  272. * This can be either the timestamp or the full name of the migration.
  273. * @return integer CLI exit code
  274. * @throws Exception if the version argument is invalid or the version cannot be found.
  275. */
  276. public function actionMark($version)
  277. {
  278. $originalVersion = $version;
  279. if (preg_match('/^m?(\d{6}_\d{6})(_.*?)?$/', $version, $matches)) {
  280. $version = 'm' . $matches[1];
  281. } else {
  282. 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).");
  283. }
  284. // try mark up
  285. $migrations = $this->getNewMigrations();
  286. foreach ($migrations as $i => $migration) {
  287. if (strpos($migration, $version . '_') === 0) {
  288. if ($this->confirm("Set migration history at $originalVersion?")) {
  289. for ($j = 0; $j <= $i; ++$j) {
  290. $this->addMigrationHistory($migrations[$j]);
  291. }
  292. $this->stdout("The migration history is set at $originalVersion.\nNo actual migration was performed.\n", Console::FG_GREEN);
  293. }
  294. return self::EXIT_CODE_NORMAL;
  295. }
  296. }
  297. // try mark down
  298. $migrations = array_keys($this->getMigrationHistory(null));
  299. foreach ($migrations as $i => $migration) {
  300. if (strpos($migration, $version . '_') === 0) {
  301. if ($i === 0) {
  302. $this->stdout("Already at '$originalVersion'. Nothing needs to be done.\n", Console::FG_YELLOW);
  303. } else {
  304. if ($this->confirm("Set migration history at $originalVersion?")) {
  305. for ($j = 0; $j < $i; ++$j) {
  306. $this->removeMigrationHistory($migrations[$j]);
  307. }
  308. $this->stdout("The migration history is set at $originalVersion.\nNo actual migration was performed.\n", Console::FG_GREEN);
  309. }
  310. }
  311. return self::EXIT_CODE_NORMAL;
  312. }
  313. }
  314. throw new Exception("Unable to find the version '$originalVersion'.");
  315. }
  316. /**
  317. * Displays the migration history.
  318. *
  319. * This command will show the list of migrations that have been applied
  320. * so far. For example,
  321. *
  322. * ~~~
  323. * yii migrate/history # showing the last 10 migrations
  324. * yii migrate/history 5 # showing the last 5 migrations
  325. * yii migrate/history all # showing the whole history
  326. * ~~~
  327. *
  328. * @param integer $limit the maximum number of migrations to be displayed.
  329. * If it is 0, the whole migration history will be displayed.
  330. * @throws \yii\console\Exception if invalid limit value passed
  331. */
  332. public function actionHistory($limit = 10)
  333. {
  334. if ($limit === 'all') {
  335. $limit = null;
  336. } else {
  337. $limit = (int) $limit;
  338. if ($limit < 1) {
  339. throw new Exception("The limit must be greater than 0.");
  340. }
  341. }
  342. $migrations = $this->getMigrationHistory($limit);
  343. if (empty($migrations)) {
  344. $this->stdout("No migration has been done before.\n", Console::FG_YELLOW);
  345. } else {
  346. $n = count($migrations);
  347. if ($limit > 0) {
  348. $this->stdout("Showing the last $n applied " . ($n === 1 ? 'migration' : 'migrations') . ":\n", Console::FG_YELLOW);
  349. } else {
  350. $this->stdout("Total $n " . ($n === 1 ? 'migration has' : 'migrations have') . " been applied before:\n", Console::FG_YELLOW);
  351. }
  352. foreach ($migrations as $version => $time) {
  353. $this->stdout("\t(" . date('Y-m-d H:i:s', $time) . ') ' . $version . "\n");
  354. }
  355. }
  356. }
  357. /**
  358. * Displays the un-applied new migrations.
  359. *
  360. * This command will show the new migrations that have not been applied.
  361. * For example,
  362. *
  363. * ~~~
  364. * yii migrate/new # showing the first 10 new migrations
  365. * yii migrate/new 5 # showing the first 5 new migrations
  366. * yii migrate/new all # showing all new migrations
  367. * ~~~
  368. *
  369. * @param integer $limit the maximum number of new migrations to be displayed.
  370. * If it is `all`, all available new migrations will be displayed.
  371. * @throws \yii\console\Exception if invalid limit value passed
  372. */
  373. public function actionNew($limit = 10)
  374. {
  375. if ($limit === 'all') {
  376. $limit = null;
  377. } else {
  378. $limit = (int) $limit;
  379. if ($limit < 1) {
  380. throw new Exception("The limit must be greater than 0.");
  381. }
  382. }
  383. $migrations = $this->getNewMigrations();
  384. if (empty($migrations)) {
  385. $this->stdout("No new migrations found. Your system is up-to-date.\n", Console::FG_GREEN);
  386. } else {
  387. $n = count($migrations);
  388. if ($limit && $n > $limit) {
  389. $migrations = array_slice($migrations, 0, $limit);
  390. $this->stdout("Showing $limit out of $n new " . ($n === 1 ? 'migration' : 'migrations') . ":\n", Console::FG_YELLOW);
  391. } else {
  392. $this->stdout("Found $n new " . ($n === 1 ? 'migration' : 'migrations') . ":\n", Console::FG_YELLOW);
  393. }
  394. foreach ($migrations as $migration) {
  395. $this->stdout("\t" . $migration . "\n");
  396. }
  397. }
  398. }
  399. /**
  400. * Creates a new migration.
  401. *
  402. * This command creates a new migration using the available migration template.
  403. * After using this command, developers should modify the created migration
  404. * skeleton by filling up the actual migration logic.
  405. *
  406. * ~~~
  407. * yii migrate/create create_user_table
  408. * ~~~
  409. *
  410. * @param string $name the name of the new migration. This should only contain
  411. * letters, digits and/or underscores.
  412. * @throws Exception if the name argument is invalid.
  413. */
  414. public function actionCreate($name)
  415. {
  416. if (!preg_match('/^\w+$/', $name)) {
  417. throw new Exception("The migration name should contain letters, digits and/or underscore characters only.");
  418. }
  419. $name = 'm' . gmdate('ymd_His') . '_' . $name;
  420. $file = $this->migrationPath . DIRECTORY_SEPARATOR . $name . '.php';
  421. if ($this->confirm("Create new migration '$file'?")) {
  422. $content = $this->renderFile(Yii::getAlias($this->templateFile), ['className' => $name]);
  423. file_put_contents($file, $content);
  424. $this->stdout("New migration created successfully.\n", Console::FG_GREEN);
  425. }
  426. }
  427. /**
  428. * Upgrades with the specified migration class.
  429. * @param string $class the migration class name
  430. * @return boolean whether the migration is successful
  431. */
  432. protected function migrateUp($class)
  433. {
  434. if ($class === self::BASE_MIGRATION) {
  435. return true;
  436. }
  437. $this->stdout("*** applying $class\n", Console::FG_YELLOW);
  438. $start = microtime(true);
  439. $migration = $this->createMigration($class);
  440. if ($migration->up() !== false) {
  441. $this->addMigrationHistory($class);
  442. $time = microtime(true) - $start;
  443. $this->stdout("*** applied $class (time: " . sprintf("%.3f", $time) . "s)\n\n", Console::FG_GREEN);
  444. return true;
  445. } else {
  446. $time = microtime(true) - $start;
  447. $this->stdout("*** failed to apply $class (time: " . sprintf("%.3f", $time) . "s)\n\n", Console::FG_RED);
  448. return false;
  449. }
  450. }
  451. /**
  452. * Downgrades with the specified migration class.
  453. * @param string $class the migration class name
  454. * @return boolean whether the migration is successful
  455. */
  456. protected function migrateDown($class)
  457. {
  458. if ($class === self::BASE_MIGRATION) {
  459. return true;
  460. }
  461. $this->stdout("*** reverting $class\n", Console::FG_YELLOW);
  462. $start = microtime(true);
  463. $migration = $this->createMigration($class);
  464. if ($migration->down() !== false) {
  465. $this->removeMigrationHistory($class);
  466. $time = microtime(true) - $start;
  467. $this->stdout("*** reverted $class (time: " . sprintf("%.3f", $time) . "s)\n\n", Console::FG_GREEN);
  468. return true;
  469. } else {
  470. $time = microtime(true) - $start;
  471. $this->stdout("*** failed to revert $class (time: " . sprintf("%.3f", $time) . "s)\n\n", Console::FG_RED);
  472. return false;
  473. }
  474. }
  475. /**
  476. * Creates a new migration instance.
  477. * @param string $class the migration class name
  478. * @return \yii\db\MigrationInterface the migration instance
  479. */
  480. protected function createMigration($class)
  481. {
  482. $file = $this->migrationPath . DIRECTORY_SEPARATOR . $class . '.php';
  483. require_once($file);
  484. return new $class();
  485. }
  486. /**
  487. * Migrates to the specified apply time in the past.
  488. * @param integer $time UNIX timestamp value.
  489. */
  490. protected function migrateToTime($time)
  491. {
  492. $count = 0;
  493. $migrations = array_values($this->getMigrationHistory(null));
  494. while ($count < count($migrations) && $migrations[$count] > $time) {
  495. ++$count;
  496. }
  497. if ($count === 0) {
  498. $this->stdout("Nothing needs to be done.\n", Console::FG_GREEN);
  499. } else {
  500. $this->actionDown($count);
  501. }
  502. }
  503. /**
  504. * Migrates to the certain version.
  505. * @param string $version name in the full format.
  506. * @return integer CLI exit code
  507. * @throws Exception if the provided version cannot be found.
  508. */
  509. protected function migrateToVersion($version)
  510. {
  511. $originalVersion = $version;
  512. // try migrate up
  513. $migrations = $this->getNewMigrations();
  514. foreach ($migrations as $i => $migration) {
  515. if (strpos($migration, $version . '_') === 0) {
  516. $this->actionUp($i + 1);
  517. return self::EXIT_CODE_NORMAL;
  518. }
  519. }
  520. // try migrate down
  521. $migrations = array_keys($this->getMigrationHistory(null));
  522. foreach ($migrations as $i => $migration) {
  523. if (strpos($migration, $version . '_') === 0) {
  524. if ($i === 0) {
  525. $this->stdout("Already at '$originalVersion'. Nothing needs to be done.\n", Console::FG_YELLOW);
  526. } else {
  527. $this->actionDown($i);
  528. }
  529. return self::EXIT_CODE_NORMAL;
  530. }
  531. }
  532. throw new Exception("Unable to find the version '$originalVersion'.");
  533. }
  534. /**
  535. * Returns the migrations that are not applied.
  536. * @return array list of new migrations
  537. */
  538. protected function getNewMigrations()
  539. {
  540. $applied = [];
  541. foreach ($this->getMigrationHistory(null) as $version => $time) {
  542. $applied[substr($version, 1, 13)] = true;
  543. }
  544. $migrations = [];
  545. $handle = opendir($this->migrationPath);
  546. while (($file = readdir($handle)) !== false) {
  547. if ($file === '.' || $file === '..') {
  548. continue;
  549. }
  550. $path = $this->migrationPath . DIRECTORY_SEPARATOR . $file;
  551. if (preg_match('/^(m(\d{6}_\d{6})_.*?)\.php$/', $file, $matches) && !isset($applied[$matches[2]]) && is_file($path)) {
  552. $migrations[] = $matches[1];
  553. }
  554. }
  555. closedir($handle);
  556. sort($migrations);
  557. return $migrations;
  558. }
  559. /**
  560. * Returns the migration history.
  561. * @param integer $limit the maximum number of records in the history to be returned. `null` for "no limit".
  562. * @return array the migration history
  563. */
  564. abstract protected function getMigrationHistory($limit);
  565. /**
  566. * Adds new migration entry to the history.
  567. * @param string $version migration version name.
  568. */
  569. abstract protected function addMigrationHistory($version);
  570. /**
  571. * Removes existing migration from the history.
  572. * @param string $version migration version name.
  573. */
  574. abstract protected function removeMigrationHistory($version);
  575. }