PageRenderTime 43ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/classes/migrate.php

https://github.com/monsonis/core
PHP | 609 lines | 342 code | 67 blank | 200 comment | 24 complexity | be715da98d4ff3d072c0c4520f142f2a MD5 | raw file
Possible License(s): BSD-3-Clause
  1. <?php
  2. /**
  3. * Part of the Fuel framework.
  4. *
  5. * @package Fuel
  6. * @version 1.0
  7. * @author Fuel Development Team
  8. * @license MIT License
  9. * @copyright 2010 - 2012 Fuel Development Team
  10. * @link http://fuelphp.com
  11. */
  12. namespace Fuel\Core;
  13. /**
  14. * Migrate Class
  15. *
  16. * @package Fuel
  17. * @category Migrations
  18. * @link http://docs.fuelphp.com/classes/migrate.html
  19. */
  20. class Migrate
  21. {
  22. /**
  23. * @var array current migrations registered in the database
  24. */
  25. protected static $migrations = array();
  26. /**
  27. * @var string migration classes namespace prefix
  28. */
  29. protected static $prefix = 'Fuel\\Migrations\\';
  30. /**
  31. * @var string name of the migration table
  32. */
  33. protected static $table = 'migration';
  34. /**
  35. * @var array migration table schema
  36. */
  37. protected static $table_definition = array(
  38. 'type' => array('type' => 'varchar', 'constraint' => 25),
  39. 'name' => array('type' => 'varchar', 'constraint' => 50),
  40. 'migration' => array('type' => 'varchar', 'constraint' => 100, 'null' => false, 'default' => ''),
  41. );
  42. /**
  43. * loads in the migrations config file, checks to see if the migrations
  44. * table is set in the database (if not, create it), and reads in all of
  45. * the versions from the DB.
  46. *
  47. * @return void
  48. */
  49. public static function _init()
  50. {
  51. logger(\Fuel::L_DEBUG, 'Migrate class initialized');
  52. // load the migrations config
  53. \Config::load('migrations', true);
  54. // set the name of the table containing the installed migrations
  55. static::$table = \Config::get('migrations.table', static::$table);
  56. // installs or upgrades the migration table to the current schema
  57. static::table_version_check();
  58. //get all installed migrations from db
  59. $migrations = \DB::select()
  60. ->from(static::$table)
  61. ->order_by('type', 'ASC')
  62. ->order_by('name', 'ASC')
  63. ->order_by('migration', 'ASC')
  64. ->execute()
  65. ->as_array();
  66. // convert the db migrations to match the config file structure
  67. foreach($migrations as $migration)
  68. {
  69. isset(static::$migrations[$migration['type']]) or static::$migrations[$migration['type']] = array();
  70. static::$migrations[$migration['type']][$migration['name']][] = $migration['migration'];
  71. }
  72. }
  73. /**
  74. * migrate to a specific version or range of versions
  75. *
  76. * @param mixed version to migrate to (up or down!)
  77. * @param string name of the package, module or app
  78. * @param string type of migration (package, module or app)
  79. *
  80. * @throws UnexpectedValueException
  81. * @return array
  82. */
  83. public static function version($version = null, $name = 'default', $type = 'app')
  84. {
  85. // get the current version from the config
  86. $current = \Config::get('migrations.version.'.$type.'.'.$name);
  87. // any migrations defined?
  88. if ( ! empty($current))
  89. {
  90. // get the timestamp of the last installed migration
  91. if (preg_match('/^(.*?)_(.*)$/', end($current), $match))
  92. {
  93. // determine the direction
  94. $direction = (is_null($version) or $match[1] < $version) ? 'up' : 'down';
  95. // fetch the migrations
  96. if ($direction == 'up')
  97. {
  98. $migrations = static::find_migrations($name, $type, $match[1], $version);
  99. }
  100. else
  101. {
  102. $migrations = static::find_migrations($name, $type, $version, $match[1], $direction);
  103. // we're going down, so reverse the order of mygrations
  104. $migrations = array_reverse($migrations, true);
  105. }
  106. // run migrations from current version to given version
  107. return static::run($migrations, $name, $type, $direction);
  108. }
  109. else
  110. {
  111. throw new \UnexpectedValueException('Could not determine a valid version from '.$current.'.');
  112. }
  113. }
  114. // run migrations from the beginning to given version
  115. return static::run(static::find_migrations($name, $type, null, $version), $name, $type, 'up');
  116. }
  117. /**
  118. * migrate to a latest version
  119. *
  120. * @param string name of the package, module or app
  121. * @param string type of migration (package, module or app)
  122. *
  123. * @return array
  124. */
  125. public static function latest($name = 'default', $type = 'app')
  126. {
  127. // equivalent to from current version to latest
  128. return static::version(null, $name, $type);
  129. }
  130. /**
  131. * migrate to the version defined in the config file
  132. *
  133. * @param string name of the package, module or app
  134. * @param string type of migration (package, module or app)
  135. *
  136. * @return array
  137. */
  138. public static function current($name = 'default', $type = 'app')
  139. {
  140. // get the current version from the config
  141. $current = \Config::get('migrations.version.'.$type.'.'.$name);
  142. // any migrations defined?
  143. if ( ! empty($current))
  144. {
  145. // get the timestamp of the last installed migration
  146. if (preg_match('/^(.*?)_(.*)$/', end($current), $match))
  147. {
  148. // run migrations from start to current version
  149. return static::run(static::find_migrations($name, $type, null, $match[1]), $name, $type, 'up');
  150. }
  151. }
  152. // nothing to migrate
  153. return array();
  154. }
  155. /**
  156. * migrate up to the next version
  157. *
  158. * @param mixed version to migrate up to
  159. * @param string name of the package, module or app
  160. * @param string type of migration (package, module or app)
  161. *
  162. * @return array
  163. */
  164. public static function up($version = null, $name = 'default', $type = 'app')
  165. {
  166. // get the current version info from the config
  167. $current = \Config::get('migrations.version.'.$type.'.'.$name);
  168. // get the last migration installed
  169. $current = empty($current) ? null : end($current);
  170. // get the available migrations after the current one
  171. $migrations = static::find_migrations($name, $type, $current, $version);
  172. // found any?
  173. if ( ! empty($migrations))
  174. {
  175. // if no version was given, only install the next migration
  176. is_null($version) and $migrations = array(reset($migrations));
  177. // install migrations found
  178. return static::run($migrations, $name, $type, 'up');
  179. }
  180. // nothing to migrate
  181. return array();
  182. }
  183. /**
  184. * migrate down to the previous version
  185. *
  186. * @param mixed version to migrate down to
  187. * @param string name of the package, module or app
  188. * @param string type of migration (package, module or app)
  189. *
  190. * @return array
  191. */
  192. public static function down($version = null, $name = 'default', $type = 'app')
  193. {
  194. // get the current version info from the config
  195. $current = \Config::get('migrations.version.'.$type.'.'.$name);
  196. // any migrations defined?
  197. if ( ! empty($current))
  198. {
  199. // get the last entry
  200. $current = end($current);
  201. // get the available migrations before the last current one
  202. $migrations = static::find_migrations($name, $type, $version, $current, 'down');
  203. // found any?
  204. if ( ! empty($migrations))
  205. {
  206. // we're going down, so reverse the order of mygrations
  207. $migrations = array_reverse($migrations, true);
  208. // if no version was given, only revert the last migration
  209. is_null($version) and $migrations = array(reset($migrations));
  210. // revert the installed migrations
  211. return static::run($migrations, $name, $type, 'down');
  212. }
  213. }
  214. // nothing to migrate
  215. return array();
  216. }
  217. /**
  218. * run the action migrations found
  219. *
  220. * @param array list of files to migrate
  221. * @param string name of the package, module or app
  222. * @param string type of migration (package, module or app)
  223. * @param string method to call on the migration
  224. *
  225. * @return array
  226. */
  227. protected static function run($migrations, $name, $type, $method = 'up')
  228. {
  229. // storage for installed migrations
  230. $done = array();
  231. // Loop through the runnable migrations and run them
  232. foreach ($migrations as $ver => $migration)
  233. {
  234. logger(Fuel::L_INFO, 'Migrating to version: '.$ver);
  235. call_user_func(array(new $migration['class'], $method));
  236. $file = basename($migration['path'], '.php');
  237. $method == 'up' ? static::write_install($name, $type, $file) : static::write_revert($name, $type, $file);
  238. $done[] = $file;
  239. }
  240. empty($done) or logger(Fuel::L_INFO, 'Migrated to '.$ver.' successfully.');
  241. return $done;
  242. }
  243. /**
  244. * add an installed migration to the database
  245. *
  246. * @param string name of the package, module or app
  247. * @param string type of migration (package, module or app)
  248. * @param string name of the migration file just run
  249. *
  250. * @return void
  251. */
  252. protected static function write_install($name, $type, $file)
  253. {
  254. // add the migration just run
  255. \DB::insert(static::$table)->set(array(
  256. 'name' => $name,
  257. 'type' => $type,
  258. 'migration' => $file,
  259. ))->execute();
  260. // add the file to the list of run migrations
  261. static::$migrations[$type][$name][] = $file;
  262. // make sure the migrations are in the correct order
  263. sort(static::$migrations[$type][$name]);
  264. // and save the update to the environment config file
  265. \Config::set('migrations.version.'.$type.'.'.$name, static::$migrations[$type][$name]);
  266. \Config::save(\Fuel::$env.DS.'migrations', 'migrations');
  267. }
  268. /**
  269. * remove a reverted migration from the database
  270. *
  271. * @param string name of the package, module or app
  272. * @param string type of migration (package, module or app)
  273. * @param string name of the migration file just run
  274. *
  275. * @return void
  276. */
  277. protected static function write_revert($name, $type, $file)
  278. {
  279. // remove the migration just run
  280. \DB::delete(static::$table)
  281. ->where('name', $name)
  282. ->where('type', $type)
  283. ->where('migration', $file)
  284. ->execute();
  285. // remove the file from the list of run migrations
  286. if (($key = array_search($file, static::$migrations[$type][$name])) !== false)
  287. {
  288. unset(static::$migrations[$type][$name][$key]);
  289. }
  290. // make sure the migrations are in the correct order
  291. sort(static::$migrations[$type][$name]);
  292. // and save the update to the config file
  293. \Config::set('migrations.version.'.$type.'.'.$name, static::$migrations[$type][$name]);
  294. \Config::save(\Fuel::$env.DS.'migrations', 'migrations');
  295. }
  296. /**
  297. * migrate down to the previous version
  298. *
  299. * @param string name of the package, module or app
  300. * @param string type of migration (package, module or app)
  301. * @param mixed version to start migrations from, or null to start at the beginning
  302. * @param mixed version to end migrations by, or null to migrate to the end
  303. *
  304. * @return array
  305. */
  306. protected static function find_migrations($name, $type, $start = null, $end = null, $direction = 'up')
  307. {
  308. // Load all *_*.php files in the migrations path
  309. $method = '_find_'.$type;
  310. if ( ! $files = static::$method($name))
  311. {
  312. return array();
  313. }
  314. // get the currently installed migrations from the DB
  315. $current = \Arr::get(static::$migrations, $type.'.'.$name, array());
  316. // storage for the result
  317. $migrations = array();
  318. // normalize start and end values
  319. if ( ! is_null($start))
  320. {
  321. // if we have a prefix, use that
  322. ($pos = strpos($start, '_')) === false or $start = ltrim(substr($start, 0, $pos), '0');
  323. is_numeric($start) and $start = (int) $start;
  324. }
  325. if ( ! is_null($end))
  326. {
  327. // if we have a prefix, use that
  328. ($pos = strpos($end, '_')) === false or $end = ltrim(substr($end, 0, $pos), '0');
  329. is_numeric($end) and $end = (int) $end;
  330. }
  331. // filter the migrations out of bounds
  332. foreach ($files as $file)
  333. {
  334. // get the version for this migration and normalize it
  335. $migration = basename($file);
  336. ($pos = strpos($migration, '_')) === false or $migration = ltrim(substr($migration, 0, $pos), '0');
  337. is_numeric($migration) and $migration = (int) $migration;
  338. // add the file to the migrations list if it's in between version bounds
  339. if ((is_null($start) or $migration > $start) and (is_null($end) or $migration <= $end))
  340. {
  341. // see if it is already installed
  342. if ( in_array(basename($file, '.php'), $current))
  343. {
  344. // already installed. store it only if we're going down
  345. $direction == 'down' and $migrations[$migration] = array('path' => $file);
  346. }
  347. else
  348. {
  349. // not installed yet. store it only if we're going up
  350. $direction == 'up' and $migrations[$migration] = array('path' => $file);
  351. }
  352. }
  353. }
  354. // We now prepare to actually DO the migrations
  355. // But first let's make sure that everything is the way it should be
  356. foreach ($migrations as $ver => $migration)
  357. {
  358. // get the migration filename from the path
  359. $migration['file'] = basename($migration['path']);
  360. // make sure the migration filename has a valid format
  361. if (preg_match('/^.*?_(.*).php$/', $migration['file'], $match))
  362. {
  363. // determine the classname for this migration
  364. $class_name = ucfirst(strtolower($match[1]));
  365. // load the file and determiine the classname
  366. include $migration['path'];
  367. $class = static::$prefix.$class_name;
  368. // make sure it exists in the migration file loaded
  369. if ( ! class_exists($class, false))
  370. {
  371. throw new FuelException(sprintf('Migration "%s" does not contain expected class "%s"', $migration['path'], $class));
  372. }
  373. // and that it contains an "up" and "down" method
  374. if ( ! is_callable(array($class, 'up')) or ! is_callable(array($class, 'down')))
  375. {
  376. throw new FuelException(sprintf('Migration class "%s" must include public methods "up" and "down"', $name));
  377. }
  378. $migrations[$ver]['class'] = $class;
  379. }
  380. else
  381. {
  382. throw new FuelException(sprintf('Invalid Migration filename "%s"', $migration['path']));
  383. }
  384. }
  385. // make sure the result is sorted properly with all version types
  386. uksort($migrations, 'strnatcasecmp');
  387. return $migrations;
  388. }
  389. /**
  390. * finds migrations for the given app
  391. *
  392. * @param string name of the app (not used at the moment)
  393. *
  394. * @return array
  395. */
  396. protected static function _find_app($name = null)
  397. {
  398. return glob(APPPATH.\Config::get('migrations.folder').'*_*.php');
  399. }
  400. /**
  401. * finds migrations for the given module (or all if name is not given)
  402. *
  403. * @param string name of the module
  404. *
  405. * @return array
  406. */
  407. protected static function _find_module($name = null)
  408. {
  409. if ($name)
  410. {
  411. // find a module
  412. foreach (\Config::get('module_paths') as $m)
  413. {
  414. $files = glob($m .$name.'/'.\Config::get('migrations.folder').'*_*.php');
  415. if (count($files))
  416. {
  417. break;
  418. }
  419. }
  420. }
  421. else
  422. {
  423. // find all modules
  424. $files = array();
  425. foreach (\Config::get('module_paths') as $m)
  426. {
  427. $files = array_merge($files, glob($m.'*/'.\Config::get('migrations.folder').'*_*.php'));
  428. }
  429. }
  430. return $files;
  431. }
  432. /**
  433. * finds migrations for the given package (or all if name is not given)
  434. *
  435. * @param string name of the package
  436. *
  437. * @return array
  438. */
  439. protected static function _find_package($name = null)
  440. {
  441. if ($name)
  442. {
  443. // find a package
  444. foreach (\Config::get('package_paths', array(PKGPATH)) as $p)
  445. {
  446. $files = glob($p .$name.'/'.\Config::get('migrations.folder').'*_*.php');
  447. if (count($files))
  448. {
  449. break;
  450. }
  451. }
  452. }
  453. else
  454. {
  455. // find all packages
  456. $files = array();
  457. foreach (\Config::get('package_paths', array(PKGPATH)) as $p)
  458. {
  459. $files = array_merge($files, glob($p.'*/'.\Config::get('migrations.folder').'*_*.php'));
  460. }
  461. }
  462. return $files;
  463. }
  464. /**
  465. * installs or upgrades the migration table to the current schema
  466. *
  467. * @return void
  468. *
  469. * @deprecated Remove upgrade check in 1.3
  470. */
  471. protected static function table_version_check()
  472. {
  473. // if table does not exist
  474. if ( ! \DBUtil::table_exists(static::$table))
  475. {
  476. // create table
  477. \DBUtil::create_table(static::$table, static::$table_definition);
  478. }
  479. // check if a table upgrade is needed
  480. elseif ( ! \DBUtil::field_exists(static::$table, array('migration')))
  481. {
  482. // get the current migration status
  483. $current = \DB::select()->from(static::$table)->order_by('type', 'ASC')->order_by('name', 'ASC')->execute()->as_array();
  484. // drop the existing table, and recreate it in the new layout
  485. \DBUtil::drop_table(static::$table);
  486. \DBUtil::create_table(static::$table, static::$table_definition);
  487. // check if we had a current migration status
  488. if ( ! empty($current))
  489. {
  490. // do we need to migrate from a v1.0 migration environment?
  491. if (isset($current[0]['current']))
  492. {
  493. // convert the current result into a v1.1. migration environment structure
  494. $current = array(0 => array('name' => 'default', 'type' => 'app', 'version' => $current[0]['current']));
  495. }
  496. // build a new config structure
  497. $configs = array();
  498. // convert the v1.1 structure to the v1.2 structure
  499. foreach ($current as $migration)
  500. {
  501. // find the migrations for this entry
  502. $migrations = static::find_migrations($migration['name'], $migration['type'], null, $migration['version']);
  503. // array to keep track of the migrations already run
  504. $config = array();
  505. // add the individual migrations found
  506. foreach ($migrations as $file)
  507. {
  508. $file = pathinfo($file['path']);
  509. // add this migration to the table
  510. \DB::insert(static::$table)->set(array(
  511. 'name' => $migration['name'],
  512. 'type' => $migration['type'],
  513. 'migration' => $file['filename'],
  514. ))->execute();
  515. // and to the config
  516. $config[] = $file['filename'];
  517. }
  518. // create a config entry for this name and type if needed
  519. isset($configs[$migration['type']]) or $configs[$migration['type']] = array();
  520. $configs[$migration['type']][$migration['name']] = $config;
  521. }
  522. // write the updated migrations config back
  523. \Config::set('migrations.version', $configs);
  524. \Config::save(\Fuel::$env.DS.'migrations', 'migrations');
  525. }
  526. }
  527. // delete any old migration config file that may exist
  528. file_exists(APPPATH.'config'.DS.'migrations.php') and unlink(APPPATH.'config'.DS.'migrations.php');
  529. }
  530. }