PageRenderTime 42ms CodeModel.GetById 12ms RepoModel.GetById 0ms app.codeStats 1ms

/classes/migrate.php

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