You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
739 lines
20 KiB
739 lines
20 KiB
<?php
|
|
/**
|
|
* Part of the Fuel framework.
|
|
*
|
|
* @package Fuel
|
|
* @version 1.8
|
|
* @author Fuel Development Team
|
|
* @license MIT License
|
|
* @copyright 2010 - 2016 Fuel Development Team
|
|
* @link http://fuelphp.com
|
|
*/
|
|
|
|
namespace Fuel\Core;
|
|
|
|
/**
|
|
* Migrate Class
|
|
*
|
|
* @package Fuel
|
|
* @category Migrations
|
|
* @link http://docs.fuelphp.com/classes/migrate.html
|
|
*/
|
|
class Migrate
|
|
{
|
|
/**
|
|
* @var array current migrations registered in the database
|
|
*/
|
|
protected static $migrations = array();
|
|
|
|
/**
|
|
* @var string migration classes namespace prefix
|
|
*/
|
|
protected static $prefix = 'Fuel\\Migrations\\';
|
|
|
|
/**
|
|
* @var string name of the migration table
|
|
*/
|
|
protected static $table = 'migration';
|
|
|
|
/**
|
|
* @var string database connection group
|
|
*/
|
|
protected static $connection = null;
|
|
|
|
/**
|
|
* @var array migration table schema
|
|
*/
|
|
protected static $table_definition = array(
|
|
'type' => array('type' => 'varchar', 'constraint' => 25),
|
|
'name' => array('type' => 'varchar', 'constraint' => 50),
|
|
'migration' => array('type' => 'varchar', 'constraint' => 100, 'null' => false, 'default' => ''),
|
|
);
|
|
|
|
/**
|
|
* loads in the migrations config file, checks to see if the migrations
|
|
* table is set in the database (if not, create it), and reads in all of
|
|
* the versions from the DB.
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function _init()
|
|
{
|
|
logger(\Fuel::L_DEBUG, 'Migrate class initialized');
|
|
|
|
// load the migrations config
|
|
\Config::load('migrations', true);
|
|
|
|
// set the name of the table containing the installed migrations
|
|
static::$table = \Config::get('migrations.table', static::$table);
|
|
|
|
// set the name of the connection group to use
|
|
static::$connection = \Config::get('migrations.connection', static::$connection);
|
|
|
|
// installs or upgrades the migration table to the current schema
|
|
static::table_version_check();
|
|
|
|
//get all installed migrations from db
|
|
$migrations = \DB::select()
|
|
->from(static::$table)
|
|
->order_by('type', 'ASC')
|
|
->order_by('name', 'ASC')
|
|
->order_by('migration', 'ASC')
|
|
->execute(static::$connection)
|
|
->as_array();
|
|
|
|
foreach($migrations as $migration)
|
|
{
|
|
// convert the db migrations to match the config file structure
|
|
isset(static::$migrations[$migration['type']]) or static::$migrations[$migration['type']] = array();
|
|
static::$migrations[$migration['type']][$migration['name']][] = $migration['migration'];
|
|
|
|
// make sure we have this in the config too
|
|
$config = \Config::get('migrations.version.'.$migration['type'].'.'.$migration['name'], array());
|
|
is_array($config) or $config = array();
|
|
if ( ! in_array($migration['migration'], $config))
|
|
{
|
|
$config[] = $migration['migration'];
|
|
sort($config);
|
|
\Config::set('migrations.version.'.$migration['type'].'.'.$migration['name'], $config);
|
|
}
|
|
}
|
|
// write the updated config
|
|
\Config::save(\Fuel::$env.DS.'migrations', 'migrations');
|
|
}
|
|
|
|
/**
|
|
* migrate to a specific version, range of versions, or all
|
|
*
|
|
* @param mixed $version version to migrate to (up or down!)
|
|
* @param string $name name of the package, module or app
|
|
* @param string $type type of migration (package, module or app)
|
|
* @param bool $all if true, also run out-of-sequence migrations
|
|
*
|
|
* @throws \UnexpectedValueException
|
|
* @return array
|
|
*/
|
|
public static function version($version = null, $name = 'default', $type = 'app', $all = false)
|
|
{
|
|
// get the current version from the config
|
|
$all or $current = \Config::get('migrations.version.'.$type.'.'.$name);
|
|
|
|
// any migrations defined?
|
|
if ( ! empty($current))
|
|
{
|
|
// get the timestamp of the last installed migration
|
|
if (preg_match('/^(.*?)_(.*)$/', end($current), $match))
|
|
{
|
|
// determine the direction
|
|
$direction = (is_null($version) or $match[1] < $version) ? 'up' : 'down';
|
|
|
|
// fetch the migrations
|
|
if ($direction == 'up')
|
|
{
|
|
$migrations = static::find_migrations($name, $type, $match[1], $version);
|
|
}
|
|
else
|
|
{
|
|
$migrations = static::find_migrations($name, $type, $version, $match[1], $direction);
|
|
|
|
// we're going down, so reverse the order of mygrations
|
|
$migrations = array_reverse($migrations, true);
|
|
}
|
|
|
|
// run migrations from current version to given version
|
|
return static::run($migrations, $name, $type, $direction);
|
|
}
|
|
else
|
|
{
|
|
throw new \UnexpectedValueException('Could not determine a valid version from '.$current.'.');
|
|
}
|
|
}
|
|
|
|
// run migrations from the beginning to given version
|
|
return static::run(static::find_migrations($name, $type, null, $version), $name, $type, 'up');
|
|
}
|
|
|
|
/**
|
|
* migrate to a latest version
|
|
*
|
|
* @param string $name name of the package, module or app
|
|
* @param string $type type of migration (package, module or app)
|
|
* @param bool $all if true, also run out-of-sequence migrations
|
|
*
|
|
* @return array
|
|
*/
|
|
public static function latest($name = 'default', $type = 'app', $all = false)
|
|
{
|
|
// equivalent to from current version (or all) to latest
|
|
return static::version(null, $name, $type, $all);
|
|
}
|
|
|
|
/**
|
|
* migrate to the version defined in the config file
|
|
*
|
|
* @param string $name name of the package, module or app
|
|
* @param string $type type of migration (package, module or app)
|
|
*
|
|
* @return array
|
|
*/
|
|
public static function current($name = 'default', $type = 'app')
|
|
{
|
|
// get the current version from the config
|
|
$current = \Config::get('migrations.version.'.$type.'.'.$name);
|
|
|
|
// any migrations defined?
|
|
if ( ! empty($current))
|
|
{
|
|
// get the timestamp of the last installed migration
|
|
if (preg_match('/^(.*?)_(.*)$/', end($current), $match))
|
|
{
|
|
// run migrations from start to current version
|
|
return static::run(static::find_migrations($name, $type, null, $match[1]), $name, $type, 'up');
|
|
}
|
|
}
|
|
|
|
// nothing to migrate
|
|
return array();
|
|
}
|
|
|
|
/**
|
|
* migrate up to the next version
|
|
*
|
|
* @param mixed $version version to migrate up to
|
|
* @param string $name name of the package, module or app
|
|
* @param string $type type of migration (package, module or app)
|
|
*
|
|
* @return array
|
|
*/
|
|
public static function up($version = null, $name = 'default', $type = 'app')
|
|
{
|
|
// get the current version info from the config
|
|
$current = \Config::get('migrations.version.'.$type.'.'.$name);
|
|
|
|
// get the last migration installed
|
|
$current = empty($current) ? null : end($current);
|
|
|
|
// get the available migrations after the current one
|
|
$migrations = static::find_migrations($name, $type, $current, $version);
|
|
|
|
// found any?
|
|
if ( ! empty($migrations))
|
|
{
|
|
// if no version was given, only install the next migration
|
|
is_null($version) and $migrations = array(reset($migrations));
|
|
|
|
// install migrations found
|
|
return static::run($migrations, $name, $type, 'up');
|
|
}
|
|
|
|
// nothing to migrate
|
|
return array();
|
|
}
|
|
|
|
/**
|
|
* migrate down to the previous version
|
|
*
|
|
* @param mixed $version version to migrate down to
|
|
* @param string $name name of the package, module or app
|
|
* @param string $type type of migration (package, module or app)
|
|
*
|
|
* @return array
|
|
*/
|
|
public static function down($version = null, $name = 'default', $type = 'app')
|
|
{
|
|
// get the current version info from the config
|
|
$current = \Config::get('migrations.version.'.$type.'.'.$name);
|
|
|
|
// any migrations defined?
|
|
if ( ! empty($current))
|
|
{
|
|
// get the last entry
|
|
$current = end($current);
|
|
|
|
// get the available migrations before the last current one
|
|
$migrations = static::find_migrations($name, $type, $version, $current, 'down');
|
|
|
|
// found any?
|
|
if ( ! empty($migrations))
|
|
{
|
|
// if no version was given, only revert the last migration
|
|
if (is_null($version))
|
|
{
|
|
$migrations = array_slice($migrations, -1, 1, true);
|
|
}
|
|
else
|
|
{
|
|
// we're going down, so reverse the order of migrations
|
|
$migrations = array_reverse($migrations, true);
|
|
}
|
|
|
|
// revert the installed migrations
|
|
return static::run($migrations, $name, $type, 'down');
|
|
}
|
|
}
|
|
|
|
// nothing to migrate
|
|
return array();
|
|
}
|
|
|
|
/**
|
|
* run the action migrations found
|
|
*
|
|
* @param array $migrations list of files to migrate
|
|
* @param string $name name of the package, module or app
|
|
* @param string $type type of migration (package, module or app)
|
|
* @param string $method method to call on the migration
|
|
*
|
|
* @return array
|
|
*/
|
|
protected static function run($migrations, $name, $type, $method = 'up')
|
|
{
|
|
// storage for installed migrations
|
|
$done = array();
|
|
|
|
static::$connection === null or \DBUtil::set_connection(static::$connection);
|
|
|
|
// Make sure we have class access
|
|
switch ($type)
|
|
{
|
|
case 'package':
|
|
\Package::load($name);
|
|
break;
|
|
|
|
case 'module':
|
|
\Module::load($name);
|
|
break;
|
|
|
|
default:
|
|
}
|
|
|
|
// Loop through the runnable migrations and run them
|
|
foreach ($migrations as $ver => $migration)
|
|
{
|
|
logger(\Fuel::L_INFO, 'Migrating to version: '.$ver);
|
|
$result = static::_run($migration['class'], $method);
|
|
if ($result === false)
|
|
{
|
|
logger(\Fuel::L_INFO, 'Skipped migration to '.$ver.'.');
|
|
$done[] = false;
|
|
return $done;
|
|
}
|
|
|
|
$file = basename($migration['path'], '.php');
|
|
$method == 'up' ? static::write_install($name, $type, $file) : static::write_revert($name, $type, $file);
|
|
$done[] = $file;
|
|
}
|
|
|
|
static::$connection === null or \DBUtil::set_connection(null);
|
|
|
|
empty($done) or logger(\Fuel::L_INFO, 'Migrated to '.$ver.' successfully.');
|
|
|
|
return $done;
|
|
}
|
|
|
|
/**
|
|
* add an installed migration to the database
|
|
*
|
|
* @param string $name name of the package, module or app
|
|
* @param string $type type of migration (package, module or app)
|
|
* @param string $file name of the migration file just run
|
|
*
|
|
* @return void
|
|
*/
|
|
protected static function write_install($name, $type, $file)
|
|
{
|
|
// add the migration just run
|
|
\DB::insert(static::$table)->set(array(
|
|
'name' => $name,
|
|
'type' => $type,
|
|
'migration' => $file,
|
|
))->execute(static::$connection);
|
|
|
|
// add the file to the list of run migrations
|
|
static::$migrations[$type][$name][] = $file;
|
|
|
|
// make sure the migrations are in the correct order
|
|
sort(static::$migrations[$type][$name]);
|
|
|
|
// and save the update to the environment config file
|
|
\Config::set('migrations.version.'.$type.'.'.$name, static::$migrations[$type][$name]);
|
|
\Config::save(\Fuel::$env.DS.'migrations', 'migrations');
|
|
}
|
|
|
|
/**
|
|
* remove a reverted migration from the database
|
|
*
|
|
* @param string $name name of the package, module or app
|
|
* @param string $type type of migration (package, module or app)
|
|
* @param string $file name of the migration file just run
|
|
*
|
|
* @return void
|
|
*/
|
|
protected static function write_revert($name, $type, $file)
|
|
{
|
|
// remove the migration just run
|
|
\DB::delete(static::$table)
|
|
->where('name', $name)
|
|
->where('type', $type)
|
|
->where('migration', $file)
|
|
->execute(static::$connection);
|
|
|
|
// remove the file from the list of run migrations
|
|
if (($key = array_search($file, static::$migrations[$type][$name])) !== false)
|
|
{
|
|
unset(static::$migrations[$type][$name][$key]);
|
|
}
|
|
|
|
// make sure the migrations are in the correct order
|
|
sort(static::$migrations[$type][$name]);
|
|
|
|
// and save the update to the config file
|
|
\Config::set('migrations.version.'.$type.'.'.$name, static::$migrations[$type][$name]);
|
|
\Config::save(\Fuel::$env.DS.'migrations', 'migrations');
|
|
}
|
|
|
|
/**
|
|
* migrate down to the previous version
|
|
*
|
|
* @param string $name name of the package, module or app
|
|
* @param string $type type of migration (package, module or app)
|
|
* @param mixed $start version to start migrations from, or null to start at the beginning
|
|
* @param mixed $end version to end migrations by, or null to migrate to the end
|
|
* @param string $direction
|
|
*
|
|
* @return array
|
|
* @throws \FuelException
|
|
*/
|
|
protected static function find_migrations($name, $type, $start = null, $end = null, $direction = 'up')
|
|
{
|
|
// Load all *_*.php files in the migrations path
|
|
$method = '_find_'.$type;
|
|
if ( ! $files = static::$method($name))
|
|
{
|
|
return array();
|
|
}
|
|
|
|
// get the currently installed migrations from the DB
|
|
$current = \Arr::get(static::$migrations, $type.'.'.$name, array());
|
|
|
|
// storage for the result
|
|
$migrations = array();
|
|
|
|
// normalize start and end values
|
|
if ( ! is_null($start))
|
|
{
|
|
// if we have a prefix, use that
|
|
($pos = strpos($start, '_')) === false or $start = ltrim(substr($start, 0, $pos), '0');
|
|
is_numeric($start) and $start = (int) $start;
|
|
}
|
|
if ( ! is_null($end))
|
|
{
|
|
// if we have a prefix, use that
|
|
($pos = strpos($end, '_')) === false or $end = ltrim(substr($end, 0, $pos), '0');
|
|
is_numeric($end) and $end = (int) $end;
|
|
}
|
|
|
|
// filter the migrations out of bounds
|
|
foreach ($files as $file)
|
|
{
|
|
// get the version for this migration and normalize it
|
|
$migration = basename($file);
|
|
($pos = strpos($migration, '_')) === false or $migration = ltrim(substr($migration, 0, $pos), '0');
|
|
is_numeric($migration) and $migration = (int) $migration;
|
|
|
|
// add the file to the migrations list if it's in between version bounds
|
|
if ((is_null($start) or $migration > $start) and (is_null($end) or $migration <= $end))
|
|
{
|
|
// see if it is already installed
|
|
if ( in_array(basename($file, '.php'), $current))
|
|
{
|
|
// already installed. store it only if we're going down
|
|
$direction == 'down' and $migrations[$migration] = array('path' => $file);
|
|
}
|
|
else
|
|
{
|
|
// not installed yet. store it only if we're going up
|
|
$direction == 'up' and $migrations[$migration] = array('path' => $file);
|
|
}
|
|
}
|
|
}
|
|
|
|
// We now prepare to actually DO the migrations
|
|
// But first let's make sure that everything is the way it should be
|
|
foreach ($migrations as $ver => $migration)
|
|
{
|
|
// get the migration filename from the path
|
|
$migration['file'] = basename($migration['path']);
|
|
|
|
// make sure the migration filename has a valid format
|
|
if (preg_match('/^.*?_(.*).php$/', $migration['file'], $match))
|
|
{
|
|
// determine the classname for this migration
|
|
$class_name = ucfirst(strtolower($match[1]));
|
|
|
|
// load the file and determine the classname
|
|
include_once $migration['path'];
|
|
$class = static::$prefix.$class_name;
|
|
|
|
// make sure it exists in the migration file loaded
|
|
if ( ! class_exists($class, false))
|
|
{
|
|
throw new \FuelException(sprintf('Migration "%s" does not contain expected class "%s"', $migration['path'], $class));
|
|
}
|
|
|
|
// and that it contains an "up" and "down" method
|
|
if ( ! is_callable(array($class, 'up')) or ! is_callable(array($class, 'down')))
|
|
{
|
|
throw new \FuelException(sprintf('Migration class "%s" must include public methods "up" and "down"', $name));
|
|
}
|
|
|
|
$migrations[$ver]['class'] = $class;
|
|
}
|
|
else
|
|
{
|
|
throw new \FuelException(sprintf('Invalid Migration filename "%s"', $migration['path']));
|
|
}
|
|
}
|
|
|
|
// make sure the result is sorted properly with all version types
|
|
uksort($migrations, 'strnatcasecmp');
|
|
|
|
return $migrations;
|
|
}
|
|
|
|
/**
|
|
* run the actual migration, and it's before and after methods if present
|
|
*
|
|
*/
|
|
protected static function _run($class, $method)
|
|
{
|
|
// create an instance of the migration class
|
|
$class = new $class;
|
|
|
|
// if it has a before method, call that first
|
|
if (method_exists($class, 'before'))
|
|
{
|
|
if (false === call_user_func(array($class, 'before')))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// run the actual migration
|
|
$result = call_user_func(array($class, $method));
|
|
|
|
// if it has a after method, call that if the migration has run
|
|
if ($result !== false and method_exists($class, 'after'))
|
|
{
|
|
if (false === call_user_func(array($class, 'after')))
|
|
{
|
|
// revert the migration
|
|
logger(\Fuel::L_INFO, 'Migration is reverted due to failure of the after method.');
|
|
|
|
if ($method == 'up')
|
|
{
|
|
call_user_func(array($class, 'down'));
|
|
}
|
|
else
|
|
{
|
|
call_user_func(array($class, 'up'));
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* finds migrations for the given app
|
|
*
|
|
* @param string $name name of the app (not used at the moment)
|
|
*
|
|
* @return array
|
|
*/
|
|
protected static function _find_app($name = null)
|
|
{
|
|
$found = array();
|
|
|
|
$files = new \GlobIterator(APPPATH.\Config::get('migrations.folder').'*_*.php');
|
|
foreach($files as $file)
|
|
{
|
|
$found[] = $file->getPathname();
|
|
}
|
|
|
|
return $found;
|
|
}
|
|
|
|
/**
|
|
* finds migrations for the given module (or all if name is not given)
|
|
*
|
|
* @param string $name name of the module
|
|
*
|
|
* @return array
|
|
*/
|
|
protected static function _find_module($name = null)
|
|
{
|
|
$files = array();
|
|
|
|
if ($name)
|
|
{
|
|
// find a module
|
|
foreach (\Config::get('module_paths') as $m)
|
|
{
|
|
$found = new \GlobIterator($m.$name.DS.\Config::get('migrations.folder').'*_*.php');
|
|
if (count($found))
|
|
{
|
|
foreach($found as $file)
|
|
{
|
|
$files[] = $file->getPathname();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// find all modules
|
|
foreach (\Config::get('module_paths') as $m)
|
|
{
|
|
$found = new \GlobIterator($m.'*'.DS.\Config::get('migrations.folder').'*_*.php');
|
|
foreach($found as $file)
|
|
{
|
|
$files[] = $file->getPathname();
|
|
}
|
|
}
|
|
}
|
|
|
|
return $files;
|
|
}
|
|
|
|
/**
|
|
* finds migrations for the given package (or all if name is not given)
|
|
*
|
|
* @param string $name name of the package
|
|
*
|
|
* @return array
|
|
*/
|
|
protected static function _find_package($name = null)
|
|
{
|
|
$files = array();
|
|
|
|
if ($name)
|
|
{
|
|
// find a package
|
|
foreach (\Config::get('package_paths', array(PKGPATH)) as $p)
|
|
{
|
|
$found = new \GlobIterator($p.$name.DS.\Config::get('migrations.folder').'*_*.php');
|
|
if (count($found))
|
|
{
|
|
foreach($found as $file)
|
|
{
|
|
$files[] = $file->getPathname();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// find all packages
|
|
foreach (\Config::get('package_paths', array(PKGPATH)) as $p)
|
|
{
|
|
$found = new \GlobIterator($p.'*'.DS.\Config::get('migrations.folder').'*_*.php');
|
|
foreach($found as $file)
|
|
{
|
|
$files[] = $file->getPathname();
|
|
}
|
|
}
|
|
}
|
|
|
|
return $files;
|
|
}
|
|
|
|
/**
|
|
* installs or upgrades the migration table to the current schema
|
|
*
|
|
* @return void
|
|
*
|
|
* @deprecated Remove upgrade check in 1.4
|
|
*/
|
|
protected static function table_version_check()
|
|
{
|
|
// set connection
|
|
static::$connection === null or \DBUtil::set_connection(static::$connection);
|
|
|
|
// if table does not exist
|
|
if ( ! \DBUtil::table_exists(static::$table))
|
|
{
|
|
// create table
|
|
\DBUtil::create_table(static::$table, static::$table_definition);
|
|
}
|
|
|
|
// check if a table upgrade is needed
|
|
elseif ( ! \DBUtil::field_exists(static::$table, array('migration')))
|
|
{
|
|
// get the current migration status
|
|
$current = \DB::select()->from(static::$table)->order_by('type', 'ASC')->order_by('name', 'ASC')->execute(static::$connection)->as_array();
|
|
|
|
// drop the existing table, and recreate it in the new layout
|
|
\DBUtil::drop_table(static::$table);
|
|
\DBUtil::create_table(static::$table, static::$table_definition);
|
|
|
|
// check if we had a current migration status
|
|
if ( ! empty($current))
|
|
{
|
|
// do we need to migrate from a v1.0 migration environment?
|
|
if (isset($current[0]['current']))
|
|
{
|
|
// convert the current result into a v1.1. migration environment structure
|
|
$current = array(0 => array('name' => 'default', 'type' => 'app', 'version' => $current[0]['current']));
|
|
}
|
|
|
|
// build a new config structure
|
|
$configs = array();
|
|
|
|
// convert the v1.1 structure to the v1.2 structure
|
|
foreach ($current as $migration)
|
|
{
|
|
// find the migrations for this entry
|
|
$migrations = static::find_migrations($migration['name'], $migration['type'], null, $migration['version']);
|
|
|
|
// array to keep track of the migrations already run
|
|
$config = array();
|
|
|
|
// add the individual migrations found
|
|
foreach ($migrations as $file)
|
|
{
|
|
$file = pathinfo($file['path']);
|
|
|
|
// add this migration to the table
|
|
\DB::insert(static::$table)->set(array(
|
|
'name' => $migration['name'],
|
|
'type' => $migration['type'],
|
|
'migration' => $file['filename'],
|
|
))->execute(static::$connection);
|
|
|
|
// and to the config
|
|
$config[] = $file['filename'];
|
|
}
|
|
|
|
// create a config entry for this name and type if needed
|
|
isset($configs[$migration['type']]) or $configs[$migration['type']] = array();
|
|
$configs[$migration['type']][$migration['name']] = $config;
|
|
}
|
|
|
|
// write the updated migrations config back
|
|
\Config::set('migrations.version', $configs);
|
|
\Config::save(\Fuel::$env.DS.'migrations', 'migrations');
|
|
}
|
|
|
|
// delete any old migration config file that may exist
|
|
is_file(APPPATH.'config'.DS.'migrations.php') and unlink(APPPATH.'config'.DS.'migrations.php');
|
|
}
|
|
|
|
// set connection to default
|
|
static::$connection === null or \DBUtil::set_connection(null);
|
|
}
|
|
}
|