From f9e6557d9dbbc80505d2547094102cbdd66f9e86 Mon Sep 17 00:00:00 2001 From: Michael Date: Fri, 21 Oct 2011 00:47:53 +0200 Subject: [PATCH] added doctrine migrations --- application/Bootstrap.php | 24 +- bin/doctrine.php | 13 +- bin/migration-apply.php | 3 + bin/migration-create.php | 3 + bin/migration-dry.php | 3 + bin/migration-status.php | 3 + bin/migrations.yml | 4 + data/migrations/.gitignore | 0 library/App/Entity/Quote.php | 3 + .../Migrations/AbortMigrationException.php | 8 + .../DBAL/Migrations/AbstractMigration.php | 178 ++++++ .../AbstractFileConfiguration.php | 93 ++++ .../Configuration/Configuration.php | 508 ++++++++++++++++++ .../Configuration/XmlConfiguration.php | 58 ++ .../Configuration/YamlConfiguration.php | 61 +++ .../IrreversibleMigrationException.php | 33 ++ .../Doctrine/DBAL/Migrations/Migration.php | 160 ++++++ .../DBAL/Migrations/MigrationException.php | 66 +++ .../DBAL/Migrations/MigrationsVersion.php | 26 + .../Doctrine/DBAL/Migrations/OutputWriter.php | 52 ++ .../Migrations/SkipMigrationException.php | 8 + .../Tools/Console/Command/AbstractCommand.php | 122 +++++ .../Tools/Console/Command/DiffCommand.php | 106 ++++ .../Tools/Console/Command/ExecuteCommand.php | 102 ++++ .../Tools/Console/Command/GenerateCommand.php | 130 +++++ .../Tools/Console/Command/MigrateCommand.php | 107 ++++ .../Tools/Console/Command/StatusCommand.php | 115 ++++ .../Tools/Console/Command/VersionCommand.php | 95 ++++ library/Doctrine/DBAL/Migrations/Version.php | 353 ++++++++++++ 29 files changed, 2419 insertions(+), 18 deletions(-) create mode 100644 bin/migration-apply.php create mode 100644 bin/migration-create.php create mode 100644 bin/migration-dry.php create mode 100644 bin/migration-status.php create mode 100644 bin/migrations.yml create mode 100644 data/migrations/.gitignore create mode 100644 library/Doctrine/DBAL/Migrations/AbortMigrationException.php create mode 100644 library/Doctrine/DBAL/Migrations/AbstractMigration.php create mode 100644 library/Doctrine/DBAL/Migrations/Configuration/AbstractFileConfiguration.php create mode 100644 library/Doctrine/DBAL/Migrations/Configuration/Configuration.php create mode 100644 library/Doctrine/DBAL/Migrations/Configuration/XmlConfiguration.php create mode 100644 library/Doctrine/DBAL/Migrations/Configuration/YamlConfiguration.php create mode 100644 library/Doctrine/DBAL/Migrations/IrreversibleMigrationException.php create mode 100644 library/Doctrine/DBAL/Migrations/Migration.php create mode 100644 library/Doctrine/DBAL/Migrations/MigrationException.php create mode 100644 library/Doctrine/DBAL/Migrations/MigrationsVersion.php create mode 100644 library/Doctrine/DBAL/Migrations/OutputWriter.php create mode 100644 library/Doctrine/DBAL/Migrations/SkipMigrationException.php create mode 100644 library/Doctrine/DBAL/Migrations/Tools/Console/Command/AbstractCommand.php create mode 100644 library/Doctrine/DBAL/Migrations/Tools/Console/Command/DiffCommand.php create mode 100644 library/Doctrine/DBAL/Migrations/Tools/Console/Command/ExecuteCommand.php create mode 100644 library/Doctrine/DBAL/Migrations/Tools/Console/Command/GenerateCommand.php create mode 100644 library/Doctrine/DBAL/Migrations/Tools/Console/Command/MigrateCommand.php create mode 100644 library/Doctrine/DBAL/Migrations/Tools/Console/Command/StatusCommand.php create mode 100644 library/Doctrine/DBAL/Migrations/Tools/Console/Command/VersionCommand.php create mode 100644 library/Doctrine/DBAL/Migrations/Version.php diff --git a/application/Bootstrap.php b/application/Bootstrap.php index e8a9cd5..72a321a 100644 --- a/application/Bootstrap.php +++ b/application/Bootstrap.php @@ -12,27 +12,23 @@ public function _initAutoloaderNamespaces() require_once APPLICATION_PATH . '/../library/Doctrine/Common/ClassLoader.php'; + require_once APPLICATION_PATH . + '/../library/Symfony/Component/Di/sfServiceContainerAutoloader.php'; + + sfServiceContainerAutoloader::register(); $autoloader = \Zend_Loader_Autoloader::getInstance(); - $fmmAutoloader = new \Doctrine\Common\ClassLoader('Bisna'); - $autoloader->pushAutoloader( - array($fmmAutoloader, 'loadClass'), - 'Bisna' - ); + $fmmAutoloader = new \Doctrine\Common\ClassLoader('Bisna'); + $autoloader->pushAutoloader(array($fmmAutoloader, 'loadClass'), 'Bisna'); $fmmAutoloader = new \Doctrine\Common\ClassLoader('App'); $autoloader->pushAutoloader(array($fmmAutoloader, 'loadClass'), 'App'); - $fmmAutoloader = new \Doctrine\Common\ClassLoader('Boilerplate'); - - $autoloader->pushAutoloader( - array($fmmAutoloader, 'loadClass'), - 'Boilerplate' - ); - require_once APPLICATION_PATH . - '/../library/Symfony/Component/Di/sfServiceContainerAutoloader.php'; + $fmmAutoloader = new \Doctrine\Common\ClassLoader('Boilerplate'); + $autoloader->pushAutoloader(array($fmmAutoloader, 'loadClass'), 'Boilerplate'); - sfServiceContainerAutoloader::register(); + $fmmAutoloader = new \Doctrine\Common\ClassLoader('Doctrine\DBAL\Migrations'); + $autoloader->pushAutoloader(array($fmmAutoloader, 'loadClass'), 'Doctrine\DBAL\Migrations'); } public function _initModuleLayout() diff --git a/bin/doctrine.php b/bin/doctrine.php index 0395d24..e20d0e2 100644 --- a/bin/doctrine.php +++ b/bin/doctrine.php @@ -22,6 +22,9 @@ if (($em = $container->getEntityManager(getenv('EM') ?: $container->defaultEntityManager)) !== null) { $helperSet['em'] = new \Doctrine\ORM\Tools\Console\Helper\EntityManagerHelper($em); } + + $helperSet['dialog'] = new \Symfony\Component\Console\Helper\DialogHelper(); + } catch (\Exception $e) { $cli->renderException($e, new \Symfony\Component\Console\Output\ConsoleOutput()); } @@ -30,11 +33,14 @@ $cli->setHelperSet(new \Symfony\Component\Console\Helper\HelperSet($helperSet)); $cli->addCommands(array( - // DBAL Commands new \Doctrine\DBAL\Tools\Console\Command\RunSqlCommand(), new \Doctrine\DBAL\Tools\Console\Command\ImportCommand(), - - // ORM Commands + new \Doctrine\DBAL\Migrations\Tools\Console\Command\DiffCommand(), + new \Doctrine\DBAL\Migrations\Tools\Console\Command\ExecuteCommand(), + new \Doctrine\DBAL\Migrations\Tools\Console\Command\GenerateCommand(), + new \Doctrine\DBAL\Migrations\Tools\Console\Command\MigrateCommand(), + new \Doctrine\DBAL\Migrations\Tools\Console\Command\StatusCommand(), + new \Doctrine\DBAL\Migrations\Tools\Console\Command\VersionCommand(), new \Doctrine\ORM\Tools\Console\Command\ClearCache\MetadataCommand(), new \Doctrine\ORM\Tools\Console\Command\ClearCache\ResultCommand(), new \Doctrine\ORM\Tools\Console\Command\ClearCache\QueryCommand(), @@ -48,7 +54,6 @@ new \Doctrine\ORM\Tools\Console\Command\GenerateProxiesCommand(), new \Doctrine\ORM\Tools\Console\Command\ConvertMappingCommand(), new \Doctrine\ORM\Tools\Console\Command\RunDqlCommand(), - )); $cli->run(); \ No newline at end of file diff --git a/bin/migration-apply.php b/bin/migration-apply.php new file mode 100644 index 0000000..1126062 --- /dev/null +++ b/bin/migration-apply.php @@ -0,0 +1,3 @@ +getWording(); } + + + } \ No newline at end of file diff --git a/library/Doctrine/DBAL/Migrations/AbortMigrationException.php b/library/Doctrine/DBAL/Migrations/AbortMigrationException.php new file mode 100644 index 0000000..477c215 --- /dev/null +++ b/library/Doctrine/DBAL/Migrations/AbortMigrationException.php @@ -0,0 +1,8 @@ +. +*/ + +namespace Doctrine\DBAL\Migrations; + +use Doctrine\DBAL\Schema\Schema, + Doctrine\DBAL\Migrations\Configuration\Configuration, + Doctrine\DBAL\Migrations\Version; + +/** + * Abstract class for individual migrations to extend from. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Jonathan H. Wage + */ +abstract class AbstractMigration +{ + /** + * The Migrations Configuration instance for this migration + * + * @var Configuration + */ + private $configuration; + + /** + * The OutputWriter object instance used for outputting information + * + * @var OutputWriter + */ + private $outputWriter; + + /** + * The Doctrine\DBAL\Connection instance we are migrating + * + * @var Connection + */ + protected $connection; + + /** + * Reference to the SchemaManager instance referened by $_connection + * + * @var \Doctrine\DBAL\Schema\AbstractSchemaManager + */ + protected $sm; + + /** + * Reference to the DatabasePlatform instance referenced by $_conection + * + * @var \Doctrine\DBAL\Platforms\AbstractPlatform + */ + protected $platform; + + /** + * Reference to the Version instance representing this migration + * + * @var Version + */ + protected $version; + + public function __construct(Version $version) + { + $this->configuration = $version->getConfiguration(); + $this->outputWriter = $this->configuration->getOutputWriter(); + $this->connection = $this->configuration->getConnection(); + $this->sm = $this->connection->getSchemaManager(); + $this->platform = $this->connection->getDatabasePlatform(); + $this->version = $version; + } + + /** + * Get custom migration name + * + * @return string + */ + public function getName() + { + } + + abstract public function up(Schema $schema); + abstract public function down(Schema $schema); + + protected function addSql($sql, array $params = array()) + { + return $this->version->addSql($sql, $params); + } + + protected function write($message) + { + $this->outputWriter->write($message); + } + + protected function throwIrreversibleMigrationException($message = null) + { + if ($message === null) { + $message = 'This migration is irreversible and cannot be reverted.'; + } + throw new IrreversibleMigrationException($message); + } + + /** + * Print a warning message if the condition evalutes to TRUE. + * + * @param bool $condition + * @param string $message + */ + public function warnIf($condition, $message = '') + { + $message = (strlen($message)) ? $message : 'Unknown Reason'; + + if ($condition === true) { + $this->outputWriter->write(' Warning during ' . $this->version->getExecutionState() . ': ' . $message . ''); + } + } + + /** + * Abort the migration if the condition evalutes to TRUE. + * + * @param bool $condition + * @param string $message + */ + public function abortIf($condition, $message = '') + { + $message = (strlen($message)) ? $message : 'Unknown Reason'; + + if ($condition === true) { + throw new AbortMigrationException($message); + } + } + + /** + * Skip this migration (but not the next ones) if condition evalutes to TRUE. + * + * @param bool $condition + * @param string $message + */ + public function skipIf($condition, $message = '') + { + $message = (strlen($message)) ? $message : 'Unknown Reason'; + + if ($condition === true) { + throw new SkipMigrationException($message); + } + } + + public function preUp(Schema $schema) + { + } + + public function postUp(Schema $schema) + { + } + + public function preDown(Schema $schema) + { + } + + public function postDown(Schema $schema) + { + } +} diff --git a/library/Doctrine/DBAL/Migrations/Configuration/AbstractFileConfiguration.php b/library/Doctrine/DBAL/Migrations/Configuration/AbstractFileConfiguration.php new file mode 100644 index 0000000..f64f8bd --- /dev/null +++ b/library/Doctrine/DBAL/Migrations/Configuration/AbstractFileConfiguration.php @@ -0,0 +1,93 @@ +. +*/ + +namespace Doctrine\DBAL\Migrations\Configuration; + +use Doctrine\DBAL\Migrations\MigrationsException; + +/** + * Abstract Migration Configuration class for loading configuration information + * from a configuration file (xml or yml). + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Jonathan H. Wage + */ +abstract class AbstractFileConfiguration extends Configuration +{ + /** + * The configuration file used to load configuration information + * + * @var string + */ + private $file; + + /** + * Whether or not the configuration file has been loaded yet or not + * + * @var bool + */ + private $loaded = false; + + /** + * Load the information from the passed configuration file + * + * @param string $file The path to the configuration file + * @return void + * @throws MigrationException $exception Throws exception if configuration file was already loaded + */ + public function load($file) + { + if ($this->loaded) { + throw MigrationsException::configurationFileAlreadyLoaded(); + } + if (file_exists($path = getcwd() . '/' . $file)) { + $file = $path; + } + $this->file = $file; + $this->doLoad($file); + $this->loaded = true; + } + + protected function getDirectoryRelativeToFile($file, $input) + { + $path = realpath(dirname($file) . '/' . $input); + if ($path !== false) { + $directory = $path; + } else { + $directory = $input; + } + return $directory; + } + + public function getFile() + { + return $this->file; + } + + /** + * Abstract method that each file configuration driver must implement to + * load the given configuration file whether it be xml, yaml, etc. or something + * else. + * + * @param string $file The path to a configuration file. + */ + abstract protected function doLoad($file); +} \ No newline at end of file diff --git a/library/Doctrine/DBAL/Migrations/Configuration/Configuration.php b/library/Doctrine/DBAL/Migrations/Configuration/Configuration.php new file mode 100644 index 0000000..b66cdfe --- /dev/null +++ b/library/Doctrine/DBAL/Migrations/Configuration/Configuration.php @@ -0,0 +1,508 @@ +. +*/ + +namespace Doctrine\DBAL\Migrations\Configuration; + +use Doctrine\DBAL\Connection, + Doctrine\DBAL\Migrations\MigrationException, + Doctrine\DBAL\Migrations\Version, + Doctrine\DBAL\Migrations\OutputWriter, + Doctrine\DBAL\Schema\Table, + Doctrine\DBAL\Schema\Column, + Doctrine\DBAL\Types\Type; + +/** + * Default Migration Configurtion object used for configuring an instance of + * the Migration class. Set the connection, version table name, register migration + * classes/versions, etc. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Jonathan H. Wage + */ +class Configuration +{ + /** + * Name of this set of migrations + * + * @var string + */ + private $name; + + /** + * Flag for whether or not the migration table has been created + * + * @var bool + */ + private $migrationTableCreated = false; + + /** + * Connection instance to use for migrations + * + * @var Connection + */ + private $connection; + + /** + * OutputWriter instance for writing output during migrations + * + * @var OutputWriter + */ + private $outputWriter; + + /** + * The migration table name to track versions in + * + * @var string + */ + private $migrationsTableName = 'doctrine_migration_versions'; + + /** + * The path to a directory where new migration classes will be written + * + * @var string + */ + private $migrationsDirectory; + + /** + * Namespace the migration classes live in + * + * @var string + */ + private $migrationsNamespace; + + /** + * Array of the registered migrations + * + * @var array + */ + private $migrations = array(); + + /** + * Construct a migration configuration object. + * + * @param Connection $connection A Connection instance + * @param OutputWriter $outputWriter A OutputWriter instance + */ + public function __construct(Connection $connection, OutputWriter $outputWriter = null) + { + $this->connection = $connection; + if ($outputWriter === null) { + $outputWriter = new OutputWriter(); + } + $this->outputWriter = $outputWriter; + } + + /** + * Validation that this instance has all the required properties configured + * + * @return void + * @throws MigrationException + */ + public function validate() + { + if ( ! $this->migrationsNamespace) { + throw MigrationException::migrationsNamespaceRequired(); + } + if ( ! $this->migrationsDirectory) { + throw MigrationException::migrationsDirectoryRequired(); + } + } + + /** + * Set the name of this set of migrations + * + * @param string $name The name of this set of migrations + */ + public function setName($name) + { + $this->name = $name; + } + + /** + * Returns the name of this set of migrations + * + * @return string $name The name of this set of migrations + */ + public function getName() + { + return $this->name; + } + + /** + * Returns the OutputWriter instance + * + * @return OutputWriter $outputWriter The OutputWriter instance + */ + public function getOutputWriter() + { + return $this->outputWriter; + } + + /** + * Returns a timestamp version as a formatted date + * + * @param string $version + * @return string $formattedVersion The formatted version + */ + public function formatVersion($version) + { + return sprintf('%s-%s-%s %s:%s:%s', + substr($version, 0, 4), + substr($version, 4, 2), + substr($version, 6, 2), + substr($version, 8, 2), + substr($version, 10, 2), + substr($version, 12, 2) + ); + } + + /** + * Returns the Connection instance + * + * @return Connection $connection The Connection instance + */ + public function getConnection() + { + return $this->connection; + } + + /** + * Set the migration table name + * + * @param string $tableName The migration table name + */ + public function setMigrationsTableName($tableName) + { + $this->migrationsTableName = $tableName; + } + + /** + * Returns the migration table name + * + * @return string $migrationsTableName The migration table name + */ + public function getMigrationsTableName() + { + return $this->migrationsTableName; + } + + /** + * Set the new migrations directory where new migration classes are generated + * + * @param string $migrationsDirectory The new migrations directory + */ + public function setMigrationsDirectory($migrationsDirectory) + { + $this->migrationsDirectory = $migrationsDirectory; + } + + /** + * Returns the new migrations directory where new migration classes are generated + * + * @return string $migrationsDirectory The new migrations directory + */ + public function getMigrationsDirectory() + { + return $this->migrationsDirectory; + } + + /** + * Set the migrations namespace + * + * @param string $migrationsNamespace The migrations namespace + */ + public function setMigrationsNamespace($migrationsNamespace) + { + $this->migrationsNamespace = $migrationsNamespace; + } + + /** + * Returns the migrations namespace + * + * @return string $migrationsNamespace The migrations namespace + */ + public function getMigrationsNamespace() + { + return $this->migrationsNamespace; + } + + /** + * Register migrations from a given directory. Recursively finds all files + * with the pattern VersionYYYYMMDDHHMMSS.php as the filename and registers + * them as migrations. + * + * @param string $path The root directory to where some migration classes live. + * @return $migrations The array of migrations registered. + */ + public function registerMigrationsFromDirectory($path) + { + $path = realpath($path); + $path = rtrim($path, '/'); + $files = glob($path . '/Version*.php'); + $versions = array(); + if ($files) { + foreach ($files as $file) { + require_once($file); + $info = pathinfo($file); + $version = substr($info['filename'], 7); + $class = $this->migrationsNamespace . '\\' . $info['filename']; + $versions[] = $this->registerMigration($version, $class); + } + } + return $versions; + } + + /** + * Register a single migration version to be executed by a AbstractMigration + * class. + * + * @param string $version The version of the migration in the format YYYYMMDDHHMMSS. + * @param string $class The migration class to execute for the version. + */ + public function registerMigration($version, $class) + { + $version = (string) $version; + $class = (string) $class; + if (isset($this->migrations[$version])) { + throw MigrationException::duplicateMigrationVersion($version, get_class($this->migrations[$version])); + } + $version = new Version($this, $version, $class); + $this->migrations[$version->getVersion()] = $version; + ksort($this->migrations); + return $version; + } + + /** + * Register an array of migrations. Each key of the array is the version and + * the value is the migration class name. + * + * + * @param array $migrations + * @return void + */ + public function registerMigrations(array $migrations) + { + $versions = array(); + foreach ($migrations as $version => $class) { + $versions[] = $this->registerMigration($version, $class); + } + return $versions; + } + + /** + * Get the array of registered migration versions. + * + * @return array $migrations + */ + public function getMigrations() + { + return $this->migrations; + } + + /** + * Returns the Version instance for a given version in the format YYYYMMDDHHMMSS. + * + * @param string $version The version string in the format YYYYMMDDHHMMSS. + * @return Version $version + * @throws MigrationException $exception Throws exception if migration version does not exist. + */ + public function getVersion($version) + { + if ( ! isset($this->migrations[$version])) { + throw MigrationException::unknownMigrationVersion($version); + } + return $this->migrations[$version]; + } + + /** + * Check if a version exists. + * + * @param string $version + * @return bool $exists + */ + public function hasVersion($version) + { + return isset($this->migrations[$version]) ? true : false; + } + + /** + * Check if a version has been migrated or not yet + * + * @param Version $version + * @return bool $migrated + */ + public function hasVersionMigrated(Version $version) + { + $this->createMigrationTable(); + + $version = $this->connection->fetchColumn("SELECT version FROM " . $this->migrationsTableName . " WHERE version = ?", array($version->getVersion())); + return $version !== false ? true : false; + } + + /** + * Returns all migrated versions from the versions table, in an array. + * + * @return array $migrated + */ + public function getMigratedVersions() + { + $this->createMigrationTable(); + + $ret = $this->connection->fetchAll("SELECT version FROM " . $this->migrationsTableName); + $versions = array(); + foreach ($ret as $version) { + $versions[] = current($version); + } + + return $versions; + } + + /** + * Returns the current migrated version from the versions table. + * + * @return bool $currentVersion + */ + public function getCurrentVersion() + { + $this->createMigrationTable(); + + $sql = "SELECT version FROM " . $this->migrationsTableName . " ORDER BY version DESC"; + $sql = $this->connection->getDatabasePlatform()->modifyLimitQuery($sql, 1); + $result = $this->connection->fetchColumn($sql); + return $result !== false ? (string) $result : '0'; + } + + /** + * Returns the total number of executed migration versions + * + * @return integer $count + */ + public function getNumberOfExecutedMigrations() + { + $this->createMigrationTable(); + + $result = $this->connection->fetchColumn("SELECT COUNT(version) FROM " . $this->migrationsTableName); + return $result !== false ? $result : 0; + } + + /** + * Returns the total number of available migration versions + * + * @return integer $count + */ + public function getNumberOfAvailableMigrations() + { + return count($this->migrations); + } + + /** + * Returns the latest available migration version. + * + * @return string $version The version string in the format YYYYMMDDHHMMSS. + */ + public function getLatestVersion() + { + $versions = array_keys($this->migrations); + $latest = end($versions); + return $latest !== false ? (string) $latest : '0'; + } + + /** + * Create the migration table to track migrations with. + * + * @return bool $created Whether or not the table was created. + */ + public function createMigrationTable() + { + $this->validate(); + + if ($this->migrationTableCreated) { + return false; + } + + $schema = $this->connection->getSchemaManager()->createSchema(); + if ( ! $schema->hasTable($this->migrationsTableName)) { + $columns = array( + 'version' => new Column('version', Type::getType('string'), array('length' => 14)), + ); + $table = new Table($this->migrationsTableName, $columns); + $table->setPrimaryKey(array('version')); + $this->connection->getSchemaManager()->createTable($table); + + $this->migrationTableCreated = true; + + return true; + } + return false; + } + + /** + * Returns the array of migrations to executed based on the given direction + * and target version number. + * + * @param string $direction The direction we are migrating. + * @param string $to The version to migrate to. + * @return array $migrations The array of migrations we can execute. + */ + public function getMigrationsToExecute($direction, $to) + { + if ($direction === 'down') { + $allVersions = array_reverse(array_keys($this->migrations)); + $classes = array_reverse(array_values($this->migrations)); + $allVersions = array_combine($allVersions, $classes); + } else { + $allVersions = $this->migrations; + } + $versions = array(); + $migrated = $this->getMigratedVersions(); + foreach ($allVersions as $version) { + if ($this->shouldExecuteMigration($direction, $version, $to, $migrated)) { + $versions[$version->getVersion()] = $version; + } + } + return $versions; + } + + /** + * Check if we should execute a migration for a given direction and target + * migration version. + * + * @param string $direction The direction we are migrating. + * @param Version $version The Version instance to check. + * @param string $to The version we are migrating to. + * @param array $migrated Migrated versions array. + * @return void + */ + private function shouldExecuteMigration($direction, Version $version, $to, $migrated) + { + if ($direction === 'down') { + if ( ! in_array($version->getVersion(), $migrated)) { + return false; + } + return $version->getVersion() > $to ? true : false; + } else if ($direction === 'up') { + if (in_array($version->getVersion(), $migrated)) { + return false; + } + return $version->getVersion() <= $to ? true : false; + } + } +} diff --git a/library/Doctrine/DBAL/Migrations/Configuration/XmlConfiguration.php b/library/Doctrine/DBAL/Migrations/Configuration/XmlConfiguration.php new file mode 100644 index 0000000..627f99c --- /dev/null +++ b/library/Doctrine/DBAL/Migrations/Configuration/XmlConfiguration.php @@ -0,0 +1,58 @@ +. +*/ + +namespace Doctrine\DBAL\Migrations\Configuration; + +/** + * Load migration configuration information from a XML configuration file. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Jonathan H. Wage + */ +class XmlConfiguration extends AbstractFileConfiguration +{ + /** + * @inheritdoc + */ + protected function doLoad($file) + { + $xml = simplexml_load_file($file); + if (isset($xml->name)) { + $this->setName((string) $xml->name); + } + if (isset($xml->table['name'])) { + $this->setMigrationsTableName((string) $xml->table['name']); + } + if (isset($xml->{'migrations-namespace'})) { + $this->setMigrationsNamespace((string) $xml->{'migrations-namespace'}); + } + if (isset($xml->{'migrations-directory'})) { + $migrationsDirectory = $this->getDirectoryRelativeToFile($file, (string) $xml->{'migrations-directory'}); + $this->setMigrationsDirectory($migrationsDirectory); + $this->registerMigrationsFromDirectory($migrationsDirectory); + } + if (isset($xml->migrations->migration)) { + foreach ($xml->migrations->migration as $migration) { + $this->registerMigration((string) $migration['version'], (string) $migration['class']); + } + } + } +} \ No newline at end of file diff --git a/library/Doctrine/DBAL/Migrations/Configuration/YamlConfiguration.php b/library/Doctrine/DBAL/Migrations/Configuration/YamlConfiguration.php new file mode 100644 index 0000000..7c16cb2 --- /dev/null +++ b/library/Doctrine/DBAL/Migrations/Configuration/YamlConfiguration.php @@ -0,0 +1,61 @@ +. +*/ + +namespace Doctrine\DBAL\Migrations\Configuration; + +use Symfony\Component\Yaml\Yaml; + +/** + * Load migration configuration information from a YAML configuration file. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Jonathan H. Wage + */ +class YamlConfiguration extends AbstractFileConfiguration +{ + /** + * @inheritdoc + */ + protected function doLoad($file) + { + $array = Yaml::parse($file); + + if (isset($array['name'])) { + $this->setName($array['name']); + } + if (isset($array['table_name'])) { + $this->setMigrationsTableName($array['table_name']); + } + if (isset($array['migrations_namespace'])) { + $this->setMigrationsNamespace($array['migrations_namespace']); + } + if (isset($array['migrations_directory'])) { + $migrationsDirectory = $this->getDirectoryRelativeToFile($file, $array['migrations_directory']); + $this->setMigrationsDirectory($migrationsDirectory); + $this->registerMigrationsFromDirectory($migrationsDirectory); + } + if (isset($array['migrations']) && is_array($array['migrations'])) { + foreach ($array['migrations'] as $migration) { + $this->registerMigration($migration['version'], $migration['class']); + } + } + } +} diff --git a/library/Doctrine/DBAL/Migrations/IrreversibleMigrationException.php b/library/Doctrine/DBAL/Migrations/IrreversibleMigrationException.php new file mode 100644 index 0000000..ae93cc3 --- /dev/null +++ b/library/Doctrine/DBAL/Migrations/IrreversibleMigrationException.php @@ -0,0 +1,33 @@ +. +*/ + +namespace Doctrine\DBAL\Migrations; + +/** + * Exception to be thrown in the down() methods of migrations that signifies it + * is an irreversible migration and stops execution. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Jonathan H. Wage + */ +class IrreversibleMigrationException extends \Exception +{ +} \ No newline at end of file diff --git a/library/Doctrine/DBAL/Migrations/Migration.php b/library/Doctrine/DBAL/Migrations/Migration.php new file mode 100644 index 0000000..c14860a --- /dev/null +++ b/library/Doctrine/DBAL/Migrations/Migration.php @@ -0,0 +1,160 @@ +. +*/ + +namespace Doctrine\DBAL\Migrations; + +use Doctrine\DBAL\Migrations\Configuration\Configuration, + Doctrine\DBAL\Schema\Schema; + +/** + * Class for running migrations to the current version or a manually specified version. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Jonathan H. Wage + */ +class Migration +{ + /** + * The OutputWriter object instance used for outputting information + * + * @var OutputWriter + */ + private $outputWriter; + + /** + * @var Configuration + */ + private $configuration; + + /** + * Construct a Migration instance + * + * @param Configuration $configuration A migration Configuration instance + */ + public function __construct(Configuration $configuration) + { + $this->configuration = $configuration; + $this->outputWriter = $configuration->getOutputWriter(); + } + + /** + * Get the array of versions and SQL queries that would be executed for + * each version but do not execute anything. + * + * @param string $to The version to migrate to. + * @return array $sql The array of SQL queries. + */ + public function getSql($to = null) + { + return $this->migrate($to, true); + } + + /** + * Write a migration SQL file to the given path + * + * @param string $path The path to write the migration SQL file. + * @param string $to The version to migrate to. + * @return bool $written + */ + public function writeSqlFile($path, $to = null) + { + $sql = $this->getSql($to); + + $from = $this->configuration->getCurrentVersion(); + if ($to === null) { + $to = $this->configuration->getLatestVersion(); + } + + $string = sprintf("# Doctrine Migration File Generated on %s\n", date('Y-m-d H:m:s')); + $string .= sprintf("# Migrating from %s to %s\n", $from, $to); + + foreach ($sql as $version => $queries) { + $string .= "\n# Version " . $version . "\n"; + foreach ($queries as $query) { + $string .= $query . ";\n"; + } + } + if (is_dir($path)) { + $path = realpath($path); + $path = $path . '/doctrine_migration_' . date('YmdHis') . '.sql'; + } + + $this->outputWriter->write("\n".sprintf('Writing migration file to "%s"', $path)); + + return file_put_contents($path, $string); + } + + /** + * Run a migration to the current version or the given target version. + * + * @param string $to The version to migrate to. + * @param string $dryRun Whether or not to make this a dry run and not execute anything. + * @return array $sql The array of migration sql statements + * @throws MigrationException + */ + public function migrate($to = null, $dryRun = false) + { + if ($to === null) { + $to = $this->configuration->getLatestVersion(); + } + + $from = $this->configuration->getCurrentVersion(); + $from = (string) $from; + $to = (string) $to; + + $migrations = $this->configuration->getMigrations(); + if ( ! isset($migrations[$to]) && $to > 0) { + throw MigrationException::unknownMigrationVersion($to); + } + + if ($from === $to) { + return array(); + } + + $direction = $from > $to ? 'down' : 'up'; + $migrations = $this->configuration->getMigrationsToExecute($direction, $to); + + if ($dryRun === false) { + $this->outputWriter->write(sprintf('Migrating %s to %s from %s', $direction, $to, $from)); + } else { + $this->outputWriter->write(sprintf('Executing dry run of migration %s to %s from %s', $direction, $to, $from)); + } + + if (empty($migrations)) { + throw MigrationException::noMigrationsToExecute(); + } + + $sql = array(); + $time = 0; + foreach ($migrations as $version) { + $versionSql = $version->execute($direction, $dryRun); + $sql[$version->getVersion()] = $versionSql; + $time += $version->getTime(); + } + + $this->outputWriter->write("\n ------------------------\n"); + $this->outputWriter->write(sprintf(" ++ finished in %s", $time)); + $this->outputWriter->write(sprintf(" ++ %s migrations executed", count($migrations))); + $this->outputWriter->write(sprintf(" ++ %s sql queries", count($sql, true) - count($sql))); + + return $sql; + } +} diff --git a/library/Doctrine/DBAL/Migrations/MigrationException.php b/library/Doctrine/DBAL/Migrations/MigrationException.php new file mode 100644 index 0000000..d7f8afc --- /dev/null +++ b/library/Doctrine/DBAL/Migrations/MigrationException.php @@ -0,0 +1,66 @@ +. +*/ + +namespace Doctrine\DBAL\Migrations; + +/** + * Class for Migrations specific exceptions + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Jonathan H. Wage + */ +class MigrationException extends \Exception +{ + public static function migrationsNamespaceRequired() + { + return new self('Migrations namespace must be configured in order to use Doctrine migrations.', 2); + } + + public static function migrationsDirectoryRequired() + { + return new self('Migrations directory must be configured in order to use Doctrine migrations.', 3); + } + + public static function noMigrationsToExecute() + { + return new self('Could not find any migrations to execute.', 4); + } + + public static function unknownMigrationVersion($version) + { + return new self(sprintf('Could not find migration version %s', $version), 5); + } + + public static function alreadyAtVersion($version) + { + return new self(sprintf('Database is already at version %s', $version), 6); + } + + public static function duplicateMigrationVersion($version, $class) + { + return new self(sprintf('Migration version %s already registered with class %s', $version, $class), 7); + } + + public static function configurationFileAlreadyLoaded() + { + return new self(sprintf('Migrations configuration file already loaded'), 8); + } +} diff --git a/library/Doctrine/DBAL/Migrations/MigrationsVersion.php b/library/Doctrine/DBAL/Migrations/MigrationsVersion.php new file mode 100644 index 0000000..2c47932 --- /dev/null +++ b/library/Doctrine/DBAL/Migrations/MigrationsVersion.php @@ -0,0 +1,26 @@ +. + */ + + +namespace Doctrine\DBAL\Migrations; + +class MigrationsVersion +{ + const VERSION = '2.0.0-DEV'; +} \ No newline at end of file diff --git a/library/Doctrine/DBAL/Migrations/OutputWriter.php b/library/Doctrine/DBAL/Migrations/OutputWriter.php new file mode 100644 index 0000000..13f14a2 --- /dev/null +++ b/library/Doctrine/DBAL/Migrations/OutputWriter.php @@ -0,0 +1,52 @@ +. +*/ + +namespace Doctrine\DBAL\Migrations; + +/** + * Simple class for outputting information from migrations. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Jonathan H. Wage + */ +class OutputWriter +{ + private $closure; + + public function __construct(\Closure $closure = null) + { + if ($closure === null) { + $closure = function($message) {}; + } + $this->closure = $closure; + } + + /** + * Write output using the configured closure. + * + * @param string $message The message to write. + */ + public function write($message) + { + $closure = $this->closure; + $closure($message); + } +} \ No newline at end of file diff --git a/library/Doctrine/DBAL/Migrations/SkipMigrationException.php b/library/Doctrine/DBAL/Migrations/SkipMigrationException.php new file mode 100644 index 0000000..7f4dbe7 --- /dev/null +++ b/library/Doctrine/DBAL/Migrations/SkipMigrationException.php @@ -0,0 +1,8 @@ +. + */ + +namespace Doctrine\DBAL\Migrations\Tools\Console\Command; + +use Symfony\Component\Console\Command\Command, + Symfony\Component\Console\Input\InputInterface, + Symfony\Component\Console\Output\OutputInterface, + Symfony\Component\Console\Input\InputOption, + Doctrine\DBAL\Migrations\Migration, + Doctrine\DBAL\Migrations\MigrationException, + Doctrine\DBAL\Migrations\OutputWriter, + Doctrine\DBAL\Migrations\Configuration\Configuration, + Doctrine\DBAL\Migrations\Configuration\YamlConfiguration, + Doctrine\DBAL\Migrations\Configuration\XmlConfiguration; + +/** + * CLI Command for adding and deleting migration versions from the version table. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Jonathan Wage + */ +abstract class AbstractCommand extends Command +{ + /** + * @var Configuration + */ + private $configuration; + + protected function configure() + { + $this->addOption('configuration', null, InputOption::VALUE_OPTIONAL, 'The path to a migrations configuration file.'); + $this->addOption('db-configuration', null, InputOption::VALUE_OPTIONAL, 'The path to a database connection configuration file.'); + } + + protected function outputHeader(Configuration $configuration, OutputInterface $output) + { + $name = $configuration->getName(); + $name = $name ? $name : 'Doctrine Database Migrations'; + $name = str_repeat(' ', 20) . $name . str_repeat(' ', 20); + $output->writeln('' . str_repeat(' ', strlen($name)) . ''); + $output->writeln('' . $name . ''); + $output->writeln('' . str_repeat(' ', strlen($name)) . ''); + $output->writeln(''); + } + + public function setMigrationConfiguration(Configuration $config) + { + $this->configuration = $config; + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + * @return Configuration + */ + protected function getMigrationConfiguration(InputInterface $input, OutputInterface $output) + { + if ( ! $this->configuration) { + $outputWriter = new OutputWriter(function($message) use ($output) { + return $output->writeln($message); + }); + + if ($this->getApplication()->getHelperSet()->has('db')) { + $conn = $this->getHelper('db')->getConnection(); + } else if($input->getOption('db-configuration')) { + if (!file_exists($input->getOption('db-configuration'))) { + throw new \InvalidArgumentException("The specified connection file is not a valid file."); + } + + $params = include($input->getOption('db-configuration')); + if (!is_array($params)) { + throw new \InvalidArgumentException('The connection file has to return an array with database configuration parameters.'); + } + $conn = \Doctrine\DBAL\DriverManager::getConnection($params); + } else if (file_exists('migrations-db.php')) { + $params = include("migrations-db.php"); + if (!is_array($params)) { + throw new \InvalidArgumentException('The connection file has to return an array with database configuration parameters.'); + } + $conn = \Doctrine\DBAL\DriverManager::getConnection($params); + } else { + throw new \InvalidArgumentException('You have to specify a --db-configuration file or pass a Database Connection as a dependency to the Migrations.'); + } + + if ($input->getOption('configuration')) { + $info = pathinfo($input->getOption('configuration')); + $class = $info['extension'] === 'xml' ? 'Doctrine\DBAL\Migrations\Configuration\XmlConfiguration' : 'Doctrine\DBAL\Migrations\Configuration\YamlConfiguration'; + $configuration = new $class($conn, $outputWriter); + $configuration->load($input->getOption('configuration')); + } else if (file_exists('migrations.xml')) { + $configuration = new XmlConfiguration($conn, $outputWriter); + $configuration->load('migrations.xml'); + } else if (file_exists('migrations.yml')) { + $configuration = new YamlConfiguration($conn, $outputWriter); + $configuration->load('migrations.yml'); + } else { + $configuration = new Configuration($conn, $outputWriter); + } + $this->configuration = $configuration; + } + return $this->configuration; + } +} diff --git a/library/Doctrine/DBAL/Migrations/Tools/Console/Command/DiffCommand.php b/library/Doctrine/DBAL/Migrations/Tools/Console/Command/DiffCommand.php new file mode 100644 index 0000000..1a0de77 --- /dev/null +++ b/library/Doctrine/DBAL/Migrations/Tools/Console/Command/DiffCommand.php @@ -0,0 +1,106 @@ +. + */ + +namespace Doctrine\DBAL\Migrations\Tools\Console\Command; + +use Symfony\Component\Console\Input\InputInterface, + Symfony\Component\Console\Output\OutputInterface, + Symfony\Component\Console\Input\InputArgument, + Symfony\Component\Console\Input\InputOption, + Doctrine\ORM\Tools\SchemaTool, + Doctrine\DBAL\Migrations\Configuration\Configuration; + +/** + * Command for generate migration classes by comparing your current database schema + * to your mapping information. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Jonathan Wage + */ +class DiffCommand extends GenerateCommand +{ + protected function configure() + { + parent::configure(); + + $this + ->setName('migrations:diff') + ->setDescription('Generate a migration by comparing your current database to your mapping information.') + ->setHelp(<<%command.name% command generates a migration by comparing your current database to your mapping information: + + %command.full_name% + +You can optionally specify a --editor-cmd option to open the generated file in your favorite editor: + + %command.full_name% --editor-cmd=mate +EOT + ); + + } + + public function execute(InputInterface $input, OutputInterface $output) + { + $configuration = $this->getMigrationConfiguration($input, $output); + + $em = $this->getHelper('em')->getEntityManager(); + $conn = $em->getConnection(); + $platform = $conn->getDatabasePlatform(); + $metadata = $em->getMetadataFactory()->getAllMetadata(); + + if (empty($metadata)) { + $output->writeln('No mapping information to process.', 'ERROR'); + return; + } + + $tool = new SchemaTool($em); + + $fromSchema = $conn->getSchemaManager()->createSchema(); + $toSchema = $tool->getSchemaFromMetadata($metadata); + $up = $this->buildCodeFromSql($configuration, $fromSchema->getMigrateToSql($toSchema, $platform)); + $down = $this->buildCodeFromSql($configuration, $fromSchema->getMigrateFromSql($toSchema, $platform)); + + if ( ! $up && ! $down) { + $output->writeln('No changes detected in your mapping information.', 'ERROR'); + return; + } + + $version = date('YmdHis'); + $path = $this->generateMigration($configuration, $input, $version, $up, $down); + + $output->writeln(sprintf('Generated new migration class to "%s" from schema differences.', $path)); + } + + private function buildCodeFromSql(Configuration $configuration, array $sql) + { + $currentPlatform = $configuration->getConnection()->getDatabasePlatform()->getName(); + $code = array( + "\$this->abortIf(\$this->connection->getDatabasePlatform()->getName() != \"$currentPlatform\");", "", + ); + foreach ($sql as $query) { + if (strpos($query, $configuration->getMigrationsTableName()) !== false) { + continue; + } + $code[] = "\$this->addSql(\"$query\");"; + } + return implode("\n", $code); + } +} diff --git a/library/Doctrine/DBAL/Migrations/Tools/Console/Command/ExecuteCommand.php b/library/Doctrine/DBAL/Migrations/Tools/Console/Command/ExecuteCommand.php new file mode 100644 index 0000000..ceb7c09 --- /dev/null +++ b/library/Doctrine/DBAL/Migrations/Tools/Console/Command/ExecuteCommand.php @@ -0,0 +1,102 @@ +. + */ + +namespace Doctrine\DBAL\Migrations\Tools\Console\Command; + +use Symfony\Component\Console\Input\InputInterface, + Symfony\Component\Console\Output\OutputInterface, + Symfony\Component\Console\Input\InputArgument, + Symfony\Component\Console\Input\InputOption, + Doctrine\DBAL\Migrations\Migration, + Doctrine\DBAL\Migrations\Configuration\Configuration, + Doctrine\DBAL\Migrations\Configuration\YamlConfiguration, + Doctrine\DBAL\Migrations\Configuration\XmlConfiguration; + +/** + * Command for executing single migrations up or down manually. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Jonathan Wage + */ +class ExecuteCommand extends AbstractCommand +{ + protected function configure() + { + $this + ->setName('migrations:execute') + ->setDescription('Execute a single migration version up or down manually.') + ->addArgument('version', InputArgument::REQUIRED, 'The version to execute.', null) + ->addOption('write-sql', null, InputOption::VALUE_NONE, 'The path to output the migration SQL file instead of executing it.') + ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Execute the migration as a dry run.') + ->addOption('up', null, InputOption::VALUE_NONE, 'Execute the migration down.') + ->addOption('down', null, InputOption::VALUE_NONE, 'Execute the migration down.') + ->setHelp(<<%command.name% command executes a single migration version up or down manually: + + %command.full_name% YYYYMMDDHHMMSS + +If no --up or --down option is specified it defaults to up: + + %command.full_name% YYYYMMDDHHMMSS --down + +You can also execute the migration as a --dry-run: + + %command.full_name% YYYYMMDDHHMMSS --dry-run + +You can output the would be executed SQL statements to a file with --write-sql: + + %command.full_name% YYYYMMDDHHMMSS --write-sql + +Or you can also execute the migration without a warning message wich you need to interact with: + + %command.full_name% --no-interaction +EOT + ); + + parent::configure(); + } + + public function execute(InputInterface $input, OutputInterface $output) + { + $version = $input->getArgument('version'); + $direction = $input->getOption('down') ? 'down' : 'up'; + + $configuration = $this->getMigrationConfiguration($input, $output); + $version = $configuration->getVersion($version); + + if ($path = $input->getOption('write-sql')) { + $path = is_bool($path) ? getcwd() : $path; + $version->writeSqlFile($path, $direction); + } else { + $noInteraction = $input->getOption('no-interaction') ? true : false; + if ($noInteraction === true) { + $version->execute($direction, $input->getOption('dry-run') ? true : false); + } else { + $confirmation = $this->getHelper('dialog')->askConfirmation($output, 'WARNING! You are about to execute a database migration that could result in schema changes and data lost. Are you sure you wish to continue? (y/n)', false); + if ($confirmation === true) { + $version->execute($direction, $input->getOption('dry-run') ? true : false); + } else { + $output->writeln('Migration cancelled!'); + } + } + } + } +} diff --git a/library/Doctrine/DBAL/Migrations/Tools/Console/Command/GenerateCommand.php b/library/Doctrine/DBAL/Migrations/Tools/Console/Command/GenerateCommand.php new file mode 100644 index 0000000..600118e --- /dev/null +++ b/library/Doctrine/DBAL/Migrations/Tools/Console/Command/GenerateCommand.php @@ -0,0 +1,130 @@ +. + */ + +namespace Doctrine\DBAL\Migrations\Tools\Console\Command; + +use Symfony\Component\Console\Input\InputInterface, + Symfony\Component\Console\Output\OutputInterface, + Symfony\Component\Console\Input\InputArgument, + Symfony\Component\Console\Input\InputOption, + Doctrine\DBAL\Migrations\MigrationException, + Doctrine\DBAL\Migrations\Configuration\Configuration; + +/** + * Command for generating new blank migration classes + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Jonathan Wage + */ +class GenerateCommand extends AbstractCommand +{ + + private static $_template = + '; + +use Doctrine\DBAL\Migrations\AbstractMigration, + Doctrine\DBAL\Schema\Schema; + +/** + * Auto-generated Migration: Please modify to your need! + */ +class Version extends AbstractMigration +{ + public function up(Schema $schema) + { + // this up() migration is autogenerated, please modify it to your needs + + } + + public function down(Schema $schema) + { + // this down() migration is autogenerated, please modify it to your needs + + } +} +'; + + protected function configure() + { + $this + ->setName('migrations:generate') + ->setDescription('Generate a blank migration class.') + ->addOption('editor-cmd', null, InputOption::VALUE_OPTIONAL, 'Open file with this command upon creation.') + ->setHelp(<<%command.name% command generates a blank migration class: + + %command.full_name% + +You can optionally specify a --editor-cmd option to open the generated file in your favorite editor: + + %command.full_name% --editor-cmd=mate +EOT + ); + + parent::configure(); + } + + public function execute(InputInterface $input, OutputInterface $output) + { + $configuration = $this->getMigrationConfiguration($input, $output); + + $version = date('YmdHis'); + $path = $this->generateMigration($configuration, $input, $version); + + $output->writeln(sprintf('Generated new migration class to "%s"', $path)); + } + + protected function generateMigration(Configuration $configuration, InputInterface $input, $version, $up = null, $down = null) + { + $placeHolders = array( + '', + '', + '', + '' + ); + $replacements = array( + $configuration->getMigrationsNamespace(), + $version, + $up ? " " . implode("\n ", explode("\n", $up)) : null, + $down ? " " . implode("\n ", explode("\n", $down)) : null + ); + $code = str_replace($placeHolders, $replacements, self::$_template); + $dir = $configuration->getMigrationsDirectory(); + $dir = $dir ? $dir : getcwd(); + $dir = rtrim($dir, '/'); + $path = $dir . '/Version' . $version . '.php'; + + if (!file_exists($dir)) { + throw new \InvalidArgumentException(sprintf('Migrations directory "%s" does not exist.', $dir)); + } + + file_put_contents($path, $code); + + if ($editorCmd = $input->getOption('editor-cmd')) { + shell_exec($editorCmd . ' ' . escapeshellarg($path)); + } + + return $path; + } +} \ No newline at end of file diff --git a/library/Doctrine/DBAL/Migrations/Tools/Console/Command/MigrateCommand.php b/library/Doctrine/DBAL/Migrations/Tools/Console/Command/MigrateCommand.php new file mode 100644 index 0000000..c1559f5 --- /dev/null +++ b/library/Doctrine/DBAL/Migrations/Tools/Console/Command/MigrateCommand.php @@ -0,0 +1,107 @@ +. + */ + +namespace Doctrine\DBAL\Migrations\Tools\Console\Command; + +use Symfony\Component\Console\Input\InputInterface, + Symfony\Component\Console\Output\OutputInterface, + Symfony\Component\Console\Input\InputArgument, + Symfony\Component\Console\Input\InputOption, + Doctrine\DBAL\Migrations\Migration, + Doctrine\DBAL\Migrations\Configuration\Configuration, + Doctrine\DBAL\Migrations\Configuration\YamlConfiguration, + Doctrine\DBAL\Migrations\Configuration\XmlConfiguration; + +/** + * Command for executing a migration to a specified version or the latest available version. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Jonathan Wage + */ +class MigrateCommand extends AbstractCommand +{ + protected function configure() + { + $this + ->setName('migrations:migrate') + ->setDescription('Execute a migration to a specified version or the latest available version.') + ->addArgument('version', InputArgument::OPTIONAL, 'The version to migrate to.', null) + ->addOption('write-sql', null, InputOption::VALUE_NONE, 'The path to output the migration SQL file instead of executing it.') + ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Execute the migration as a dry run.') + ->setHelp(<<%command.name% command executes a migration to a specified version or the latest available version: + + %command.full_name% + +You can optionally manually specify the version you wish to migrate to: + + %command.full_name% YYYYMMDDHHMMSS + +You can also execute the migration as a --dry-run: + + %command.full_name% YYYYMMDDHHMMSS --dry-run + +You can output the would be executed SQL statements to a file with --write-sql: + + %command.full_name% YYYYMMDDHHMMSS --write-sql + +Or you can also execute the migration without a warning message wich you need to interact with: + + %command.full_name% --no-interaction + +EOT + ); + + parent::configure(); + } + + public function execute(InputInterface $input, OutputInterface $output) + { + $version = $input->getArgument('version'); + + $configuration = $this->getMigrationConfiguration($input, $output); + $migration = new Migration($configuration); + + $this->outputHeader($configuration, $output); + + if ($path = $input->getOption('write-sql')) { + $path = is_bool($path) ? getcwd() : $path; + $migration->writeSqlFile($path, $version); + } else { + $dryRun = $input->getOption('dry-run') ? true : false; + if ($dryRun === true) { + $migration->migrate($version, true); + } else { + $noInteraction = $input->getOption('no-interaction') ? true : false; + if ($noInteraction === true) { + $migration->migrate($version, $dryRun); + } else { + $confirmation = $this->getHelper('dialog')->askConfirmation($output, 'WARNING! You are about to execute a database migration that could result in schema changes and data lost. Are you sure you wish to continue? (y/n)', false); + if ($confirmation === true) { + $migration->migrate($version, $dryRun); + } else { + $output->writeln('Migration cancelled!'); + } + } + } + } + } +} diff --git a/library/Doctrine/DBAL/Migrations/Tools/Console/Command/StatusCommand.php b/library/Doctrine/DBAL/Migrations/Tools/Console/Command/StatusCommand.php new file mode 100644 index 0000000..b736d28 --- /dev/null +++ b/library/Doctrine/DBAL/Migrations/Tools/Console/Command/StatusCommand.php @@ -0,0 +1,115 @@ +. + */ + +namespace Doctrine\DBAL\Migrations\Tools\Console\Command; + +use Symfony\Component\Console\Input\InputInterface, + Symfony\Component\Console\Output\OutputInterface, + Symfony\Component\Console\Input\InputArgument, + Symfony\Component\Console\Input\InputOption, + Doctrine\DBAL\Migrations\Migration, + Doctrine\DBAL\Migrations\MigrationException, + Doctrine\DBAL\Migrations\Configuration\Configuration, + Doctrine\DBAL\Migrations\Configuration\YamlConfiguration, + Doctrine\DBAL\Migrations\Configuration\XmlConfiguration; + +/** + * Command to view the status of a set of migrations. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Jonathan Wage + */ +class StatusCommand extends AbstractCommand +{ + protected function configure() + { + $this + ->setName('migrations:status') + ->setDescription('View the status of a set of migrations.') + ->addOption('show-versions', null, InputOption::VALUE_NONE, 'This will display a list of all available migrations and their status') + ->setHelp(<<%command.name% command outputs the status of a set of migrations: + + %command.full_name% + +You can output a list of all available migrations and their status with --show-versions: + + %command.full_name% --show-versions +EOT + ); + + parent::configure(); + } + + public function execute(InputInterface $input, OutputInterface $output) + { + $configuration = $this->getMigrationConfiguration($input, $output); + + $currentVersion = $configuration->getCurrentVersion(); + if ($currentVersion) { + $currentVersionFormatted = $configuration->formatVersion($currentVersion) . ' ('.$currentVersion.')'; + } else { + $currentVersionFormatted = 0; + } + $latestVersion = $configuration->getLatestVersion(); + if ($latestVersion) { + $latestVersionFormatted = $configuration->formatVersion($latestVersion) . ' ('.$latestVersion.')'; + } else { + $latestVersionFormatted = 0; + } + $executedMigrations = $configuration->getNumberOfExecutedMigrations(); + $availableMigrations = $configuration->getNumberOfAvailableMigrations(); + $newMigrations = $availableMigrations - $executedMigrations; + + $output->writeln("\n == Configuration\n"); + + $info = array( + 'Name' => $configuration->getName() ? $configuration->getName() : 'Doctrine Database Migrations', + 'Database Driver' => $configuration->getConnection()->getDriver()->getName(), + 'Database Name' => $configuration->getConnection()->getDatabase(), + 'Configuration Source' => $configuration instanceof \Doctrine\DBAL\Migrations\Configuration\AbstractFileConfiguration ? $configuration->getFile() : 'manually configured', + 'Version Table Name' => $configuration->getMigrationsTableName(), + 'Migrations Namespace' => $configuration->getMigrationsNamespace(), + 'Migrations Directory' => $configuration->getMigrationsDirectory(), + 'Current Version' => $currentVersionFormatted, + 'Latest Version' => $latestVersionFormatted, + 'Executed Migrations' => $executedMigrations, + 'Available Migrations' => $availableMigrations, + 'New Migrations' => $newMigrations > 0 ? '' . $newMigrations . '' : $newMigrations + ); + foreach ($info as $name => $value) { + $output->writeln(' >> ' . $name . ': ' . str_repeat(' ', 50 - strlen($name)) . $value); + } + + $showVersions = $input->getOption('show-versions') ? true : false; + if ($showVersions === true) { + if ($migrations = $configuration->getMigrations()) { + $output->writeln("\n == Migration Versions\n"); + $migratedVersions = $configuration->getMigratedVersions(); + foreach ($migrations as $version) { + $isMigrated = in_array($version->getVersion(), $migratedVersions); + $status = $isMigrated ? 'migrated' : 'not migrated'; + $output->writeln(' >> ' . $configuration->formatVersion($version->getVersion()) . ' (' . $version->getVersion() . ')' . str_repeat(' ', 30 - strlen($name)) . $status); + } + } + } + } +} diff --git a/library/Doctrine/DBAL/Migrations/Tools/Console/Command/VersionCommand.php b/library/Doctrine/DBAL/Migrations/Tools/Console/Command/VersionCommand.php new file mode 100644 index 0000000..1c2497b --- /dev/null +++ b/library/Doctrine/DBAL/Migrations/Tools/Console/Command/VersionCommand.php @@ -0,0 +1,95 @@ +. + */ + +namespace Doctrine\DBAL\Migrations\Tools\Console\Command; + +use Symfony\Component\Console\Input\InputInterface, + Symfony\Component\Console\Output\OutputInterface, + Symfony\Component\Console\Input\InputArgument, + Symfony\Component\Console\Input\InputOption, + Doctrine\DBAL\Migrations\Migration, + Doctrine\DBAL\Migrations\MigrationException, + Doctrine\DBAL\Migrations\Configuration\Configuration, + Doctrine\DBAL\Migrations\Configuration\YamlConfiguration, + Doctrine\DBAL\Migrations\Configuration\XmlConfiguration; + +/** + * Command for manually adding and deleting migration versions from the version table. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Jonathan Wage + */ +class VersionCommand extends AbstractCommand +{ + protected function configure() + { + $this + ->setName('migrations:version') + ->setDescription('Manually add and delete migration versions from the version table.') + ->addArgument('version', InputArgument::REQUIRED, 'The version to add or delete.', null) + ->addOption('add', null, InputOption::VALUE_NONE, 'Add the specified version.') + ->addOption('delete', null, InputOption::VALUE_NONE, 'Delete the specified version.') + ->setHelp(<<%command.name% command allows you to manually add and delete migration versions from the version table: + + %command.full_name% YYYYMMDDHHMMSS --add + +If you want to delete a version you can use the --delete option: + + %command.full_name% YYYYMMDDHHMMSS --delete +EOT + ); + + parent::configure(); + } + + public function execute(InputInterface $input, OutputInterface $output) + { + $configuration = $this->getMigrationConfiguration($input, $output); + $migration = new Migration($configuration); + + if ($input->getOption('add') === false && $input->getOption('delete') === false) { + throw new \InvalidArgumentException('You must specify whether you want to --add or --delete the specified version.'); + } + + $version = $input->getArgument('version'); + $markMigrated = $input->getOption('add') ? true : false; + + if ( ! $configuration->hasVersion($version)) { + throw MigrationException::unknownMigrationVersion($version); + } + + $version = $configuration->getVersion($version); + if ($markMigrated && $configuration->hasVersionMigrated($version)) { + throw new \InvalidArgumentException(sprintf('The version "%s" already exists in the version table.', $version)); + } + + if ( ! $markMigrated && ! $configuration->hasVersionMigrated($version)) { + throw new \InvalidArgumentException(sprintf('The version "%s" does not exists in the version table.', $version)); + } + + if ($markMigrated) { + $version->markMigrated(); + } else { + $version->markNotMigrated(); + } + } +} diff --git a/library/Doctrine/DBAL/Migrations/Version.php b/library/Doctrine/DBAL/Migrations/Version.php new file mode 100644 index 0000000..a0333de --- /dev/null +++ b/library/Doctrine/DBAL/Migrations/Version.php @@ -0,0 +1,353 @@ +. +*/ + +namespace Doctrine\DBAL\Migrations; + +use Doctrine\DBAL\Migrations\Configuration\Configuration, + Doctrine\DBAL\Schema\Schema; + +/** + * Class which wraps a migration version and allows execution of the + * individual migration version up or down method. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Jonathan H. Wage + */ +class Version +{ + const STATE_NONE = 0; + const STATE_PRE = 1; + const STATE_EXEC = 2; + const STATE_POST = 3; + + /** + * The Migrations Configuration instance for this migration + * + * @var Configuration + */ + private $configuration; + + /** + * The OutputWriter object instance used for outputting information + * + * @var OutputWriter + */ + private $outputWriter; + + /** + * The version in timestamp format (YYYYMMDDHHMMSS) + * + * @param int + */ + private $version; + + /** + * @var AbstractSchemaManager + */ + private $sm; + + /** + * @var AbstractPlatform + */ + private $platform; + + /** + * The migration instance for this version + * + * @var AbstractMigration + */ + private $migration; + + /** + * @var Connection + */ + private $connection; + + /** + * @var string + */ + private $class; + + /** The array of collected SQL statements for this version */ + private $sql = array(); + + /** The array of collected parameters for SQL statements for this version */ + private $params = array(); + + /** The array of collected types for SQL statements for this version */ + private $types = array(); + + /** The time in seconds that this migration version took to execute */ + private $time; + + /** + * @var int + */ + private $state = self::STATE_NONE; + + public function __construct(Configuration $configuration, $version, $class) + { + $this->configuration = $configuration; + $this->outputWriter = $configuration->getOutputWriter(); + $this->class = $class; + $this->connection = $configuration->getConnection(); + $this->sm = $this->connection->getSchemaManager(); + $this->platform = $this->connection->getDatabasePlatform(); + $this->migration = new $class($this); + $this->version = $this->migration->getName() ?: $version; + } + + /** + * Returns the string version in the format YYYYMMDDHHMMSS + * + * @return string $version + */ + public function getVersion() + { + return $this->version; + } + + /** + * Returns the Migrations Configuration object instance + * + * @return Configuration $configuration + */ + public function getConfiguration() + { + return $this->configuration; + } + + /** + * Check if this version has been migrated or not. + * + * @param bool $bool + * @return mixed + */ + public function isMigrated() + { + return $this->configuration->hasVersionMigrated($this); + } + + public function markMigrated() + { + $this->configuration->createMigrationTable(); + $this->connection->executeQuery("INSERT INTO " . $this->configuration->getMigrationsTableName() . " (version) VALUES (?)", array($this->version)); + } + + public function markNotMigrated() + { + $this->configuration->createMigrationTable(); + $this->connection->executeQuery("DELETE FROM " . $this->configuration->getMigrationsTableName() . " WHERE version = ?", array($this->version)); + } + + /** + * Add some SQL queries to this versions migration + * + * @param mixed $sql + * @param array $params + * @param array $types + * @return void + */ + public function addSql($sql, array $params = array(), array $types = array()) + { + if (is_array($sql)) { + foreach ($sql as $key => $query) { + $this->sql[] = $query; + if (isset($params[$key])) { + $this->params[count($this->sql) - 1] = $params[$key]; + $this->types[count($this->sql) - 1] = isset($types[$key]) ? $types[$key] : array(); + } + } + } else { + $this->sql[] = $sql; + if ($params) { + $this->params[count($this->sql) - 1] = $params; + $this->types[count($this->sql) - 1] = $types ?: array(); + } + } + } + + /** + * Write a migration SQL file to the given path + * + * @param string $path The path to write the migration SQL file. + * @param string $direction The direction to execute. + * @return bool $written + */ + public function writeSqlFile($path, $direction = 'up') + { + $queries = $this->execute($direction, true); + + $string = sprintf("# Doctrine Migration File Generated on %s\n", date('Y-m-d H:m:s')); + + $string .= "\n# Version " . $this->version . "\n"; + foreach ($queries as $query) { + $string .= $query . ";\n"; + } + if (is_dir($path)) { + $path = realpath($path); + $path = $path . '/doctrine_migration_' . date('YmdHis') . '.sql'; + } + + $this->outputWriter->write("\n".sprintf('Writing migration file to "%s"', $path)); + + return file_put_contents($path, $string); + } + + /** + * @return AbstractMigration + */ + public function getMigration() + { + return $this->migration; + } + + /** + * Execute this migration version up or down and and return the SQL. + * + * @param string $direction The direction to execute the migration. + * @param string $dryRun Whether to not actually execute the migration SQL and just do a dry run. + * @return array $sql + * @throws Exception when migration fails + */ + public function execute($direction, $dryRun = false) + { + $this->sql = array(); + + $this->connection->beginTransaction(); + + try { + $start = microtime(true); + + $this->state = self::STATE_PRE; + $fromSchema = $this->sm->createSchema(); + $this->migration->{'pre' . ucfirst($direction)}($fromSchema); + + if ($direction === 'up') { + $this->outputWriter->write("\n" . sprintf(' ++ migrating %s', $this->version) . "\n"); + } else { + $this->outputWriter->write("\n" . sprintf(' -- reverting %s', $this->version) . "\n"); + } + + $this->state = self::STATE_EXEC; + + $toSchema = clone $fromSchema; + $this->migration->$direction($toSchema); + $this->addSql($fromSchema->getMigrateToSql($toSchema, $this->platform)); + + if ($dryRun === false) { + if ($this->sql) { + foreach ($this->sql as $key => $query) { + if ( ! isset($this->params[$key])) { + $this->outputWriter->write(' -> ' . $query); + $this->connection->executeQuery($query); + } else { + $this->outputWriter->write(sprintf(' - %s (with parameters)', $query)); + $this->connection->executeQuery($query, $this->params[$key], $this->types[$key]); + } + } + } else { + $this->outputWriter->write(sprintf('Migration %s was executed but did not result in any SQL statements.', $this->version)); + } + + if ($direction === 'up') { + $this->markMigrated(); + } else { + $this->markNotMigrated(); + } + + } else { + foreach ($this->sql as $query) { + $this->outputWriter->write(' -> ' . $query); + } + } + + $this->state = self::STATE_POST; + $this->migration->{'post' . ucfirst($direction)}($toSchema); + + $end = microtime(true); + $this->time = round($end - $start, 2); + if ($direction === 'up') { + $this->outputWriter->write(sprintf("\n ++ migrated (%ss)", $this->time)); + } else { + $this->outputWriter->write(sprintf("\n -- reverted (%ss)", $this->time)); + } + + $this->connection->commit(); + + return $this->sql; + } catch(SkipMigrationException $e) { + $this->connection->rollback(); + + if ($dryRun == false) { + // now mark it as migrated + if ($direction === 'up') { + $this->markMigrated(); + } else { + $this->markNotMigrated(); + } + } + + $this->outputWriter->write(sprintf("\n SS skipped (Reason: %s)", $e->getMessage())); + } catch (\Exception $e) { + + $this->outputWriter->write(sprintf( + 'Migration %s failed during %s. Error %s', + $this->version, $this->getExecutionState(), $e->getMessage() + )); + + $this->connection->rollback(); + + $this->state = self::STATE_NONE; + throw $e; + } + $this->state = self::STATE_NONE; + } + + public function getExecutionState() + { + switch($this->state) { + case self::STATE_PRE: + return 'Pre-Checks'; + case self::STATE_POST: + return 'Post-Checks'; + case self::STATE_EXEC: + return 'Execution'; + default: + return 'No State'; + } + } + + /** + * Returns the time this migration version took to execute + * + * @return integer $time The time this migration version took to execute + */ + public function getTime() + { + return $this->time; + } + + public function __toString() + { + return $this->version; + } +}