<?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); } }