. */ /** * Doctrine_Migration * * this class represents a database view * * @package Doctrine * @subpackage Migration * @license http://www.opensource.org/licenses/lgpl-license.php LGPL * @link www.phpdoctrine.org * @since 1.0 * @version $Revision: 1080 $ * @author Jonathan H. Wage */ class Doctrine_Migration { protected $_migrationTableName = 'migration_version', $_migrationClassesDirectory = array(), $_migrationClasses = array(), $_reflectionClass, $_errors = array(), $_process; protected static $_migrationClassesForDirectories = array(); /** * Specify the path to the directory with the migration classes. * The classes will be loaded and the migration table will be created if it does not already exist * * @param string $directory * @return void */ public function __construct($directory = null) { $this->_reflectionClass = new ReflectionClass('Doctrine_Migration_Base'); $this->_process = new Doctrine_Migration_Process($this); if ($directory != null) { $this->_migrationClassesDirectory = $directory; $this->loadMigrationClassesFromDirectory(); $this->_createMigrationTable(); } } /** * Get the migration classes directory * * @return string $migrationClassesDirectory */ public function getMigrationClassesDirectory() { return $this->_migrationClassesDirectory; } /** * Get the table name for storing the version number for this migration instance * * @return string $migrationTableName */ public function getTableName() { return $this->_migrationTableName; } /** * Set the table name for storing the version number for this migration instance * * @param string $tableName * @return void */ public function setTableName($tableName) { $this->_migrationTableName = Doctrine_Manager::connection() ->formatter->getTableName($tableName); } /** * Load migration classes from the passed directory. Any file found with a .php * extension will be passed to the loadMigrationClass() * * @param string $directory Directory to load migration classes from * @return void */ public function loadMigrationClassesFromDirectory($directory = null) { $directory = $directory ? $directory:$this->_migrationClassesDirectory; if (isset(self::$_migrationClassesForDirectories[$directory])) { $migrationClasses = (array) self::$_migrationClassesForDirectories[$directory]; $this->_migrationClasses = array_merge($migrationClasses, $this->_migrationClasses); } $classesToLoad = array(); $classes = get_declared_classes(); foreach ((array) $directory as $dir) { $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir), RecursiveIteratorIterator::LEAVES_ONLY); foreach ($it as $file) { $info = pathinfo($file->getFileName()); if (isset($info['extension']) && $info['extension'] == 'php') { require_once($file->getPathName()); $array = array_diff(get_declared_classes(), $classes); $className = end($array); if ($className) { $e = explode('_', $file->getFileName()); $timestamp = $e[0]; $classesToLoad[$timestamp] = array('className' => $className, 'path' => $file->getPathName()); } } } } ksort($classesToLoad); foreach ($classesToLoad as $class) { $this->loadMigrationClass($class['className'], $class['path']); } } /** * Load the specified migration class name in to this migration instances queue of * migration classes to execute. It must be a child of Doctrine_Migration in order * to be loaded. * * @param string $name * @return void */ public function loadMigrationClass($name, $path = null) { $class = new ReflectionClass($name); while ($class->isSubclassOf($this->_reflectionClass)) { $class = $class->getParentClass(); if ($class === false) { break; } } if ($class === false) { return false; } if (empty($this->_migrationClasses)) { $classMigrationNum = 1; } else { $nums = array_keys($this->_migrationClasses); $num = end($nums); $classMigrationNum = $num + 1; } $this->_migrationClasses[$classMigrationNum] = $name; if ($path) { $dir = dirname($path); self::$_migrationClassesForDirectories[$dir][$classMigrationNum] = $name; } } /** * Get all the loaded migration classes. Array where key is the number/version * and the value is the class name. * * @return array $migrationClasses */ public function getMigrationClasses() { return $this->_migrationClasses; } /** * Set the current version of the database * * @param integer $number * @return void */ public function setCurrentVersion($number) { $conn = Doctrine_Manager::connection(); if ($this->hasMigrated()) { $conn->exec("UPDATE " . $this->_migrationTableName . " SET version = $number"); } else { $conn->exec("INSERT INTO " . $this->_migrationTableName . " (version) VALUES ($number)"); } } /** * Get the current version of the database * * @return integer $version */ public function getCurrentVersion() { $conn = Doctrine_Manager::connection(); $result = $conn->fetchColumn("SELECT version FROM " . $this->_migrationTableName); return isset($result[0]) ? $result[0]:0; } /** * hReturns true/false for whether or not this database has been migrated in the past * * @return boolean $migrated */ public function hasMigrated() { $conn = Doctrine_Manager::connection(); $result = $conn->fetchColumn("SELECT version FROM " . $this->_migrationTableName); return isset($result[0]) ? true:false; } /** * Gets the latest possible version from the loaded migration classes * * @return integer $latestVersion */ public function getLatestVersion() { $versions = array_keys($this->_migrationClasses); rsort($versions); return isset($versions[0]) ? $versions[0]:0; } /** * Get the next incremented version number based on the latest version number * using getLatestVersion() * * @return integer $nextVersion */ public function getNextVersion() { return $this->getLatestVersion() + 1; } /** * Get the next incremented class version based on the loaded migration classes * * @return integer $nextMigrationClassVersion */ public function getNextMigrationClassVersion() { if (empty($this->_migrationClasses)) { return 1; } else { $nums = array_keys($this->_migrationClasses); $num = end($nums) + 1; return $num; } } /** * Perform a migration process by specifying the migration number/version to * migrate to. It will automatically know whether you are migrating up or down * based on the current version of the database. * * @param integer $to Version to migrate to * @param boolean $dryRun Whether or not to run the migrate process as a dry run * @return integer $to Version number migrated to * @throws Doctrine_Exception */ public function migrate($to = null, $dryRun = false) { $this->clearErrors(); $this->_transaction('beginTransaction'); try { // If nothing specified then lets assume we are migrating from // the current version to the latest version if ($to === null) { $to = $this->getLatestVersion(); } $this->_doMigrate($to); } catch (Exception $e) { $this->addError($e); } if ($this->hasErrors()) { $this->_transaction('rollback'); if ($dryRun) { return false; } else { $this->_throwErrorsException(); } } else { if ($dryRun) { $this->_transaction('rollback'); if ($this->hasErrors()) { return false; } else { return $to; } } else { $this->_transaction('commit'); $this->setCurrentVersion($to); return $to; } } return false; } /** * Run the migration process but rollback at the very end. Returns true or * false for whether or not the migration can be ran * * @param string $to * @return boolean $success */ public function migrateDryRun($to = null) { return $this->migrate($to, true); } /** * Get the number of errors * * @return integer $numErrors */ public function getNumErrors() { return count($this->_errors); } /** * Get all the error exceptions * * @return array $errors */ public function getErrors() { return $this->_errors; } /** * Clears the error exceptions * * @return void */ public function clearErrors() { $this->_errors = array(); } /** * Add an error to the stack. Excepts some type of Exception * * @param Exception $e * @return void */ public function addError(Exception $e) { $this->_errors[] = $e; } /** * Whether or not the migration instance has errors * * @return boolean */ public function hasErrors() { return $this->getNumErrors() > 0 ? true:false; } /** * Get instance of migration class for number/version specified * * @param integer $num * @throws Doctrine_Migration_Exception $e */ public function getMigrationClass($num) { if (isset($this->_migrationClasses[$num])) { $className = $this->_migrationClasses[$num]; return new $className(); } throw new Doctrine_Migration_Exception('Could not find migration class for migration step: '.$num); } /** * Throw an exception with all the errors trigged during the migration * * @return void * @throws Doctrine_Migration_Exception $e */ protected function _throwErrorsException() { $messages = array(); $num = 0; foreach ($this->getErrors() as $error) { $num++; $messages[] = ' Error #' . $num . ' - ' .$error->getMessage() . "\n" . $error->getTraceAsString() . "\n"; } $title = $this->getNumErrors() . ' error(s) encountered during migration'; $message = $title . "\n"; $message .= str_repeat('=', strlen($title)) . "\n"; $message .= implode("\n", $messages); throw new Doctrine_Migration_Exception($message); } /** * Do the actual migration process * * @param integer $to * @return integer $to * @throws Doctrine_Exception */ protected function _doMigrate($to) { $from = $this->getCurrentVersion(); if ($from == $to) { throw new Doctrine_Migration_Exception('Already at version # ' . $to); } $direction = $from > $to ? 'down':'up'; if ($direction === 'up') { for ($i = $from + 1; $i <= $to; $i++) { $this->_doMigrateStep($direction, $i); } } else { for ($i = $from; $i > $to; $i--) { $this->_doMigrateStep($direction, $i); } } return $to; } /** * Perform a single migration step. Executes a single migration class and * processes the changes * * @param string $direction Direction to go, 'up' or 'down' * @param integer $num * @return void */ protected function _doMigrateStep($direction, $num) { try { $migration = $this->getMigrationClass($num); $method = 'pre' . $direction; $migration->$method(); if (method_exists($migration, $direction)) { $migration->$direction(); } else if (method_exists($migration, 'migrate')) { $migration->migrate($direction); } if ($migration->getNumChanges() > 0) { $changes = $migration->getChanges(); foreach ($changes as $value) { list($type, $change) = $value; $funcName = 'process' . Doctrine_Inflector::classify($type); if (method_exists($this->_process, $funcName)) { try { $this->_process->$funcName($change); } catch (Exception $e) { $this->addError($e); } } else { throw new Doctrine_Migration_Exception(sprintf('Invalid migration change type: %s', $type)); } } } $method = 'post' . $direction; $migration->$method(); $this->setCurrentVersion($num); } catch (Exception $e) { $this->addError($e); } } /** * Create the migration table and return true. If it already exists it will * silence the exception and return false * * @return boolean $created Whether or not the table was created. Exceptions * are silenced when table already exists */ protected function _createMigrationTable() { $conn = Doctrine_Manager::connection(); try { $conn->export->createTable($this->_migrationTableName, array('version' => array('type' => 'integer', 'size' => 11))); return true; } catch(Exception $e) { return false; } } /** * Wrapper for performing transaction functions on all available connections * * @param string $funcName * @return void */ protected function _transaction($funcName) { $connections = Doctrine_Manager::getInstance()->getConnections(); foreach ($connections as $connection) { try { $connection->$funcName(); } catch (Exception $e) {} } } }