EMigrateMongoCommand.php 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. <?php
  2. Yii::import('system.cli.commands.MigrateCommand');
  3. /**
  4. * EMigrateMongoCommand manages the database migrations.
  5. *
  6. * It is based on the "yiic migrate" command. It allows to manage database migrations
  7. * for mongodb.
  8. *
  9. * To enable this command, add a "commandMap"-section to your config file:
  10. *
  11. * <pre>
  12. * 'commandMap' => array(
  13. * 'migratemongo' => array(
  14. * 'class' => 'application.extensions.MongoYii.util.EMigrateMongoCommand'
  15. * )
  16. * )
  17. * </pre>
  18. */
  19. class EMigrateMongoCommand extends MigrateCommand
  20. {
  21. /**
  22. * @var string the name of the mongodb collection that contains migration data.
  23. * If not set, it will be using 'migrations' as the collection.
  24. */
  25. public $collectionName = 'migrations';
  26. /**
  27. *
  28. * @var string the connectionId of the EMongoClient component
  29. */
  30. public $connectionID = 'mongodb';
  31. /**
  32. * @var EMongoClient
  33. */
  34. private $_db;
  35. public function actionHistory($args)
  36. {
  37. $limit = isset($args[0]) ? (int)$args[0] : 0;
  38. $migrations = $this->getMigrationHistory($limit);
  39. if($migrations === array()){
  40. echo "No migration has been done before.\n";
  41. }else{
  42. $n = count($migrations);
  43. if($limit > 0){
  44. echo "Showing the last $n applied " . ($n === 1 ? 'migration' : 'migrations') . ":\n";
  45. }else{
  46. echo "Total $n " . ($n === 1 ? 'migration has' : 'migrations have') . " been applied before:\n";
  47. }
  48. foreach($migrations as $version => $time){
  49. echo " (" . date('Y-m-d H:i:s', $time) . ') ' . $version . "\n";
  50. }
  51. }
  52. }
  53. public function actionMark($args)
  54. {
  55. if(isset($args[0])){
  56. $version = $args[0];
  57. }else{
  58. $this->usageError('Please specify which version to mark to.');
  59. }
  60. $originalVersion = $version;
  61. if(preg_match('/^m?(\d{6}_\d{6})(_.*?)?$/', $version, $matches)){
  62. $version = 'm' . $matches[1];
  63. }else{
  64. echo "Error: The version option must be either a timestamp (e.g. 101129_185401)\nor the full name of a migration (e.g. m101129_185401_create_user_table).\n";
  65. return 1;
  66. }
  67. $collection = $this->getDbConnection()->{$this->collectionName};
  68. // try mark up
  69. $migrations = $this->getNewMigrations();
  70. foreach($migrations as $i => $migration){
  71. if(strpos($migration, $version . '_') === 0){
  72. if($this->confirm("Set migration history at $originalVersion?")){
  73. for($j = 0; $j <= $i; ++ $j){
  74. $collection->save(array(
  75. 'version' => $migrations[$j],
  76. 'apply_time' => time()
  77. ));
  78. }
  79. echo "The migration history is set at $originalVersion.\nNo actual migration was performed.\n";
  80. }
  81. return 0;
  82. }
  83. }
  84. // try mark down
  85. $migrations = array_keys($this->getMigrationHistory(-1));
  86. foreach($migrations as $i => $migration){
  87. if(strpos($migration, $version . '_') === 0){
  88. if($i === 0){
  89. echo "Already at '$originalVersion'. Nothing needs to be done.\n";
  90. }else{
  91. if($this->confirm("Set migration history at $originalVersion?")){
  92. for($j = 0; $j < $i; ++ $j){
  93. $collection->delete(array(
  94. 'version' => $migrations[$j]
  95. ));
  96. }
  97. echo "The migration history is set at $originalVersion.\nNo actual migration was performed.\n";
  98. }
  99. }
  100. return 0;
  101. }
  102. }
  103. echo "Error: Unable to find the version '$originalVersion'.\n";
  104. return 1;
  105. }
  106. public function beforeAction($action, $params)
  107. {
  108. $path = Yii::getPathOfAlias($this->migrationPath);
  109. if($path === false || !is_dir($path)){
  110. echo 'Error: The migration directory does not exist: ' . $this->migrationPath . "\n";
  111. exit(1);
  112. }
  113. $this->migrationPath = $path;
  114. $yiiVersion = Yii::getVersion();
  115. echo "\nYii Migration Tool for MongoYii v1.0 (based on Yii v{$yiiVersion})\n\n";
  116. return CConsoleCommand::beforeAction($action, $params);
  117. }
  118. public function getHelp()
  119. {
  120. return <<<EOD
  121. USAGE
  122. yiic migratemongo [action] [parameter]
  123. DESCRIPTION
  124. This command provides support for mongodb migrations. The optional
  125. 'action' parameter specifies which specific migration task to perform.
  126. It can take these values: up, down, to, create, history, new, mark.
  127. If the 'action' parameter is not given, it defaults to 'up'.
  128. Each action takes different parameters. Their usage can be found in
  129. the following examples.
  130. EXAMPLES
  131. * yiic migratemongo
  132. Applies ALL new migrations. This is equivalent to 'yiic migratemongo up'.
  133. * yiic migratemongo create create_user_collection
  134. Creates a new migration named 'create_user_collection'.
  135. * yiic migratemongo up 3
  136. Applies the next 3 new migrations.
  137. * yiic migratemongo down
  138. Reverts the last applied migration.
  139. * yiic migratemongo down 3
  140. Reverts the last 3 applied migrations.
  141. * yiic migratemongo to 101129_185401
  142. Migrates up or down to version 101129_185401.
  143. * yiic migratemongo mark 101129_185401
  144. Modifies the migration history up or down to version 101129_185401.
  145. No actual migration will be performed.
  146. * yiic migratemongo history
  147. Shows all previously applied migration information.
  148. * yiic migratemongo history 10
  149. Shows the last 10 applied migrations.
  150. * yiic migratemongo new
  151. Shows all new migrations.
  152. * yiic migratemongo new 10
  153. Shows the next 10 migrations that have not been applied.
  154. EOD;
  155. }
  156. protected function createMigrationHistoryTable()
  157. {
  158. echo 'Creating initial migration history record...';
  159. $this->getDbConnection()->{$this->collectionName}->save(array(
  160. 'version' => self::BASE_MIGRATION,
  161. 'apply_time' => time ()
  162. ));
  163. echo "done.\n";
  164. }
  165. protected function getDbConnection()
  166. {
  167. if($this->_db !== null){
  168. return $this->_db;
  169. }elseif(($this->_db = Yii::app ()->getComponent($this->connectionID)) instanceof EMongoClient){
  170. return $this->_db;
  171. }
  172. echo "Error: MigrateMongoCommand.connectionID '{$this->connectionID}' is invalid. Please make sure it refers to the ID of a EMongoClient application component.\n";
  173. exit(1);
  174. }
  175. protected function getMigrationHistory($limit)
  176. {
  177. return CHtml::listData(iterator_to_array($this->getDbConnection()->{$this->collectionName}->find()->sort(array(
  178. 'version' => - 1
  179. ))->limit($limit)), 'version', 'apply_time');
  180. }
  181. protected function getNewMigrations()
  182. {
  183. $applied = array();
  184. foreach($this->getMigrationHistory(0) as $version => $time){
  185. $applied[substr($version, 1, 13)] = true;
  186. }
  187. $migrations = array();
  188. $handle = opendir($this->migrationPath);
  189. while(($file = readdir($handle)) !== false){
  190. if($file === '.' || $file === '..'){
  191. continue;
  192. }
  193. $path = $this->migrationPath . DIRECTORY_SEPARATOR . $file;
  194. if(preg_match('/^(m(\d{6}_\d{6})_.*?)\.php$/', $file, $matches) && is_file($path) && !isset($applied[$matches [2]])){
  195. $migrations[] = $matches[1];
  196. }
  197. }
  198. closedir($handle);
  199. sort($migrations);
  200. return $migrations;
  201. }
  202. protected function getTemplate()
  203. {
  204. if($this->templateFile !== null){
  205. return file_get_contents(Yii::getPathOfAlias($this->templateFile) . '.php');
  206. }else{
  207. return <<<EOD
  208. <?php
  209. class {ClassName} extends EMongoMigration
  210. {
  211. public function up()
  212. {
  213. }
  214. public function down()
  215. {
  216. echo "{ClassName} does not support migration down.\\n";
  217. return false;
  218. }
  219. }
  220. EOD;
  221. }
  222. }
  223. protected function migrateDown($class)
  224. {
  225. if($class === self::BASE_MIGRATION){
  226. return;
  227. }
  228. echo "*** reverting $class\n";
  229. $start = microtime(true);
  230. $migration = $this->instantiateMigration($class);
  231. if($migration->down() !== false){
  232. $this->getDbConnection()->{$this->collectionName}->remove(array(
  233. 'version' => $class
  234. ));
  235. $time = microtime(true) - $start;
  236. echo "*** reverted $class (time: " . sprintf("%.3f", $time) . "s)\n\n";
  237. }else{
  238. $time = microtime(true) - $start;
  239. echo "*** failed to revert $class (time: " . sprintf("%.3f", $time) . "s)\n\n";
  240. return false;
  241. }
  242. }
  243. protected function migrateUp($class)
  244. {
  245. if($class === self::BASE_MIGRATION){
  246. return;
  247. }
  248. echo "*** applying $class\n";
  249. $start = microtime(true);
  250. $migration = $this->instantiateMigration($class);
  251. if($migration->up() !== false){
  252. $this->getDbConnection()->{$this->collectionName}->save(array(
  253. 'version' => $class,
  254. 'apply_time' => time()
  255. ));
  256. $time = microtime(true) - $start;
  257. echo "*** applied $class (time: " . sprintf("%.3f", $time) . "s)\n\n";
  258. }else{
  259. $time = microtime(true) - $start;
  260. echo "*** failed to apply $class (time: " . sprintf("%.3f", $time) . "s)\n\n";
  261. return false;
  262. }
  263. }
  264. }