BaseMigrateController.php 37 KB

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