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

/classes/migrate.php

https://github.com/studiofrenetic/core
PHP | 616 lines | 347 code | 69 blank | 200 comment | 25 complexity | d3e42550a83f754021f7c587f4b9dfeb 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. $result = call_user_func(array(new $migration['class'], $method));
  236. if ($result === false)
  237. {
  238. logger(Fuel::L_INFO, 'Skipped migration to '.$ver.'.');
  239. return $done;
  240. }
  241. $file = basename($migration['path'], '.php');
  242. $method == 'up' ? static::write_install($name, $type, $file) : static::write_revert($name, $type, $file);
  243. $done[] = $file;
  244. }
  245. empty($done) or logger(Fuel::L_INFO, 'Migrated to '.$ver.' successfully.');
  246. return $done;
  247. }
  248. /**
  249. * add an installed migration to the database
  250. *
  251. * @param string name of the package, module or app
  252. * @param string type of migration (package, module or app)
  253. * @param string name of the migration file just run
  254. *
  255. * @return void
  256. */
  257. protected static function write_install($name, $type, $file)
  258. {
  259. // add the migration just run
  260. \DB::insert(static::$table)->set(array(
  261. 'name' => $name,
  262. 'type' => $type,
  263. 'migration' => $file,
  264. ))->execute();
  265. // add the file to the list of run migrations
  266. static::$migrations[$type][$name][] = $file;
  267. // make sure the migrations are in the correct order
  268. sort(static::$migrations[$type][$name]);
  269. // and save the update to the environment config file
  270. \Config::set('migrations.version.'.$type.'.'.$name, static::$migrations[$type][$name]);
  271. \Config::save(\Fuel::$env.DS.'migrations', 'migrations');
  272. }
  273. /**
  274. * remove a reverted migration from the database
  275. *
  276. * @param string name of the package, module or app
  277. * @param string type of migration (package, module or app)
  278. * @param string name of the migration file just run
  279. *
  280. * @return void
  281. */
  282. protected static function write_revert($name, $type, $file)
  283. {
  284. // remove the migration just run
  285. \DB::delete(static::$table)
  286. ->where('name', $name)
  287. ->where('type', $type)
  288. ->where('migration', $file)
  289. ->execute();
  290. // remove the file from the list of run migrations
  291. if (($key = array_search($file, static::$migrations[$type][$name])) !== false)
  292. {
  293. unset(static::$migrations[$type][$name][$key]);
  294. }
  295. // make sure the migrations are in the correct order
  296. sort(static::$migrations[$type][$name]);
  297. // and save the update to the config file
  298. \Config::set('migrations.version.'.$type.'.'.$name, static::$migrations[$type][$name]);
  299. \Config::save(\Fuel::$env.DS.'migrations', 'migrations');
  300. }
  301. /**
  302. * migrate down to the previous version
  303. *
  304. * @param string name of the package, module or app
  305. * @param string type of migration (package, module or app)
  306. * @param mixed version to start migrations from, or null to start at the beginning
  307. * @param mixed version to end migrations by, or null to migrate to the end
  308. *
  309. * @return array
  310. */
  311. protected static function find_migrations($name, $type, $start = null, $end = null, $direction = 'up')
  312. {
  313. // Load all *_*.php files in the migrations path
  314. $method = '_find_'.$type;
  315. if ( ! $files = static::$method($name))
  316. {
  317. return array();
  318. }
  319. // get the currently installed migrations from the DB
  320. $current = \Arr::get(static::$migrations, $type.'.'.$name, array());
  321. // storage for the result
  322. $migrations = array();
  323. // normalize start and end values
  324. if ( ! is_null($start))
  325. {
  326. // if we have a prefix, use that
  327. ($pos = strpos($start, '_')) === false or $start = ltrim(substr($start, 0, $pos), '0');
  328. is_numeric($start) and $start = (int) $start;
  329. }
  330. if ( ! is_null($end))
  331. {
  332. // if we have a prefix, use that
  333. ($pos = strpos($end, '_')) === false or $end = ltrim(substr($end, 0, $pos), '0');
  334. is_numeric($end) and $end = (int) $end;
  335. }
  336. // filter the migrations out of bounds
  337. foreach ($files as $file)
  338. {
  339. // get the version for this migration and normalize it
  340. $migration = basename($file);
  341. ($pos = strpos($migration, '_')) === false or $migration = ltrim(substr($migration, 0, $pos), '0');
  342. is_numeric($migration) and $migration = (int) $migration;
  343. // add the file to the migrations list if it's in between version bounds
  344. if ((is_null($start) or $migration > $start) and (is_null($end) or $migration <= $end))
  345. {
  346. // see if it is already installed
  347. if ( in_array(basename($file, '.php'), $current))
  348. {
  349. // already installed. store it only if we're going down
  350. $direction == 'down' and $migrations[$migration] = array('path' => $file);
  351. }
  352. else
  353. {
  354. // not installed yet. store it only if we're going up
  355. $direction == 'up' and $migrations[$migration] = array('path' => $file);
  356. }
  357. }
  358. }
  359. // We now prepare to actually DO the migrations
  360. // But first let's make sure that everything is the way it should be
  361. foreach ($migrations as $ver => $migration)
  362. {
  363. // get the migration filename from the path
  364. $migration['file'] = basename($migration['path']);
  365. // make sure the migration filename has a valid format
  366. if (preg_match('/^.*?_(.*).php$/', $migration['file'], $match))
  367. {
  368. // determine the classname for this migration
  369. $class_name = ucfirst(strtolower($match[1]));
  370. // load the file and determiine the classname
  371. include $migration['path'];
  372. $class = static::$prefix.$class_name;
  373. // make sure it exists in the migration file loaded
  374. if ( ! class_exists($class, false))
  375. {
  376. throw new FuelException(sprintf('Migration "%s" does not contain expected class "%s"', $migration['path'], $class));
  377. }
  378. // and that it contains an "up" and "down" method
  379. if ( ! is_callable(array($class, 'up')) or ! is_callable(array($class, 'down')))
  380. {
  381. throw new FuelException(sprintf('Migration class "%s" must include public methods "up" and "down"', $name));
  382. }
  383. $migrations[$ver]['class'] = $class;
  384. }
  385. else
  386. {
  387. throw new FuelException(sprintf('Invalid Migration filename "%s"', $migration['path']));
  388. }
  389. }
  390. // make sure the result is sorted properly with all version types
  391. uksort($migrations, 'strnatcasecmp');
  392. return $migrations;
  393. }
  394. /**
  395. * finds migrations for the given app
  396. *
  397. * @param string name of the app (not used at the moment)
  398. *
  399. * @return array
  400. */
  401. protected static function _find_app($name = null)
  402. {
  403. return glob(APPPATH.\Config::get('migrations.folder').'*_*.php');
  404. }
  405. /**
  406. * finds migrations for the given module (or all if name is not given)
  407. *
  408. * @param string name of the module
  409. *
  410. * @return array
  411. */
  412. protected static function _find_module($name = null)
  413. {
  414. if ($name)
  415. {
  416. // find a module
  417. foreach (\Config::get('module_paths') as $m)
  418. {
  419. $files = glob($m .$name.'/'.\Config::get('migrations.folder').'*_*.php');
  420. if (count($files))
  421. {
  422. break;
  423. }
  424. }
  425. }
  426. else
  427. {
  428. // find all modules
  429. $files = array();
  430. foreach (\Config::get('module_paths') as $m)
  431. {
  432. $files = array_merge($files, glob($m.'*/'.\Config::get('migrations.folder').'*_*.php'));
  433. }
  434. }
  435. return $files;
  436. }
  437. /**
  438. * finds migrations for the given package (or all if name is not given)
  439. *
  440. * @param string name of the package
  441. *
  442. * @return array
  443. */
  444. protected static function _find_package($name = null)
  445. {
  446. $files = array();
  447. if ($name)
  448. {
  449. // find a package
  450. foreach (\Config::get('package_paths', array(PKGPATH)) as $p)
  451. {
  452. $files = glob($p .$name.'/'.\Config::get('migrations.folder').'*_*.php');
  453. if (count($files))
  454. {
  455. break;
  456. }
  457. }
  458. }
  459. else
  460. {
  461. // find all packages
  462. foreach (\Config::get('package_paths', array(PKGPATH)) as $p)
  463. {
  464. $files = array_merge($files, glob($p.'*/'.\Config::get('migrations.folder').'*_*.php'));
  465. }
  466. }
  467. return $files;
  468. }
  469. /**
  470. * installs or upgrades the migration table to the current schema
  471. *
  472. * @return void
  473. *
  474. * @deprecated Remove upgrade check in 1.3
  475. */
  476. protected static function table_version_check()
  477. {
  478. // if table does not exist
  479. if ( ! \DBUtil::table_exists(static::$table))
  480. {
  481. // create table
  482. \DBUtil::create_table(static::$table, static::$table_definition);
  483. }
  484. // check if a table upgrade is needed
  485. elseif ( ! \DBUtil::field_exists(static::$table, array('migration')))
  486. {
  487. // get the current migration status
  488. $current = \DB::select()->from(static::$table)->order_by('type', 'ASC')->order_by('name', 'ASC')->execute()->as_array();
  489. // drop the existing table, and recreate it in the new layout
  490. \DBUtil::drop_table(static::$table);
  491. \DBUtil::create_table(static::$table, static::$table_definition);
  492. // check if we had a current migration status
  493. if ( ! empty($current))
  494. {
  495. // do we need to migrate from a v1.0 migration environment?
  496. if (isset($current[0]['current']))
  497. {
  498. // convert the current result into a v1.1. migration environment structure
  499. $current = array(0 => array('name' => 'default', 'type' => 'app', 'version' => $current[0]['current']));
  500. }
  501. // build a new config structure
  502. $configs = array();
  503. // convert the v1.1 structure to the v1.2 structure
  504. foreach ($current as $migration)
  505. {
  506. // find the migrations for this entry
  507. $migrations = static::find_migrations($migration['name'], $migration['type'], null, $migration['version']);
  508. // array to keep track of the migrations already run
  509. $config = array();
  510. // add the individual migrations found
  511. foreach ($migrations as $file)
  512. {
  513. $file = pathinfo($file['path']);
  514. // add this migration to the table
  515. \DB::insert(static::$table)->set(array(
  516. 'name' => $migration['name'],
  517. 'type' => $migration['type'],
  518. 'migration' => $file['filename'],
  519. ))->execute();
  520. // and to the config
  521. $config[] = $file['filename'];
  522. }
  523. // create a config entry for this name and type if needed
  524. isset($configs[$migration['type']]) or $configs[$migration['type']] = array();
  525. $configs[$migration['type']][$migration['name']] = $config;
  526. }
  527. // write the updated migrations config back
  528. \Config::set('migrations.version', $configs);
  529. \Config::save(\Fuel::$env.DS.'migrations', 'migrations');
  530. }
  531. }
  532. // delete any old migration config file that may exist
  533. file_exists(APPPATH.'config'.DS.'migrations.php') and unlink(APPPATH.'config'.DS.'migrations.php');
  534. }
  535. }