diff options
Diffstat (limited to 'system/libraries/Migration.php')
-rw-r--r-- | system/libraries/Migration.php | 400 |
1 files changed, 274 insertions, 126 deletions
diff --git a/system/libraries/Migration.php b/system/libraries/Migration.php index 241ce1e59..2a87d9d7c 100644 --- a/system/libraries/Migration.php +++ b/system/libraries/Migration.php @@ -1,19 +1,41 @@ -<?php defined('BASEPATH') OR exit('No direct script access allowed'); +<?php /** * CodeIgniter * - * An open source application development framework for PHP 5.1.6 or newer + * An open source application development framework for PHP * - * @package CodeIgniter - * @author EllisLab Dev Team - * @copyright Copyright (c) 2006 - 2014, EllisLab, Inc. - * @license http://codeigniter.com/user_guide/license.html - * @link http://codeigniter.com - * @since Version 1.0 + * This content is released under the MIT License (MIT) + * + * Copyright (c) 2014 - 2017, British Columbia Institute of Technology + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @package CodeIgniter + * @author EllisLab Dev Team + * @copyright Copyright (c) 2008 - 2014, EllisLab, Inc. (https://ellislab.com/) + * @copyright Copyright (c) 2014 - 2017, British Columbia Institute of Technology (http://bcit.ca/) + * @license http://opensource.org/licenses/MIT MIT License + * @link https://codeigniter.com + * @since Version 3.0.0 * @filesource */ - -// ------------------------------------------------------------------------ +defined('BASEPATH') OR exit('No direct script access allowed'); /** * Migration Class @@ -29,26 +51,82 @@ */ class CI_Migration { + /** + * Whether the library is enabled + * + * @var bool + */ protected $_migration_enabled = FALSE; + + /** + * Migration numbering type + * + * @var bool + */ + protected $_migration_type = 'sequential'; + + /** + * Path to migration classes + * + * @var string + */ protected $_migration_path = NULL; + + /** + * Current migration version + * + * @var mixed + */ protected $_migration_version = 0; + /** + * Database table with migration info + * + * @var string + */ + protected $_migration_table = 'migrations'; + + /** + * Whether to automatically run migrations + * + * @var bool + */ + protected $_migration_auto_latest = FALSE; + + /** + * Migration basename regex + * + * @var string + */ + protected $_migration_regex; + + /** + * Error message + * + * @var string + */ protected $_error_string = ''; + /** + * Initialize Migration Class + * + * @param array $config + * @return void + */ public function __construct($config = array()) { - # Only run this constructor on main library load - if (get_parent_class($this) !== FALSE) + // Only run this constructor on main library load + if ( ! in_array(get_class($this), array('CI_Migration', config_item('subclass_prefix').'Migration'), TRUE)) { return; } foreach ($config as $key => $val) { - $this->{'_' . $key} = $val; + $this->{'_'.$key} = $val; } - log_message('debug', 'Migrations class initialized'); + log_message('info', 'Migrations Class Initialized'); // Are they trying to use migrations while it is disabled? if ($this->_migration_enabled !== TRUE) @@ -57,7 +135,7 @@ class CI_Migration { } // If not set, set it - $this->_migration_path == '' AND $this->_migration_path = APPPATH . 'migrations/'; + $this->_migration_path !== '' OR $this->_migration_path = APPPATH.'migrations/'; // Add trailing slash if not set $this->_migration_path = rtrim($this->_migration_path, '/').'/'; @@ -68,16 +146,39 @@ class CI_Migration { // They'll probably be using dbforge $this->load->dbforge(); + // Make sure the migration table name was set. + if (empty($this->_migration_table)) + { + show_error('Migrations configuration file (migration.php) must have "migration_table" set.'); + } + + // Migration basename regex + $this->_migration_regex = ($this->_migration_type === 'timestamp') + ? '/^\d{14}_(\w+)$/' + : '/^\d{3}_(\w+)$/'; + + // Make sure a valid migration numbering type was set. + if ( ! in_array($this->_migration_type, array('sequential', 'timestamp'))) + { + show_error('An invalid migration numbering type was specified: '.$this->_migration_type); + } + // If the migrations table is missing, make it - if ( ! $this->db->table_exists('migrations')) + if ( ! $this->db->table_exists($this->_migration_table)) { $this->dbforge->add_field(array( - 'version' => array('type' => 'INT', 'constraint' => 3), + 'version' => array('type' => 'BIGINT', 'constraint' => 20), )); - $this->dbforge->create_table('migrations', TRUE); + $this->dbforge->create_table($this->_migration_table, TRUE); + + $this->db->insert($this->_migration_table, array('version' => 0)); + } - $this->db->insert('migrations', array('version' => 0)); + // Do we auto migrate to the latest migration? + if ($this->_migration_auto_latest === TRUE && ! $this->latest()) + { + show_error($this->error_string()); } } @@ -89,154 +190,166 @@ class CI_Migration { * Calls each migration step required to get to the schema version of * choice * - * @param int Target schema version - * @return mixed TRUE if already latest, FALSE if failed, int if upgraded + * @param string $target_version Target schema version + * @return mixed TRUE if no migrations are found, current version string on success, FALSE on failure */ public function version($target_version) { - $start = $current_version = $this->_get_version(); - $stop = $target_version; + // Note: We use strings, so that timestamp versions work on 32-bit systems + $current_version = $this->_get_version(); - if ($target_version > $current_version) + if ($this->_migration_type === 'sequential') { - // Moving Up - ++$start; - ++$stop; - $step = 1; + $target_version = sprintf('%03d', $target_version); } else { - // Moving Down - $step = -1; + $target_version = (string) $target_version; } - $method = ($step === 1) ? 'up' : 'down'; - $migrations = array(); + $migrations = $this->find_migrations(); - // We now prepare to actually DO the migrations - // But first let's make sure that everything is the way it should be - for ($i = $start; $i != $stop; $i += $step) + if ($target_version > 0 && ! isset($migrations[$target_version])) { - $f = glob(sprintf($this->_migration_path . '%03d_*.php', $i)); + $this->_error_string = sprintf($this->lang->line('migration_not_found'), $target_version); + return FALSE; + } - // Only one migration per step is permitted - if (count($f) > 1) - { - $this->_error_string = sprintf($this->lang->line('migration_multiple_version'), $i); - return FALSE; - } + if ($target_version > $current_version) + { + $method = 'up'; + } + elseif ($target_version < $current_version) + { + $method = 'down'; + // We need this so that migrations are applied in reverse order + krsort($migrations); + } + else + { + // Well, there's nothing to migrate then ... + return TRUE; + } - // Migration step not found - if (count($f) == 0) + // Validate all available migrations within our target range. + // + // Unfortunately, we'll have to use another loop to run them + // in order to avoid leaving the procedure in a broken state. + // + // See https://github.com/bcit-ci/CodeIgniter/issues/4539 + $pending = array(); + foreach ($migrations as $number => $file) + { + // Ignore versions out of our range. + // + // Because we've previously sorted the $migrations array depending on the direction, + // we can safely break the loop once we reach $target_version ... + if ($method === 'up') { - // If trying to migrate up to a version greater than the last - // existing one, migrate to the last one. - if ($step == 1) + if ($number <= $current_version) + { + continue; + } + elseif ($number > $target_version) { break; } - - // If trying to migrate down but we're missing a step, - // something must definitely be wrong. - $this->_error_string = sprintf($this->lang->line('migration_not_found'), $i); - return FALSE; } - - $file = basename($f[0]); - $name = basename($f[0], '.php'); - - // Filename validations - if (preg_match('/^\d{3}_(\w+)$/', $name, $match)) + else { - $match[1] = strtolower($match[1]); - - // Cannot repeat a migration at different steps - if (in_array($match[1], $migrations)) + if ($number > $current_version) { - $this->_error_string = sprintf($this->lang->line('migration_multiple_version'), $match[1]); - return FALSE; + continue; } - - include $f[0]; - $class = 'Migration_' . ucfirst($match[1]); - - if ( ! class_exists($class)) + elseif ($number <= $target_version) { - $this->_error_string = sprintf($this->lang->line('migration_class_doesnt_exist'), $class); - return FALSE; + break; } + } - if ( ! is_callable(array($class, $method))) + // Check for sequence gaps + if ($this->_migration_type === 'sequential') + { + if (isset($previous) && abs($number - $previous) > 1) { - $this->_error_string = sprintf($this->lang->line('migration_missing_'.$method.'_method'), $class); + $this->_error_string = sprintf($this->lang->line('migration_sequence_gap'), $number); return FALSE; } - $migrations[] = $match[1]; + $previous = $number; } - else + + include_once($file); + $class = 'Migration_'.ucfirst(strtolower($this->_get_migration_name(basename($file, '.php')))); + + // Validate the migration file structure + if ( ! class_exists($class, FALSE)) { - $this->_error_string = sprintf($this->lang->line('migration_invalid_filename'), $file); + $this->_error_string = sprintf($this->lang->line('migration_class_doesnt_exist'), $class); + return FALSE; + } + elseif ( ! is_callable(array($class, $method))) + { + $this->_error_string = sprintf($this->lang->line('migration_missing_'.$method.'_method'), $class); return FALSE; } - } - - log_message('debug', 'Current migration: ' . $current_version); - $version = $i + ($step == 1 ? -1 : 0); + $pending[$number] = array($class, $method); + } - // If there is nothing to do so quit - if ($migrations === array()) + // Now just run the necessary migrations + foreach ($pending as $number => $migration) { - return TRUE; - } + log_message('debug', 'Migrating '.$method.' from version '.$current_version.' to version '.$number); - log_message('debug', 'Migrating from ' . $method . ' to version ' . $version); + $migration[0] = new $migration[0]; + call_user_func($migration); + $current_version = $number; + $this->_update_version($current_version); + } - // Loop through the migrations - foreach ($migrations AS $migration) + // This is necessary when moving down, since the the last migration applied + // will be the down() method for the next migration up from the target + if ($current_version <> $target_version) { - // Run the migration class - $class = 'Migration_' . ucfirst(strtolower($migration)); - call_user_func(array(new $class, $method)); - - $current_version += $step; + $current_version = $target_version; $this->_update_version($current_version); } log_message('debug', 'Finished migrating to '.$current_version); - return $current_version; } // -------------------------------------------------------------------- /** - * Set's the schema to the latest migration + * Sets the schema to the latest migration * - * @return mixed true if already latest, false if failed, int if upgraded + * @return mixed Current version string on success, FALSE on failure */ public function latest() { - if ( ! $migrations = $this->find_migrations()) + $migrations = $this->find_migrations(); + + if (empty($migrations)) { $this->_error_string = $this->lang->line('migration_none_found'); - return false; + return FALSE; } $last_migration = basename(end($migrations)); // Calculate the last migration step from existing migration - // filenames and procceed to the standard version migration - return $this->version((int) substr($last_migration, 0, 3)); + // filenames and proceed to the standard version migration + return $this->version($this->_get_migration_number($last_migration)); } // -------------------------------------------------------------------- /** - * Set's the schema to the migration version set in config + * Sets the schema to the migration version set in config * - * @return mixed true if already current, false if failed, int if upgraded + * @return mixed TRUE if no migrations are found, current version string on success, FALSE on failure */ public function current() { @@ -258,28 +371,66 @@ class CI_Migration { // -------------------------------------------------------------------- /** - * Set's the schema to the latest migration + * Retrieves list of available migration scripts * - * @return mixed true if already latest, false if failed, int if upgraded + * @return array list of migration file paths sorted by version */ - protected function find_migrations() + public function find_migrations() { - // Load all *_*.php files in the migrations path - $files = glob($this->_migration_path . '*_*.php'); - $file_count = count($files); + $migrations = array(); - for ($i = 0; $i < $file_count; $i++) + // Load all *_*.php files in the migrations path + foreach (glob($this->_migration_path.'*_*.php') as $file) { - // Mark wrongly formatted files as false for later filtering - $name = basename($files[$i], '.php'); - if ( ! preg_match('/^\d{3}_(\w+)$/', $name)) + $name = basename($file, '.php'); + + // Filter out non-migration files + if (preg_match($this->_migration_regex, $name)) { - $files[$i] = FALSE; + $number = $this->_get_migration_number($name); + + // There cannot be duplicate migration numbers + if (isset($migrations[$number])) + { + $this->_error_string = sprintf($this->lang->line('migration_multiple_version'), $number); + show_error($this->_error_string); + } + + $migrations[$number] = $file; } } - sort($files); - return $files; + ksort($migrations); + return $migrations; + } + + // -------------------------------------------------------------------- + + /** + * Extracts the migration number from a filename + * + * @param string $migration + * @return string Numeric portion of a migration filename + */ + protected function _get_migration_number($migration) + { + return sscanf($migration, '%[0-9]+', $number) + ? $number : '0'; + } + + // -------------------------------------------------------------------- + + /** + * Extracts the migration class name from a filename + * + * @param string $migration + * @return string text portion of a migration filename + */ + protected function _get_migration_name($migration) + { + $parts = explode('_', $migration); + array_shift($parts); + return implode('_', $parts); } // -------------------------------------------------------------------- @@ -287,12 +438,12 @@ class CI_Migration { /** * Retrieves current schema version * - * @return int Current Migration + * @return string Current migration version */ protected function _get_version() { - $row = $this->db->get('migrations')->row(); - return $row ? $row->version : 0; + $row = $this->db->select('version')->get($this->_migration_table)->row(); + return $row ? $row->version : '0'; } // -------------------------------------------------------------------- @@ -300,13 +451,13 @@ class CI_Migration { /** * Stores the current schema version * - * @param int Migration reached - * @return bool + * @param string $migration Migration reached + * @return void */ - protected function _update_version($migrations) + protected function _update_version($migration) { - return $this->db->update('migrations', array( - 'version' => $migrations + $this->db->update($this->_migration_table, array( + 'version' => $migration )); } @@ -315,7 +466,7 @@ class CI_Migration { /** * Enable the use of CI super-global * - * @param mixed $var + * @param string $var * @return mixed */ public function __get($var) @@ -324,6 +475,3 @@ class CI_Migration { } } - -/* End of file Migration.php */ -/* Location: ./system/libraries/Migration.php */
\ No newline at end of file |