实现简单的 DB 迁移管理

<?php
Core_Autoloader::loadFile(COREPATH . '/vendor/SingleTableCRUD.class.php',true);

/**
 * 迁移操作入口
 *
 * @package pkg
 *
 */
class Pkg_Gen_Table_Migration {

	private static $migrationTable = 'sql_table_migration';

	/**
	 * @var Pkg_Gen_Table_MigrationLog
	 */
	static $logger = null;
	
	/**
	 * @return TplEngine
	 */
	private static function getTplEngine(){
		static $tplEngine = null;
		if (!$tplEngine){
			Core_Autoloader::loadFile(COREPATH . '/vendor/TplEngine.class.php');

			$tplConfig = array(
				'templateDir' => dirname(__FILE__) . '/_views',
				'enableCache' => false,
			);
			$tplEngine = new TplEngine($tplConfig);
		}
		return $tplEngine;
	}

	private static function getMigrations($migrationDir,$tableClassPrefix){
		static $migrations = null;
		if ($migrations) return $migrations;

		$migrations = array();
		$index = 1;
		// 获取迁移类对象
		foreach (glob("{$migrationDir}/*.php") as $filename) {
			$id = basename($filename,'.php');
			$className = "{$tableClassPrefix}{$id}";
			// 加载迁移类到系统
			Core_Autoloader::loadClass($className);

			$obj = new $className();
			// 校验迁移类是否实现了Pkg_Gen_Table_MigrationElement接口
			if ( !($obj instanceof Pkg_Gen_Table_MigrationElement) ){
				throw new Core_Exception_TypeMismatch('迁移类对象','Pkg_Gen_Table_MigrationElement',$className);
			}

			$migrations[$index] = array('id' =>$id,'class' => $className ,'instance' => $obj);
			$index ++;
		}

		return $migrations;
	}

	private static function initMigrationTable(Core_DB $dbo){
		static $is = false;
		if (!$is){
			$row = $dbo->getRow( sprintf("SHOW TABLES LIKE '%s'",self::$migrationTable) );
			if (!empty($row)){
				$is = true;
				return;
			}
			$tb = Pkg_Gen_Table_DML::newInstance($dbo,self::$migrationTable);

			$tb->struct(array(
				$tb->combindColumnParams('version','int',true,6),
			))
			->setPrimaryKey('version')
			->setOptions(array(
				Pkg_Gen_Table_DML::ENGINE => Pkg_Gen_Table_DML::ENGINE_INNODB,

			))->create();

			$tb->execute();
			$is = SingleTableCRUD::insert(self::$migrationTable,array('version'=>0));
			self::$logger->append($dbo->lastsql);
		}
	}

	static function ls(Core_DB $dbo,$migrationDir,$tableClassPrefix,$saveUrl){

		if ( !(is_readable($migrationDir) && is_dir($migrationDir)) )
			throw new Exception("无效的迁移类文件存放路径: {$migrationDir}");

		self::initMigrationTable($dbo);
		$migrations = self::getMigrations($migrationDir,$tableClassPrefix);

		// 得到当前版本号,缺省为0
		$curversion = (int) $dbo->getOne(sprintf('select version from %s',self::$migrationTable));

		self::getTplEngine()->assign('database',$dbo->getDSN('database'));
		self::getTplEngine()->assign('migrations',$migrations);
		self::getTplEngine()->assign('version',$curversion);
		self::getTplEngine()->assign('saveurl',$saveUrl);
		self::getTplEngine()->display('migrations.php');
	}

	static function change(Core_DB $dbo,$migrationDir,$tableClassPrefix,$newversion,$lastversion){

		if ( !(is_readable($migrationDir) && is_dir($migrationDir)) )
			throw new Exception("无效的迁移类文件存放路径: {$migrationDir}");

		self::initMigrationTable($dbo);
		$migrations = self::getMigrations($migrationDir,$tableClassPrefix);

		// 得到当前版本号,缺省为0
		$curversion = (int) $dbo->getOne(sprintf('select version from %s',self::$migrationTable));

		if ($curversion != $lastversion) throw new Exception("无效的参数 lastversion: {$lastversion}");

		if ($curversion == $newversion) throw new Exception("版本无需迁移操作");
		
		if ($newversion > 0){
			if (!isset($migrations[$newversion])) throw new Exception("无效的参数 newversion: {$newversion}");
		}
		
		// 开始进行版本迁移操作
		if ($curversion > $newversion){
			// 反向
			for($start=$curversion,$end = $newversion; $start > $end; $start --){
				$instance = $migrations[$start]['instance'];
				/* @var $instance Pkg_Gen_Table_MigrationElement */
				self::$logger->append($migrations[$start]['class'] . '::down()');
				try {
					$instance->down();
				} catch( Exception $ex){
					throw new Exception("反向迁移: {$curversion}到{$newversion}失败,请修正后再操作... 可参考迁移日志");
				}
				$dbo->startTrans();
				$is = SingleTableCRUD::incrField(self::$migrationTable,null,'version',-1);
				self::$logger->append($dbo->lastsql);
				$dbo->completeTrans($is);
				
				if (!$is) throw new Exception("反向迁移: {$curversion}到{$newversion}失败,请修正后再操作... 可参考迁移日志");
			}
			
		}else {
			// 正向
			for($start=$curversion + 1,$end = $newversion + 1; $start < $end; $start ++){
				$instance = $migrations[$start]['instance'];
				/* @var $instance Pkg_Gen_Table_MigrationElement */
				self::$logger->append($migrations[$start]['class'] . '::up()');
				try {
					$instance->up();
				} catch( Exception $ex){
					throw new Exception("正向迁移: {$curversion}到{$newversion}失败,请修正后再操作... 可参考迁移日志");
				}
				
				$dbo->startTrans();
				$is = SingleTableCRUD::incrField(self::$migrationTable,null,'version',1);
				self::$logger->append($dbo->lastsql);
				$dbo->completeTrans($is);
				
				if (!$is) throw new Exception("正向迁移: {$curversion}到{$newversion}失败,请修正后再操作... 可参考迁移日志");
			}
			
		}
	
		
	}

}

/**
 * 迁移元素接口
 *
 * @package pkg
 *
 */
interface Pkg_Gen_Table_MigrationElement {

	/**
	 * 正向迁移操作
	 *
	 * @return bool
	 */
	function up();

	/**
	 * 逆向此次迁移操作
	 *
	 * @return bool
	 */
	function down();

	/**
	 * 迁移操作的说明
	 *
	 * @return string
	 */
	function description();
}

/**
 * 迁移日志类
 *
 * @package pkg
 *
 */
class Pkg_Gen_Table_MigrationLog extends Core_LogWriterAbstract {

	/**
	 * 保存运行期间的日志
	 *
	 * @var string
	 */
	private $_log = '';

	/**
	 * 日期格式
	 *
	 * @var string
	 */
	private $dateFormat = 'Y-m-d H:i:s';

	/**
	 * 保存日志的文件名
	 *
	 * @var string
	 */
	private $_logFilename = '';

	function __construct($logDir){
		if ( !(is_writable($logDir) && is_dir($logDir)) ){
			throw new Exception("无效的迁移日志文件存放路径: {$logDir}");
		}
	
        $logDir = realpath($logDir);
        if (substr($logDir, -1) != DIRECTORY_SEPARATOR) {
            $logDir .= DIRECTORY_SEPARATOR;
        }
        
		$this->_logFilename = $logDir . 'sql_table_migration.txt';
		unset($logDir);
		$app_start_time = Core_App::ini('+app_start_time+');
		$sec = (int) $app_start_time;
		$usec = $app_start_time - $sec;

		$this->_startTag = sprintf("[%s %s] ======= IWP Migration Loaded =======\n",
		date($this->dateFormat, $sec), $usec);
		
		// 注册脚本结束时要运行的方法,将缓存的日志内容写入文件
		Core_Halt::getInstance()->add(array($this, '__writeLog'));
	}

	function append($msg, $title = '', $level = 'info'){
		if (empty($msg)) return;
		$this->_log .= sprintf("[%s] %s\n", date($this->dateFormat), print_r($msg, true));
	}

	/**
	 * 将缓存的日志信息写入实际存储,并清空缓存
	 * 此方法由系统自动调用
	 *
	 */
	function __writeLog(){
		if (empty($this->_log)) return;
		$app_start_time = Core_App::ini('+app_start_time+');

		$shutdown_time = microtime(true);
		$sec = (int) $shutdown_time;
		$usec = $shutdown_time - $sec;

		$elapsedTime = $shutdown_time - $app_start_time;

		$content = $this->_startTag . $this->_log . sprintf("[%s %s] ======= IWP Migration End (elapsed: %f seconds) =======\n\n",date($this->dateFormat, $sec), $usec, $elapsedTime);

		$fp = fopen($this->_logFilename, 'a');
		if (!$fp) { return; }
		flock($fp, LOCK_EX);
		fwrite($fp, str_replace("\r", '', $content));
		flock($fp, LOCK_UN);
		fclose($fp);
	}
}
 

你可能感兴趣的:(PHP)