PageRenderTime 29ms CodeModel.GetById 31ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/Ruckusing/Util/Migrator.php

https://bitbucket.org/salimane/ruckusing-migrations
PHP | 324 lines | 209 code | 19 blank | 96 comment | 26 complexity | a1c0b1d2bef6f8bae8424493b99e84bf MD5 | raw file
  1. <?php
  2. /**
  3. * Ruckusing
  4. *
  5. * @category Ruckusing
  6. * @package Ruckusing_Util
  7. * @author Cody Caughlan <codycaughlan % gmail . com>
  8. * @link https://github.com/ruckus/ruckusing-migrations
  9. */
  10. /**
  11. * Implementation of Ruckusing_Util_Migrator
  12. *
  13. * @category Ruckusing
  14. * @package Ruckusing_Util
  15. * @author Cody Caughlan <codycaughlan % gmail . com>
  16. * @link https://github.com/ruckus/ruckusing-migrations
  17. */
  18. class Ruckusing_Util_Migrator
  19. {
  20. /**
  21. * adapter
  22. *
  23. * @var Ruckusing_Adapter_Base
  24. */
  25. private $_adapter = null;
  26. /**
  27. * migrations
  28. *
  29. * @var array
  30. */
  31. private $_migrations = array();
  32. /**
  33. * Creates an instance of Ruckusing_Util_Migrator
  34. *
  35. * @param Ruckusing_Adapter_Base $adapter The current adapter being used
  36. *
  37. * @return Ruckusing_Util_Migrator
  38. */
  39. public function __construct($adapter)
  40. {
  41. $this->setAdapter($adapter);
  42. }
  43. /**
  44. * set adapter
  45. *
  46. * @param Ruckusing_Adapter_Base $adapter the current adapter
  47. *
  48. * @return Ruckusing_Util_Migrator
  49. */
  50. public function setAdapter($adapter)
  51. {
  52. if (!($adapter instanceof Ruckusing_Adapter_Base)) {
  53. throw new Ruckusing_Exception(
  54. 'Adapter must be implement Ruckusing_Adapter_Base!',
  55. Ruckusing_Exception::INVALID_ADAPTER
  56. );
  57. }
  58. $this->_adapter = $adapter;
  59. return $this;
  60. }
  61. /**
  62. * Return the max version number from the DB, or "0" in the case of no versions available.
  63. * We must use strings because our date/timestamp when treated as an integer would cause overflow.
  64. *
  65. * @return string
  66. */
  67. public function get_max_version()
  68. {
  69. // We only want one row but we cannot assume that we are using MySQL and use a LIMIT statement
  70. // as it is not part of the SQL standard. Thus we have to select all rows and use PHP to return
  71. // the record we need
  72. $versions_nested = $this->_adapter->select_all(sprintf("SELECT version FROM %s", RUCKUSING_TS_SCHEMA_TBL_NAME));
  73. $versions = array();
  74. foreach ($versions_nested as $v) {
  75. $versions[] = $v['version'];
  76. }
  77. $num_versions = count($versions);
  78. if ($num_versions) {
  79. sort($versions); //sorts lowest-to-highest (ascending)
  80. return (string) $versions[$num_versions-1];
  81. } else {
  82. return null;
  83. }
  84. }
  85. /**
  86. * This methods calculates the actual set of migrations that should be performed, taking into account
  87. * the current version, the target version and the direction (up/down). When going up this method will
  88. * skip migrations that have not been executed, when going down this method will only include migrations
  89. * that have been executed.
  90. *
  91. * @param string $directory the migration dir
  92. * @param string $direction up/down
  93. * @param string $destination the version to migrate to
  94. * @param boolean $use_cache the current logger
  95. *
  96. * @return array
  97. */
  98. public function get_runnable_migrations($directory, $direction, $destination = null, $use_cache = true)
  99. {
  100. // cache migration lookups and early return if we've seen this requested set
  101. if ($use_cache == true) {
  102. $key = $direction . '-' . $destination;
  103. if (array_key_exists($key, $this->_migrations)) {
  104. return($this->_migrations[$key]);
  105. }
  106. }
  107. $runnable = array();
  108. $migrations = array();
  109. $migrations = $this->get_migration_files($directory, $direction);
  110. $current = $this->find_version($migrations, $this->get_max_version());
  111. $target = $this->find_version($migrations, $destination);
  112. if (is_null($target) && !is_null($destination) && $destination > 0) {
  113. throw new Ruckusing_Exception(
  114. "Could not find target version {$destination} in set of migrations.",
  115. Ruckusing_Exception::INVALID_TARGET_MIGRATION
  116. );
  117. }
  118. $start = $direction == 'up' ? 0 : array_search($current, $migrations);
  119. $start = $start !== false ? $start : 0;
  120. $finish = array_search($target, $migrations);
  121. $finish = $finish !== false ? $finish : (count($migrations) - 1);
  122. $item_length = ($finish - $start) + 1;
  123. $runnable = array_slice($migrations, $start, $item_length);
  124. //dont include first item if going down but not if going all the way to the bottom
  125. if ($direction == 'down' && count($runnable) > 0 && $target != null) {
  126. array_pop($runnable);
  127. }
  128. $executed = $this->get_executed_migrations();
  129. $to_execute = array();
  130. foreach ($runnable as $migration) {
  131. //Skip ones that we have already executed
  132. if ($direction == 'up' && in_array($migration['version'], $executed)) {
  133. continue;
  134. }
  135. //Skip ones that we never executed
  136. if ($direction == 'down' && !in_array($migration['version'], $executed)) {
  137. continue;
  138. }
  139. $to_execute[] = $migration;
  140. }
  141. if ($use_cache == true) {
  142. $this->_migrations[$key] = $to_execute;
  143. }
  144. return($to_execute);
  145. }
  146. /**
  147. * Generate a timestamp for the current time in UTC format
  148. * Returns a string like '20090122193325'
  149. *
  150. * @return string
  151. */
  152. public static function generate_timestamp()
  153. {
  154. return gmdate('YmdHis', time());
  155. }
  156. /**
  157. * If we are going UP then log this version as executed, if going DOWN then delete
  158. * this version from our set of executed migrations.
  159. *
  160. * @param object $version the version
  161. * @param object $direction up/down
  162. *
  163. * @return string
  164. */
  165. public function resolve_current_version($version, $direction)
  166. {
  167. if ($direction === 'up') {
  168. $this->_adapter->set_current_version($version);
  169. }
  170. if ($direction === 'down') {
  171. $this->_adapter->remove_version($version);
  172. }
  173. return $version;
  174. }
  175. /**
  176. * Returns an array of strings which represent version numbers that we *have* migrated
  177. *
  178. * @return array
  179. */
  180. public function get_executed_migrations()
  181. {
  182. return $this->executed_migrations();
  183. }
  184. /**
  185. * Return a set of migration files, according to the given direction.
  186. * If nested, then return a complex array with the migration parts broken up into parts
  187. * which make analysis much easier.
  188. *
  189. * @param string $directory the migration dir
  190. * @param string $direction the direction up/down
  191. *
  192. * @return array
  193. */
  194. public static function get_migration_files($directory, $direction)
  195. {
  196. $valid_files = array();
  197. if (!is_dir($directory)) {
  198. printf("\n\tMigrations directory (%s doesn't exist, attempting to create.", $directory);
  199. if (mkdir($directory, 0755, true) === FALSE) {
  200. throw new Ruckusing_Exception(
  201. "\n\tUnable to create migrations directory at %s, check permissions?", $directory,
  202. Ruckusing_Exception::INVALID_MIGRATION_DIR
  203. );
  204. } else {
  205. printf("\n\tCreated OK");
  206. }
  207. }
  208. $files = scandir($directory);
  209. $file_cnt = count($files);
  210. if ($file_cnt > 0) {
  211. for ($i = 0; $i < $file_cnt; $i++) {
  212. if (preg_match('/^(\d+)_(.*)\.php$/', $files[$i], $matches)) {
  213. if (count($matches) == 3) {
  214. $valid_files[] = $files[$i];
  215. }
  216. }
  217. }
  218. }
  219. sort($valid_files); //sorts in place
  220. if ($direction == 'down') {
  221. $valid_files = array_reverse($valid_files);
  222. }
  223. //user wants a nested structure
  224. $files = array();
  225. $cnt = count($valid_files);
  226. for ($i = 0; $i < $cnt; $i++) {
  227. $migration = $valid_files[$i];
  228. if (preg_match('/^(\d+)_(.*)\.php$/', $migration, $matches)) {
  229. $files[] = array(
  230. 'version' => $matches[1],
  231. 'class' => $matches[2],
  232. 'file' => $matches[0]
  233. );
  234. }
  235. }
  236. return $files;
  237. }
  238. //== Private methods
  239. /**
  240. * Find the specified structure (representing a migration) that matches the given version
  241. *
  242. * @param array $migrations the list of migrations
  243. * @param string $version the version being searched
  244. *
  245. * @return string
  246. */
  247. private function find_version($migrations, $version)
  248. {
  249. $len = count($migrations);
  250. for ($i = 0; $i < $len; $i++) {
  251. if ($migrations[$i]['version'] == $version) {
  252. return $migrations[$i];
  253. }
  254. }
  255. return null;
  256. }
  257. /**
  258. * Find the index of the migration in the set of migrations that match the given version
  259. *
  260. * @param array $migrations the list of migrations
  261. * @param string $version the version being searched
  262. *
  263. * @return integer
  264. */
  265. private function find_version_index($migrations, $version)
  266. {
  267. //edge case
  268. if (is_null($version)) {
  269. return null;
  270. }
  271. $len = count($migrations);
  272. for ($i = 0; $i < $len; $i++) {
  273. if ($migrations[$i]['version'] == $version) {
  274. return $i;
  275. }
  276. }
  277. return null;
  278. }
  279. /**
  280. * Query the database and return a list of migration versions that *have* been executed
  281. *
  282. * @return array
  283. */
  284. private function executed_migrations()
  285. {
  286. $query_sql = sprintf('SELECT version FROM %s', RUCKUSING_TS_SCHEMA_TBL_NAME);
  287. $versions = $this->_adapter->select_all($query_sql);
  288. $executed = array();
  289. foreach ($versions as $v) {
  290. $executed[] = $v['version'];
  291. }
  292. sort($executed);
  293. return $executed;
  294. }
  295. }