MigrateController.php 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  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\mongodb\console\controllers;
  8. use Yii;
  9. use yii\console\controllers\BaseMigrateController;
  10. use yii\console\Exception;
  11. use yii\mongodb\Connection;
  12. use yii\mongodb\Query;
  13. use yii\helpers\ArrayHelper;
  14. /**
  15. * Manages application MongoDB migrations.
  16. *
  17. * This is an analog of [[\yii\console\controllers\MigrateController]] for MongoDB.
  18. *
  19. * This command provides support for tracking the migration history, upgrading
  20. * or downloading with migrations, and creating new migration skeletons.
  21. *
  22. * The migration history is stored in a MongoDB collection named
  23. * as [[migrationCollection]]. This collection will be automatically created the first time
  24. * this command is executed, if it does not exist.
  25. *
  26. * In order to enable this command you should adjust the configuration of your console application:
  27. *
  28. * ```php
  29. * return [
  30. * // ...
  31. * 'controllerMap' => [
  32. * 'mongodb-migrate' => 'yii\mongodb\console\controllers\MigrateController'
  33. * ],
  34. * ];
  35. * ```
  36. *
  37. * Below are some common usages of this command:
  38. *
  39. * ```php
  40. * # creates a new migration named 'create_user_collection'
  41. * yii mongodb-migrate/create create_user_collection
  42. *
  43. * # applies ALL new migrations
  44. * yii mongodb-migrate
  45. *
  46. * # reverts the last applied migration
  47. * yii mongodb-migrate/down
  48. * ```
  49. *
  50. * Since 2.1.2, in case of usage Yii version >= 2.0.10, you can use namespaced migrations. In order to enable this
  51. * feature you should configure [[migrationNamespaces]] property for the controller at application configuration:
  52. *
  53. * ```php
  54. * return [
  55. * 'controllerMap' => [
  56. * 'mongodb-migrate' => [
  57. * 'class' => 'yii\mongodb\console\controllers\MigrateController',
  58. * 'migrationNamespaces' => [
  59. * 'app\migrations',
  60. * 'some\extension\migrations',
  61. * ],
  62. * //'migrationPath' => null, // allows to disable not namespaced migration completely
  63. * ],
  64. * ],
  65. * ];
  66. * ```
  67. *
  68. * @author Klimov Paul <klimov@zfort.com>
  69. * @since 2.0
  70. */
  71. class MigrateController extends BaseMigrateController
  72. {
  73. /**
  74. * @var string|array the name of the collection for keeping applied migration information.
  75. */
  76. public $migrationCollection = 'migration';
  77. /**
  78. * {@inheritdoc}
  79. */
  80. public $templateFile = '@yii/mongodb/views/migration.php';
  81. /**
  82. * @var Connection|string the DB connection object or the application
  83. * component ID of the DB connection.
  84. */
  85. public $db = 'mongodb';
  86. /**
  87. * {@inheritdoc}
  88. */
  89. public function options($actionID)
  90. {
  91. return array_merge(
  92. parent::options($actionID),
  93. ['migrationCollection', 'db'] // global for all actions
  94. );
  95. }
  96. /**
  97. * This method is invoked right before an action is to be executed (after all possible filters.)
  98. * It checks the existence of the [[migrationPath]].
  99. * @param \yii\base\Action $action the action to be executed.
  100. * @throws Exception if db component isn't configured
  101. * @return bool whether the action should continue to be executed.
  102. */
  103. public function beforeAction($action)
  104. {
  105. if (parent::beforeAction($action)) {
  106. if ($action->id !== 'create') {
  107. if (is_string($this->db)) {
  108. $this->db = Yii::$app->get($this->db);
  109. }
  110. if (!$this->db instanceof Connection) {
  111. throw new Exception("The 'db' option must refer to the application component ID of a MongoDB connection.");
  112. }
  113. }
  114. return true;
  115. }
  116. return false;
  117. }
  118. /**
  119. * Creates a new migration instance.
  120. * @param string $class the migration class name
  121. * @return \yii\mongodb\Migration the migration instance
  122. */
  123. protected function createMigration($class)
  124. {
  125. // since Yii 2.0.12 includeMigrationFile() exists, which replaced the code below
  126. // remove this construct when composer requirement raises above 2.0.12
  127. if (method_exists($this, 'includeMigrationFile')) {
  128. $this->includeMigrationFile($class);
  129. } else {
  130. $class = trim($class, '\\');
  131. if (strpos($class, '\\') === false) {
  132. $file = $this->migrationPath . DIRECTORY_SEPARATOR . $class . '.php';
  133. require_once($file);
  134. }
  135. }
  136. return new $class(['db' => $this->db, 'compact' => isset($this->compact) ? $this->compact : false]);
  137. }
  138. /**
  139. * {@inheritdoc}
  140. */
  141. protected function getMigrationHistory($limit)
  142. {
  143. $this->ensureBaseMigrationHistory();
  144. $query = (new Query())
  145. ->select(['version', 'apply_time'])
  146. ->from($this->migrationCollection)
  147. ->orderBy(['apply_time' => SORT_DESC, 'version' => SORT_DESC]);
  148. if (empty($this->migrationNamespaces)) {
  149. $query->limit($limit);
  150. $rows = $query->all($this->db);
  151. $history = ArrayHelper::map($rows, 'version', 'apply_time');
  152. unset($history[self::BASE_MIGRATION]);
  153. return $history;
  154. }
  155. $rows = $query->all($this->db);
  156. $history = [];
  157. foreach ($rows as $key => $row) {
  158. if ($row['version'] === self::BASE_MIGRATION) {
  159. continue;
  160. }
  161. if (preg_match('/m?(\d{6}_?\d{6})(\D.*)?$/is', $row['version'], $matches)) {
  162. $time = str_replace('_', '', $matches[1]);
  163. $row['canonicalVersion'] = $time;
  164. } else {
  165. $row['canonicalVersion'] = $row['version'];
  166. }
  167. $row['apply_time'] = (int)$row['apply_time'];
  168. $history[] = $row;
  169. }
  170. usort($history, function ($a, $b) {
  171. if ($a['apply_time'] === $b['apply_time']) {
  172. if (($compareResult = strcasecmp($b['canonicalVersion'], $a['canonicalVersion'])) !== 0) {
  173. return $compareResult;
  174. }
  175. return strcasecmp($b['version'], $a['version']);
  176. }
  177. return ($a['apply_time'] > $b['apply_time']) ? -1 : +1;
  178. });
  179. $history = array_slice($history, 0, $limit);
  180. $history = ArrayHelper::map($history, 'version', 'apply_time');
  181. return $history;
  182. }
  183. private $baseMigrationEnsured = false;
  184. /**
  185. * Ensures migration history contains at least base migration entry.
  186. */
  187. protected function ensureBaseMigrationHistory()
  188. {
  189. if (!$this->baseMigrationEnsured) {
  190. $query = new Query;
  191. $row = $query->select(['version'])
  192. ->from($this->migrationCollection)
  193. ->andWhere(['version' => self::BASE_MIGRATION])
  194. ->limit(1)
  195. ->one($this->db);
  196. if (empty($row)) {
  197. $this->addMigrationHistory(self::BASE_MIGRATION);
  198. }
  199. $this->baseMigrationEnsured = true;
  200. }
  201. }
  202. /**
  203. * {@inheritdoc}
  204. */
  205. protected function addMigrationHistory($version)
  206. {
  207. $this->db->getCollection($this->migrationCollection)->insert([
  208. 'version' => $version,
  209. 'apply_time' => time(),
  210. ]);
  211. }
  212. /**
  213. * {@inheritdoc}
  214. */
  215. protected function removeMigrationHistory($version)
  216. {
  217. $this->db->getCollection($this->migrationCollection)->remove([
  218. 'version' => $version,
  219. ]);
  220. }
  221. /**
  222. * {@inheritdoc}
  223. * @since 2.1.5
  224. */
  225. protected function truncateDatabase()
  226. {
  227. $collections = $this->db->getDatabase()->createCommand()->listCollections();
  228. foreach ($collections as $collection) {
  229. if (in_array($collection['name'], ['system.roles', 'system.users', 'system.indexes'])) {
  230. // prevent deleting database auth data
  231. // access to 'system.indexes' is more likely to be restricted, thus indexes will be dropped manually per collection
  232. $this->stdout("System collection {$collection['name']} skipped.\n");
  233. continue;
  234. }
  235. if (in_array($collection['name'], ['system.profile', 'system.js'])) {
  236. // dropping of system collection is unlikely to be permitted, attempt to clear them out instead
  237. $this->db->getDatabase()->createCommand()->delete($collection['name'], []);
  238. $this->stdout("System collection {$collection['name']} truncated.\n");
  239. continue;
  240. }
  241. $this->db->getDatabase()->createCommand()->dropIndexes($collection['name'], '*');
  242. $this->db->getDatabase()->dropCollection($collection['name']);
  243. $this->stdout("Collection {$collection['name']} dropped.\n");
  244. }
  245. }
  246. }