PageRenderTime 48ms CodeModel.GetById 9ms RepoModel.GetById 1ms app.codeStats 0ms

/bonfire/application/core_modules/migrations/libraries/Migrations.php

https://github.com/carlosezeta/Bonfire
PHP | 744 lines | 350 code | 123 blank | 271 comment | 54 complexity | 26bf1fb838c727f5dae5619240faf405 MD5 | raw file
Possible License(s): LGPL-2.1
  1. <?php defined('BASEPATH') OR exit('No direct script access allowed');
  2. /**
  3. * Migrations Library
  4. *
  5. * Migrations provide a simple method to version the contents of your database, and make
  6. * those changes easily distributable to other developers in different server environments.
  7. *
  8. * Migrations are stored in specially-named PHP files under *bonfire/application/db/migrations/*.
  9. * Each migration file must be numbered consecutively, starting at *001* and growing larger
  10. * with each new migration from there. The rest of the filename should tell what the migration does.
  11. *
  12. * For example: 001_install_initial_schema.php
  13. *
  14. * The class inside of the file must extend the abstract Migration class and implement both the
  15. * up() and down() methods to install and uninstall the tables/changes. The class itself should be
  16. * named:
  17. *
  18. * : class Migration_install_initial_schema extends Migration {
  19. * :
  20. * : function up() {}
  21. * :
  22. * : function down() {}
  23. * : }
  24. *
  25. * @package Bonfire
  26. * @author Mat�as Montes
  27. * @author Phil Sturgeon http://philsturgeon.co.uk/
  28. * @author Spicer Matthews <spicer@cloudmanic.com> Cloudmanic Labs, LLC http://www.cloudmanic.com/
  29. * @author Bonfire Dev Team
  30. */
  31. // ------------------------------------------------------------------------
  32. /**
  33. * Migration Interface
  34. *
  35. * All migrations should implement this, forces up() and down() and gives
  36. * access to the CI super-global.
  37. *
  38. * @package Bonfire
  39. * @subpackage Modules_Migrations
  40. * @category Libraries
  41. * @abstract
  42. * @author Phil Sturgeon http://philsturgeon.co.uk/
  43. * @link http://guides.cibonfire.com/helpers/file_helpers.html
  44. */
  45. abstract class Migration
  46. {
  47. /**
  48. * The type of migration being ran, either 'forge' or 'sql'.
  49. *
  50. * @var string
  51. */
  52. public $migration_type = 'forge';
  53. //--------------------------------------------------------------------
  54. /**
  55. * Abstract method ran when increasing the schema version. Typically installs
  56. * new data to the database or creates new tables.
  57. *
  58. * @access public
  59. * @abstract
  60. */
  61. public abstract function up();
  62. /**
  63. * Abstract method ran when decreasing the schema version.
  64. *
  65. * @access public
  66. * @abstract
  67. */
  68. public abstract function down();
  69. //--------------------------------------------------------------------
  70. /**
  71. * Getter method
  72. *
  73. * @param mixed $var
  74. *
  75. * @return object
  76. */
  77. function __get($var)
  78. {
  79. return get_instance()->$var;
  80. }//end __get()
  81. //--------------------------------------------------------------------
  82. }//end Migration
  83. // ------------------------------------------------------------------------
  84. /**
  85. * Migrations Library
  86. *
  87. * Utility main controller.
  88. *
  89. * @package Bonfire
  90. * @subpackage Modules_Migrations
  91. * @category Libraries
  92. * @author Mat�as Montes
  93. * @link http://guides.cibonfire.com/helpers/file_helpers.html
  94. *
  95. */
  96. class Migrations
  97. {
  98. /**
  99. * Enable or disable the migrations functionality
  100. *
  101. * @access private
  102. *
  103. * @var bool
  104. */
  105. private $migrations_enabled = FALSE;
  106. /**
  107. * Path to the migrations files
  108. *
  109. * @access private
  110. *
  111. * @var string
  112. */
  113. private $migrations_path = ".";
  114. /**
  115. * Show verbose messages or not
  116. *
  117. * @access private
  118. *
  119. * @var bool
  120. */
  121. private $verbose = FALSE;
  122. /**
  123. * Error message
  124. *
  125. * @access public
  126. *
  127. * @var string
  128. */
  129. public $error = "";
  130. //--------------------------------------------------------------------
  131. /**
  132. * Initialize the configuration settings
  133. *
  134. * @return void
  135. */
  136. function __construct()
  137. {
  138. $this->_ci =& get_instance();
  139. $this->_ci->config->load('migrations/migrations');
  140. $this->migrations_enabled = $this->_ci->config->item('migrations_enabled');
  141. $this->migrations_path = realpath($this->_ci->config->item('migrations_path'));
  142. // Idiot check
  143. $this->migrations_enabled AND $this->migrations_path OR show_error('Migrations has been loaded but is disabled or set up incorrectly.');
  144. // If not set, set it
  145. if ($this->migrations_path == '')
  146. {
  147. $this->migrations_path = APPPATH . 'migrations/';
  148. }
  149. // Add trailing slash if not set
  150. else if (substr($this->migrations_path, -1) != '/')
  151. {
  152. $this->migrations_path .= '/';
  153. }
  154. $this->_ci->load->dbforge();
  155. $this->_ci->lang->load('migrations/migrations');
  156. // If the schema_version table is missing, make it
  157. if ( ! $this->_ci->db->table_exists('schema_version'))
  158. {
  159. $this->_ci->dbforge->add_field(array(
  160. 'type' => array('type' => 'VARCHAR', 'constraint' => 20, 'null' => FALSE),
  161. 'version' => array('type' => 'INT', 'constraint' => 4, 'default' => 0),
  162. ));
  163. $this->_ci->dbforge->add_key('type', TRUE);
  164. $this->_ci->dbforge->create_table('schema_version', TRUE);
  165. $this->_ci->db->insert('schema_version', array('type' => 'core', 'version' => 0));
  166. }
  167. // Make sure out application helper is loaded.
  168. if (!function_exists('logit'))
  169. {
  170. $this->_ci->load->helper('application');
  171. }
  172. }//end __construct()
  173. //--------------------------------------------------------------------
  174. /**
  175. * This will set if there should be verbose output or not
  176. *
  177. * @access public
  178. *
  179. * @param bool $state TRUE/FALSE
  180. */
  181. public function set_verbose($state)
  182. {
  183. $this->verbose = $state;
  184. }//end set_verbose()
  185. //--------------------------------------------------------------------
  186. /**
  187. * Installs the schema up to the last version
  188. *
  189. * @param string $type A string that represents the name of the module, or 'app_' for application migrations. If empty, it returns core migrations.
  190. *
  191. * @return int The version number this type is at
  192. */
  193. public function install($type='')
  194. {
  195. $migrations_path = $type == 'app_' ? $this->migrations_path : $this->migrations_path .'core/';
  196. // Load all *_*.php files in the migrations path
  197. $files = glob($migrations_path.'*_*'.EXT);
  198. $file_count = count($files);
  199. for($i=0; $i < $file_count; $i++)
  200. {
  201. // Mark wrongly formatted files as FALSE for later filtering
  202. $name = basename($files[$i],EXT);
  203. if(!preg_match('/^\d{3}_(\w+)$/',$name)) $files[$i] = FALSE;
  204. }
  205. $migrations = array_filter($files);
  206. if ( ! empty($migrations))
  207. {
  208. sort($migrations);
  209. $last_migration = basename(end($migrations));
  210. // Calculate the last migration step from existing migration
  211. // filenames and procceed to the standard version migration
  212. $last_version = substr($last_migration,0,3);
  213. return $this->version(intval($last_version,10), $type);
  214. }
  215. else
  216. {
  217. $this->error = $this->_ci->lang->line('no_migrations_found');
  218. return 0;
  219. }
  220. }//end install()
  221. // --------------------------------------------------------------------
  222. /**
  223. * Migrate to a schema version.
  224. *
  225. * Calls each migration step required to get to the schema version of
  226. * choice.
  227. *
  228. * @access public
  229. *
  230. * @param int $version An int that is the target version to migrate to.
  231. * @param string $type A string that represents the name of the module, or 'app_' for application migrations. If empty, it returns core migrations.
  232. *
  233. * @return mixed TRUE if already latest, FALSE if failed, int if upgraded
  234. */
  235. function version($version, $type='')
  236. {
  237. $schema_version = $this->get_schema_version($type);
  238. $start = $schema_version;
  239. $stop = $version;
  240. switch ($type)
  241. {
  242. case '':
  243. $migrations_path = $this->migrations_path .'core/';
  244. break;
  245. case 'app_':
  246. $migrations_path = $this->migrations_path;
  247. break;
  248. default:
  249. $migrations_path = module_path(substr($type, 0, -1), 'migrations') .'/';
  250. break;
  251. }
  252. if ($version > $schema_version)
  253. {
  254. // Moving Up
  255. $start++;
  256. $stop++;
  257. $step = 1;
  258. }
  259. else
  260. {
  261. // Moving Down
  262. $step = -1;
  263. }
  264. $method = $step == 1 ? 'up' : 'down';
  265. $migrations = array();
  266. // We now prepare to actually DO the migrations
  267. // But first let's make sure that everything is the way it should be
  268. for($i=$start; $i != $stop; $i += $step)
  269. {
  270. $f = glob(sprintf($migrations_path . '%03d_*'.EXT, $i));
  271. logit($f);
  272. // Only one migration per step is permitted
  273. if (count($f) > 1)
  274. {
  275. $this->error = sprintf($this->_ci->lang->line("multiple_migrations_version"),$i);
  276. return 0;
  277. }
  278. // Migration step not found
  279. if (count($f) == 0)
  280. {
  281. // If trying to migrate up to a version greater than the last
  282. // existing one, migrate to the last one.
  283. if ($step == 1)
  284. {
  285. break;
  286. }
  287. // If trying to migrate down but we're missing a step,
  288. // something must definitely be wrong.
  289. $this->error = sprintf($this->_ci->lang->line("migration_not_found"),$i);
  290. return 0;
  291. }
  292. $file = basename($f[0]);
  293. $name = basename($f[0],EXT);
  294. // Filename validations
  295. if (preg_match('/^\d{3}_(\w+)$/', $name, $match))
  296. {
  297. $match[1] = strtolower($match[1]);
  298. // Cannot repeat a migration at different steps
  299. if (in_array($match[1], $migrations))
  300. {
  301. $this->error = sprintf($this->_ci->lang->line("multiple_migrations_name"),$match[1]);
  302. return 0;
  303. }
  304. include $f[0];
  305. $class = 'Migration_'.ucfirst($match[1]);
  306. if ( ! class_exists($class))
  307. {
  308. $this->error = sprintf($this->_ci->lang->line("mig_class_doesnt_exist"), $class);
  309. return 0;
  310. }
  311. if ( ! is_callable(array($class,"up")) || ! is_callable(array($class,"down"))) {
  312. $this->error = sprintf($this->_ci->lang->line('wrong_migration_interface'),$class);
  313. return 0;
  314. }
  315. $migrations[] = $match[1];
  316. }
  317. else
  318. {
  319. $this->error = sprintf($this->_ci->lang->line("invalid_migration_filename"),$file, $migrations_path);
  320. return 0;
  321. }//end if
  322. }//end for
  323. $version = $i + ($step == 1 ? -1 : 0);
  324. // If there is nothing to do, bitch and quit
  325. if ($migrations === array())
  326. {
  327. if ($this->verbose)
  328. {
  329. echo "Nothing to do, bye!\n";
  330. }
  331. return TRUE;
  332. }
  333. if ($this->verbose)
  334. {
  335. echo "<p>Current schema version: ".$schema_version."<br/>";
  336. echo "Moving ".$method." to version ".$version."</p>";
  337. echo "<hr/>";
  338. }
  339. // Loop through the migrations
  340. foreach($migrations AS $m)
  341. {
  342. if ($this->verbose)
  343. {
  344. echo "$m:<br />";
  345. echo "<blockquote>";
  346. }
  347. $class = 'Migration_'.ucfirst($m);
  348. $c = new $class;
  349. if ($c->migration_type == 'forge')
  350. {
  351. call_user_func(array($c, $method));
  352. }
  353. else if ($c->migration_type == 'sql')
  354. {
  355. $sql = $c->$method();
  356. $this->do_sql_migration($sql);
  357. }
  358. if ($this->verbose)
  359. {
  360. echo "</blockquote>";
  361. echo "<hr/>";
  362. }
  363. $schema_version += $step;
  364. $this->_update_schema_version($schema_version, $type);
  365. }//end foreach
  366. if ($this->verbose)
  367. {
  368. echo "<p>All done. Schema is at version $schema_version.</p>";
  369. }
  370. return $schema_version;
  371. }//end version()
  372. // --------------------------------------------------------------------
  373. /**
  374. * Handles auto-upgrading migrations of core and/or app on page load.
  375. *
  376. * @access public
  377. *
  378. * @return void
  379. */
  380. public function auto_latest()
  381. {
  382. $auto_core = $this->_ci->config->item('migrate.auto_core');
  383. $auto_app = $this->_ci->config->item('migrate.auto_app');
  384. if ($auto_core)
  385. {
  386. $this->version($this->get_latest_version(''), '');
  387. }
  388. if ($auto_app)
  389. {
  390. $this->version($this->get_latest_version('app_'), 'app_');
  391. }
  392. }//end auto_latest()
  393. //--------------------------------------------------------------------
  394. //--------------------------------------------------------------------
  395. // ! Utility Methods
  396. //--------------------------------------------------------------------
  397. /**
  398. * Set's the schema to the latest migration
  399. *
  400. * @access public
  401. *
  402. * @return mixed TRUE if already latest, FALSE if failed, int if upgraded
  403. */
  404. public function latest()
  405. {
  406. $version = $this->_ci->config->item('migrations_version');
  407. return $this->version($version);
  408. }//end latest()
  409. // --------------------------------------------------------------------
  410. /**
  411. * Retrieves current schema version
  412. *
  413. * @access public
  414. *
  415. * @param string $type A string that represents the name of the module, or 'app_' for application migrations. If empty, it returns core migrations.
  416. *
  417. * @return int Current Schema version
  418. */
  419. public function get_schema_version($type='')
  420. {
  421. if ($this->_check_migrations_column('type'))
  422. {
  423. // new schema table layout
  424. $type = empty($type) ? 'core' : $type;
  425. $row = $this->_ci->db->get_where('schema_version', array('type' => $type))->row();
  426. return isset($row->version) ? $row->version: 0;
  427. }
  428. else
  429. {
  430. $row = $this->_ci->db->get('schema_version')->row();
  431. $schema = $type .'version';
  432. return isset($row->$schema) ? $row->$schema : 0;
  433. }
  434. }//end latest()
  435. // --------------------------------------------------------------------
  436. /**
  437. * Retrieves the latest available version.
  438. *
  439. * @param string $type A string that represents the name of the module, or 'app_' for application migrations. If empty, it returns core migrations.
  440. *
  441. * @return int Latest available migration file.
  442. */
  443. public function get_latest_version($type='')
  444. {
  445. switch ($type)
  446. {
  447. case '':
  448. $migrations_path = $this->migrations_path .'core/';
  449. break;
  450. case 'app_':
  451. $migrations_path = $this->migrations_path;
  452. break;
  453. default:
  454. $migrations_path = module_path(substr($type, 0, -1), 'migrations') .'/';
  455. break;
  456. }
  457. $f = glob($migrations_path .'*_*'.EXT);
  458. return count($f);
  459. }//end get_latest_version()
  460. //--------------------------------------------------------------------
  461. /**
  462. * Searches the migrations folder and returns a list of available migration files.
  463. *
  464. * @access public
  465. *
  466. * @param string $type A string that represents the name of the module, or 'app_' for application migrations. If empty, it returns core migrations.
  467. *
  468. * @return array An array of migration files
  469. */
  470. public function get_available_versions($type='')
  471. {
  472. switch ($type)
  473. {
  474. case '':
  475. $migrations_path = $this->migrations_path .'core/';
  476. break;
  477. case 'app_':
  478. $migrations_path = $this->migrations_path;
  479. break;
  480. default:
  481. $migrations_path = module_path(substr($type, 0, -1), 'migrations') .'/';
  482. break;
  483. }
  484. $files = glob($migrations_path .'*_*'.EXT);
  485. for ($i=0; $i < count($files); $i++)
  486. {
  487. $files[$i] = str_ireplace($migrations_path, '', $files[$i]);
  488. }
  489. return $files;
  490. }//end get_available_versions()
  491. //--------------------------------------------------------------------
  492. /**
  493. * Executes raw SQL migrations. Will manually break the commands on a ';' so
  494. * that multiple commmands can be run at once. Very handy for using phpMyAdmin
  495. * dumps.
  496. *
  497. * @access public
  498. *
  499. * @param string $sql A string with one or more SQL commands to be run.
  500. *
  501. * @return void
  502. */
  503. public function do_sql_migration($sql='')
  504. {
  505. if (empty($sql))
  506. {
  507. return;
  508. }
  509. // Split the sql into usable commands on ';'
  510. $queries = explode(';', $sql);
  511. foreach ($queries as $q)
  512. {
  513. if (trim($q))
  514. {
  515. $this->_ci->db->query(trim($q));
  516. }
  517. }
  518. }//end do_sql_migration()
  519. //--------------------------------------------------------------------
  520. /**
  521. * Stores the current schema version in the database.
  522. *
  523. * @access private
  524. *
  525. * @param int $schema_version An integer with the latest Schema version reached
  526. * @param string $type A string that is appended with '_schema' to create the field name to store in the database.
  527. *
  528. * @return void
  529. */
  530. private function _update_schema_version($schema_version, $type='')
  531. {
  532. logit('[Migrations] Schema '. $type.' updated to: '. $schema_version);
  533. if ($this->_check_migrations_column('type'))
  534. {
  535. // new schema table layout
  536. $type = empty($type) ? 'core' : $type;
  537. // If the row doesn't exist, create it...
  538. $query = $this->_ci->db->get_where('schema_version', array('type' => $type));
  539. if ($schema_version != 0)
  540. {
  541. if (!$query->num_rows())
  542. {
  543. $this->_ci->db->insert('schema_version', array(
  544. 'type' => $type,
  545. 'version' => $schema_version,
  546. ));
  547. }
  548. return $this->_ci->db->update('schema_version', array('version' => $schema_version), array('type' => $type));
  549. }
  550. elseif ($query->num_rows())
  551. {
  552. return $this->_ci->db->delete('schema_version', array('type' => $type));
  553. }
  554. }
  555. else
  556. {
  557. // If the row doesn't exist, create it...
  558. if (!$this->_check_migrations_column($type .'version'))
  559. {
  560. $this->_ci->load->dbforge();
  561. $this->_ci->dbforge->add_column('schema_version', array(
  562. $type .'version' => array(
  563. 'type' => 'INT',
  564. 'constraint' => 4,
  565. 'null' => true,
  566. 'default' => 0
  567. )
  568. ));
  569. }
  570. return $this->_ci->db->update('schema_version', array(
  571. $type.'version' => $schema_version
  572. ));
  573. }//end if
  574. }//end _update_schema_version()
  575. //--------------------------------------------------------------------
  576. /**
  577. * Method to check if the DB table schema_version is in the new format or old
  578. *
  579. * @access private
  580. *
  581. * @param string $column_name Name of the column to check the existance of
  582. *
  583. * @return bool
  584. */
  585. private function _check_migrations_column($column_name)
  586. {
  587. $row = $this->_ci->db->get('schema_version')->row();
  588. if (isset($row->$column_name))
  589. {
  590. return TRUE;
  591. }
  592. return FALSE;
  593. }//end _check_migrations_column()
  594. //--------------------------------------------------------------------
  595. /**
  596. * Sets the base path that Migrations uses to find it's migrations
  597. * to a user-supplied path. The path will be converted to a full
  598. * system path (via realpath) and checked to make sure it's a folder.
  599. *
  600. * @access public
  601. *
  602. * @param string $path The path to set, relative to the front controller.
  603. *
  604. * @return void
  605. */
  606. public function set_path($path=null)
  607. {
  608. if (empty($path))
  609. {
  610. return;
  611. }
  612. $path = realpath($path);
  613. if (is_dir($path))
  614. {
  615. $this->migrations_path = $path .'/';
  616. }
  617. }//end set_path()
  618. //--------------------------------------------------------------------
  619. }//end Migrations
  620. /* End of file Migrations.php */
  621. /* Location: ./libraries/Migrations.php */