PageRenderTime 51ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/fuel/core/classes/migrate.php

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