Blame | Letzte Änderung | Log anzeigen | RSS feed
<?php/** $Id: Migration.php 1080 2007-02-10 18:17:08Z jwage $** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.** This software consists of voluntary contributions made by many individuals* and is licensed under the LGPL. For more information, see* <http://www.doctrine-project.org>.*//*** Doctrine_Migration** this class represents a database view** @package Doctrine* @subpackage Migration* @license http://www.opensource.org/licenses/lgpl-license.php LGPL* @link www.doctrine-project.org* @since 1.0* @version $Revision: 1080 $* @author Jonathan H. Wage <jwage@mac.com>*/class Doctrine_Migration{protected $_migrationTableName = 'migration_version',$_migrationTableCreated = false,$_connection,$_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 The path to your migrations directory* @param mixed $connection The connection name or instance to use for this migration* @return void*/public function __construct($directory = null, $connection = null){$this->_reflectionClass = new ReflectionClass('Doctrine_Migration_Base');if (is_null($connection)) {$this->_connection = Doctrine_Manager::connection();} else {if (is_string($connection)) {$this->_connection = Doctrine_Manager::getInstance()->getConnection($connection);} else {$this->_connection = $connection;}}$this->_process = new Doctrine_Migration_Process($this);if ($directory != null) {$this->_migrationClassesDirectory = $directory;$this->loadMigrationClassesFromDirectory();}}public function getConnection(){return $this->_connection;}public function setConnection(Doctrine_Connection $conn){$this->_connection = $conn;}/*** 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 = $this->_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;$classesToLoad = array();$classes = get_declared_classes();foreach ((array) $directory as $dir) {$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir),RecursiveIteratorIterator::LEAVES_ONLY);if (isset(self::$_migrationClassesForDirectories[$dir])) {foreach (self::$_migrationClassesForDirectories[$dir] as $num => $className) {$this->_migrationClasses[$num] = $className;}}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, SORT_NUMERIC);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){if ($this->hasMigrated()) {$this->_connection->exec("UPDATE " . $this->_migrationTableName . " SET version = $number");} else {$this->_connection->exec("INSERT INTO " . $this->_migrationTableName . " (version) VALUES ($number)");}}/*** Get the current version of the database** @return integer $version*/public function getCurrentVersion(){$this->_createMigrationTable();$result = $this->_connection->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(){$this->_createMigrationTable();$result = $this->_connection->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->_createMigrationTable();$this->_connection->beginTransaction();try {// If nothing specified then lets assume we are migrating from// the current version to the latest versionif ($to === null) {$to = $this->getLatestVersion();}$this->_doMigrate($to);} catch (Exception $e) {$this->addError($e);}if ($this->hasErrors()) {$this->_connection->rollback();if ($dryRun) {return false;} else {$this->_throwErrorsException();}} else {if ($dryRun) {$this->_connection->rollback();if ($this->hasErrors()) {return false;} else {return $to;}} else {$this->_connection->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();if ($direction == 'down' && method_exists($migration, 'migrate')) {$changes = array_reverse($changes);}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();} 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(){if ($this->_migrationTableCreated) {return true;}$this->_migrationTableCreated = true;try {$this->_connection->export->createTable($this->_migrationTableName, array('version' => array('type' => 'integer', 'size' => 11)));return true;} catch(Exception $e) {return false;}}}