diff --git a/.gitignore b/.gitignore index b5f8d30..4df88fe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ +vendor/ migrations/* test/migrations/version.txt *.swp mp-*.tgz package.xml +composer.lock +.php_cs.cache diff --git a/.php_cs.dist b/.php_cs.dist new file mode 100644 index 0000000..8ee97a5 --- /dev/null +++ b/.php_cs.dist @@ -0,0 +1,18 @@ +setRules(array( + '@Symfony' => true, + '@Symfony:risky' => true, + '@PHPUnit48Migration:risky' => true, + 'php_unit_no_expectation_annotation' => false, // part of `PHPUnitXYMigration:risky` ruleset, to be enabled when PHPUnit 4.x support will be dropped, as we don't want to rewrite exceptions handling twice + 'array_syntax' => array('syntax' => 'long'), + 'protected_to_private' => false, + )) + ->setRiskyAllowed(true) + ->setFinder( + PhpCsFixer\Finder::create() + ->in(__DIR__.'/') + ->append(array(__FILE__)) + ) +; diff --git a/Migrator.php b/Migrator.php index 1d5b89f..f233c25 100644 --- a/Migrator.php +++ b/Migrator.php @@ -1,10 +1,10 @@ - * + * @author Alan Pinstein + * * Copyright (c) 2009 Alan Pinstein * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -24,7 +24,6 @@ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. - * */ /** @@ -35,6 +34,7 @@ interface MigratorVersionProvider { public function setVersion($migrator, $v); + public function getVersion($migrator); } @@ -47,19 +47,20 @@ public function setVersion($migrator, $v) { file_put_contents($this->getVersionFilePath($migrator), $v); } + public function getVersion($migrator) { $versionFile = $this->getVersionFilePath($migrator); - if (!file_exists($versionFile)) - { + if (!file_exists($versionFile)) { $this->setVersion($migrator, Migrator::VERSION_ZERO); } + return file_get_contents($this->getVersionFilePath($migrator)); } private function getVersionFilePath($migrator) { - return $migrator->getMigrationsDirectory() . '/version.txt'; + return $migrator->getMigrationsDirectory().'/version.txt'; } } @@ -72,41 +73,40 @@ private function getVersionFilePath($migrator) */ class MigratorVersionProviderDB implements MigratorVersionProvider { - const OPT_SCHEMA = 'schema'; - const OPT_VERSION_TABLE_NAME = 'versionTableName'; + const OPT_SCHEMA = 'schema'; + const OPT_VERSION_TABLE_NAME = 'versionTableName'; - protected $schema = NULL; - protected $versionTableName = NULL; + protected $schema = null; + protected $versionTableName = null; /** * Create a new MigratorVersionProviderDB instance. * - * @param array A hash with option values, see {@link MigratorVersionProviderDB::OPT_SCHEMA OPT_SCHEMA} and {@link MigratorVersionProviderDB::OPT_VERSION_TABLE_NAME}. + * @param array a hash with option values, see {@link MigratorVersionProviderDB::OPT_SCHEMA OPT_SCHEMA} and {@link MigratorVersionProviderDB::OPT_VERSION_TABLE_NAME} */ public function __construct($opts = array()) { $opts = array_merge(array( - MigratorVersionProviderDB::OPT_SCHEMA => 'public', - MigratorVersionProviderDB::OPT_VERSION_TABLE_NAME => 'mp_version', + self::OPT_SCHEMA => 'public', + self::OPT_VERSION_TABLE_NAME => 'mp_version', ), $opts); - $this->schema = $opts[MigratorVersionProviderDB::OPT_SCHEMA]; - $this->versionTableName = $opts[MigratorVersionProviderDB::OPT_VERSION_TABLE_NAME]; + $this->schema = $opts[self::OPT_SCHEMA]; + $this->versionTableName = $opts[self::OPT_VERSION_TABLE_NAME]; } protected function initDB($migrator) { try { - $sql = "SELECT count(*) as version_table_count from information_schema.tables WHERE table_schema = '{$this->schema}' AND table_name = '{$this->versionTableName}';"; + $sql = "SELECT count(*) as version_table_count from information_schema.tables WHERE table_schema = '{$this->schema}' AND table_name = '{$this->versionTableName}';"; $row = $migrator->getDbCon()->query($sql)->fetch(); - if ($row['version_table_count'] == 0) - { + if (0 == $row['version_table_count']) { $sql = "create table {$this->schema}.{$this->versionTableName} ( version text ); insert into {$this->schema}.{$this->versionTableName} (version) values (0);"; $migrator->getDbCon()->exec($sql); } } catch (Exception $e) { - throw new Exception("Error initializing DB at [{$sql}]: " . $e->getMessage()); + throw new Exception("Error initializing DB at [{$sql}]: ".$e->getMessage()); } } @@ -114,7 +114,7 @@ public function setVersion($migrator, $v) { $this->initDB($migrator); - $sql = "update {$this->schema}.{$this->versionTableName} set version = " . $migrator->getDbCon()->quote($v) . ";"; + $sql = "update {$this->schema}.{$this->versionTableName} set version = ".$migrator->getDbCon()->quote($v).';'; $migrator->getDbCon()->exec($sql); } @@ -124,6 +124,7 @@ public function getVersion($migrator) $sql = "select version from {$this->schema}.{$this->versionTableName} limit 1"; $row = $migrator->getDbCon()->query($sql)->fetch(); + return $row['version']; } } @@ -144,7 +145,7 @@ public function getVersion($migrator) */ abstract class Migration { - protected $migrator = NULL; + protected $migrator = null; public function __construct($migrator) { @@ -156,13 +157,17 @@ public function __construct($migrator) * * @return string */ - public function description() { return NULL; } + public function description() + { + return null; + } /** * Code to migration *to* this migration. * * @param object Migrator - * @throws object Exception If any exception is thrown the migration will be reverted. + * + * @throws object exception If any exception is thrown the migration will be reverted */ abstract public function up(); @@ -170,7 +175,8 @@ abstract public function up(); * Code to undo this migration. * * @param object Migrator - * @throws object Exception If any exception is thrown the migration will be reverted. + * + * @throws object exception If any exception is thrown the migration will be reverted */ abstract public function down(); @@ -179,37 +185,55 @@ abstract public function down(); * * @param object Migrator */ - public function upRollback() {} + public function upRollback() + { + } /** * Code to handle cleanup of a failed down() migration. * * @param object Migrator */ - public function downRollback() {} + public function downRollback() + { + } } /** * Exception that should be thrown by a {@link object Migration Migration's} down() method if the migration is irreversible (ie a one-way migration). */ -class MigrationOneWayException extends Exception {} -class MigrationUnknownVersionException extends Exception {} +class MigrationOneWayException extends Exception +{ +} +class MigrationUnknownVersionException extends Exception +{ +} +class MigrationNoManifestException extends Exception +{ +} +class MigrationManifestMismatchException extends Exception +{ +} abstract class MigratorDelegate { /** - * You can provide a custom {@link MigratorVersionProvider} + * You can provide a custom {@link MigratorVersionProvider}. * * @return object MigratorVersionProvider */ - public function getVersionProvider() {} + public function getVersionProvider() + { + } /** * You can provide a path to the migrations directory which holds the migrations files. * - * @return string /full/path/to/migrations_dir Which ends without a trailing '/'. + * @return string /full/path/to/migrations_dir Which ends without a trailing '/' */ - public function getMigrationsDirectory() {} + public function getMigrationsDirectory() + { + } /** * You can implement custom "clean" functionality for your application here. @@ -220,28 +244,31 @@ public function getMigrationsDirectory() {} * * @param object Migrator */ - public function clean($migrator) {} + public function clean($migrator) + { + } } class Migrator { - const OPT_MIGRATIONS_DIR = 'migrationsDir'; - const OPT_VERSION_PROVIDER = 'versionProvider'; - const OPT_DELEGATE = 'delegate'; - const OPT_PDO_DSN = 'dsn'; - const OPT_VERBOSE = 'verbose'; - const OPT_QUIET = 'quiet'; - - const DIRECTION_UP = 'up'; - const DIRECTION_DOWN = 'down'; - - const VERSION_ZERO = '0'; - const VERSION_UP = 'up'; - const VERSION_DOWN = 'down'; - const VERSION_HEAD = 'head'; + const OPT_MIGRATIONS_DIR = 'migrationsDir'; + const OPT_VERSION_PROVIDER = 'versionProvider'; + const OPT_DELEGATE = 'delegate'; + const OPT_PDO_DSN = 'dsn'; + const OPT_VERBOSE = 'verbose'; + const OPT_QUIET = 'quiet'; + const OPT_OFFER_MANIFEST_UPGRADE = 'offerUpgradeToCreateManifest'; + + const DIRECTION_UP = 'up'; + const DIRECTION_DOWN = 'down'; + + const VERSION_ZERO = '0'; + const VERSION_UP = 'up'; + const VERSION_DOWN = 'down'; + const VERSION_HEAD = 'head'; /** - * @var string The path to the directory where migrations are stored. + * @var string the path to the directory where migrations are stored */ protected $migrationsDirectory; /** @@ -253,74 +280,72 @@ class Migrator */ protected $delegate; /** - * @var object PDO A PDO connection. + * @var object PDO A PDO connection */ protected $dbCon; /** - * @var boolean TRUE to set verbose logging + * @var bool TRUE to set verbose logging */ protected $verbose; /** - * @var boolean TRUE to supress all logging. + * @var bool TRUE to supress all logging */ protected $quiet; /** - * @var array An array of all migrations installed for this app. + * @var array an array of all migrations installed for this app */ - protected $migrationFiles = array(); + protected $migrationList = array(); + /** + * @var array an audit trail of the migrations applied during this invocation + */ + protected $migrationAuditTrail = array(); /** * Create a migrator instance. * - * @param array Options Hash: set any of {@link Migrator::OPT_MIGRATIONS_DIR}, {@link Migrator::OPT_VERSION_PROVIDER}, {@link Migrator::OPT_DELEGATE} - * NOTE: values from delegate override values from the options hash. + * @param array options Hash: set any of {@link Migrator::OPT_MIGRATIONS_DIR}, {@link Migrator::OPT_VERSION_PROVIDER}, {@link Migrator::OPT_DELEGATE} + * NOTE: values from delegate override values from the options hash */ public function __construct($opts = array()) { $opts = array_merge(array( - Migrator::OPT_MIGRATIONS_DIR => './migrations', - Migrator::OPT_VERSION_PROVIDER => new MigratorVersionProviderFile($this), - Migrator::OPT_DELEGATE => NULL, - Migrator::OPT_PDO_DSN => NULL, - Migrator::OPT_VERBOSE => false, - Migrator::OPT_QUIET => false, + self::OPT_MIGRATIONS_DIR => './migrations', + self::OPT_VERSION_PROVIDER => new MigratorVersionProviderFile($this), + self::OPT_DELEGATE => null, + self::OPT_PDO_DSN => null, + self::OPT_VERBOSE => false, + self::OPT_QUIET => false, + self::OPT_OFFER_MANIFEST_UPGRADE => false, ), $opts); // set up initial data - $this->setMigrationsDirectory($opts[Migrator::OPT_MIGRATIONS_DIR]); - $this->setVersionProvider($opts[Migrator::OPT_VERSION_PROVIDER]); - $this->verbose = $opts[Migrator::OPT_VERBOSE]; - if ($opts[Migrator::OPT_DELEGATE]) - { - $this->setDelegate($opts[Migrator::OPT_DELEGATE]); - } - if ($opts[Migrator::OPT_PDO_DSN]) - { + $this->setMigrationsDirectory($opts[self::OPT_MIGRATIONS_DIR]); + $this->setVersionProvider($opts[self::OPT_VERSION_PROVIDER]); + $this->verbose = $opts[self::OPT_VERBOSE]; + if ($opts[self::OPT_DELEGATE]) { + $this->setDelegate($opts[self::OPT_DELEGATE]); + } + if ($opts[self::OPT_PDO_DSN]) { // parse out user/pass from DSN $matches = array(); $user = $pass = null; - if (preg_match('/user=([^;]+)(;|\z)/', $opts[Migrator::OPT_PDO_DSN], $matches)) - { + if (preg_match('/user=([^;]+)(;|\z)/', $opts[self::OPT_PDO_DSN], $matches)) { $user = $matches[1]; } - if (preg_match('/password=([^;]+)(;|\z)/', $opts[Migrator::OPT_PDO_DSN], $matches)) - { + if (preg_match('/password=([^;]+)(;|\z)/', $opts[self::OPT_PDO_DSN], $matches)) { $pass = $matches[1]; } - $this->dbCon = new PDO($opts[Migrator::OPT_PDO_DSN], $user, $pass); + $this->dbCon = new PDO($opts[self::OPT_PDO_DSN], $user, $pass); $this->dbCon->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); } - $this->quiet = $opts[Migrator::OPT_QUIET]; + $this->quiet = $opts[self::OPT_QUIET]; // get info from delegate - if ($this->delegate) - { - if (method_exists($this->delegate, 'getVersionProvider')) - { + if ($this->delegate) { + if (method_exists($this->delegate, 'getVersionProvider')) { $this->setVersionProvider($this->delegate->getVersionProvider()); } - if (method_exists($this->delegate, 'getMigrationsDirectory')) - { + if (method_exists($this->delegate, 'getMigrationsDirectory')) { $this->setMigrationsDirectory($this->delegate->getMigrationsDirectory()); } } @@ -329,31 +354,41 @@ public function __construct($opts = array()) $this->logMessage("MP - The PHP Migrator.\n"); $this->initializeMigrationsDir(); - $this->collectMigrationFiles(); + $migrationFileList = $this->collectMigrationFiles(); + + if (!file_exists($this->getMigrationsManifestFile()) && $opts[self::OPT_OFFER_MANIFEST_UPGRADE]) { + $this->logMessage("MP v1.0.4 now uses a migrations.json manifest file to determine order of migrations. We have generated one for you; be sure to check {$this->getMigrationsManifestFile()} into your version control.\n"); + $this->upgradeToMigrationsManifest($migrationFileList); + } + + $this->generateMigrationList($migrationFileList); - $this->logMessage("Using version provider: " . get_class($this->getVersionProvider()) . "\n", true); - $this->logMessage("Found " . count($this->migrationFiles) . " migrations: " . print_r($this->migrationFiles, true), true); + $this->logMessage('Using version provider: '.get_class($this->getVersionProvider())."\n", true); + $this->logMessage('Found '.count($this->migrationList).' migrations: '.print_r($this->migrationList, true), true); // warn if migrations exist but we are at version 0 - if (count($this->migrationFiles) && $this->getVersion() === Migrator::VERSION_ZERO) - { - $this->logMessage("\n\nWARNING: There is at least one migration defined but the current install is marked as being at Version ZERO.\n" . - "This might indicate you are running mp on an install of a project that is already at a particular migration.\n" . - "Usually in this situation your first migration will fail since the tables for it will already exist, so it is normally harmless.\n" . - "However, it could be dangerous, so be very careful.\n" . - "Please manually set the version of the current install to the proper migration version as appropriate.\n" . - "It could also indicate you are running migrations for the first time on a newly created database.\n" . + if (count($this->migrationList) && self::VERSION_ZERO === $this->getVersion()) { + $this->logMessage("\n\nWARNING: There is at least one migration defined but the current install is marked as being at Version ZERO.\n". + "This might indicate you are running mp on an install of a project that is already at a particular migration.\n". + "Usually in this situation your first migration will fail since the tables for it will already exist, so it is normally harmless.\n". + "However, it could be dangerous, so be very careful.\n". + "Please manually set the version of the current install to the proper migration version as appropriate.\n". + "It could also indicate you are running migrations for the first time on a newly created database.\n". "\n\n" ); } } + public function getMigrationAuditTrail() + { + return $this->migrationAuditTrail; + } + protected function initializeMigrationsDir() { // initialize migrations dir $migrationsDir = $this->getMigrationsDirectory(); - if (!file_exists($migrationsDir)) - { + if (!file_exists($migrationsDir)) { $this->logMessage("No migrations dir found; initializing migrations directory at {$migrationsDir}.\n"); mkdir($migrationsDir, 0777, true); $cleanTPL = <<setVersion(Migrator::VERSION_ZERO); + file_put_contents($migrationsDir.'/clean.php', $cleanTPL); + $this->setVersion(self::VERSION_ZERO); } } @@ -380,51 +415,170 @@ public function getVersion() public function setVersion($v) { // sanity check - if ($v !== Migrator::VERSION_ZERO) - { + if (self::VERSION_ZERO !== $v) { try { $versionIndex = $this->indexOfVersion($v); } catch (MigrationUnknownVersionException $e) { $this->logMessage("Cannot set the version to {$v} because it is not a known version.\n"); } } + return $this->getVersionProvider()->setVersion($this, $v); } + protected function getMigrationsManifestFile() + { + return "{$this->getMigrationsDirectory()}/migrations.json"; + } + + /** + * @return array An array of the migrations which are manifested by the migrations.json file. + */ + protected function getMigrationsManifest() + { + $manifestFile = $this->getMigrationsManifestFile(); + if (!file_exists($manifestFile)) { + throw new MigrationNoManifestException("No manifest file found: {$manifestFile}"); + } + $data = file_get_contents($manifestFile); + if (false === $data) { + throw new Exception('Error reading migrations.json manifest file.'); + } + $migrationList = json_decode($data, true); + if (!$migrationList) { + throw new Exception('Error decoding migrations.json manifest.'); + } + + return $migrationList; + } + + protected function upgradeToMigrationsManifest($migrationList) + { + $manifestFile = $this->getMigrationsManifestFile(); + if (file_exists($manifestFile)) { + return; + } + + $migrationList = array_keys($migrationList); + // Legacy way way to sort in reverse chronological order via natsort + natsort($migrationList); + $migrationList = array_values($migrationList); + + // upgrade to manifest-based migration ordering + $this->writeMigrationsManifest($migrationList); + } + + protected function writeMigrationsManifest($migrationList) + { + $manifestFile = $this->getMigrationsManifestFile(); + + $indent = ' '; + $eol = PHP_EOL; + $quote = '"'; + $ok = file_put_contents($manifestFile, "[{$eol}{$indent}{$quote}".implode("{$quote},{$eol}{$indent}{$quote}", $migrationList)."{$quote}{$eol}]{$eol}"); + if (!$ok) { + throw new Exception("Error writing {$manifestFile}."); + } + } + protected function collectMigrationFiles() { + $migrationFileList = array(); foreach (new DirectoryIterator($this->getMigrationsDirectory()) as $file) { - if ($file->isDot()) continue; - if ($file->isDir()) continue; - $matches = array(); - if (preg_match('/^([0-9]{8}_[0-9]{6}).php$/', $file->getFilename(), $matches)) - { - $this->migrationFiles[$matches[1]] = $file->getFilename(); + if ($file->isDot()) { + continue; + } + if ($file->isDir()) { + if (preg_match('/^([0-9]{4})$/', $file->getFilename(), $matches)) { + // Year dir + foreach (new DirectoryIterator($file->getPathname()) as $subFile) { + if ($subFile->isDot() || $subFile->isDir()) { + continue; + } + + if (preg_match('/^([0-9]{8}_[0-9]{6}).php$/', $subFile->getFilename(), $matches)) { + $migrationFileList[$matches[1]] = $file->getFilename().\DIRECTORY_SEPARATOR.$subFile->getFilename(); + } + } + } else { + // Not a year dir + continue; + } + } else { + $matches = array(); + if (preg_match('/^([0-9]{8}_[0-9]{6}).php$/', $file->getFilename(), $matches)) { + $migrationFileList[$matches[1]] = $file->getFilename(); + } + } + } + + return $migrationFileList; + } + + protected function generateMigrationList($migrationFileList) + { + $manifestedMigrations = $this->getMigrationsManifest(); + $migrationFileCount = count($migrationFileList); + $migrationManifestCount = count($manifestedMigrations); + if ($migrationManifestCount !== $migrationFileCount) { + $setOfManifestedMigrations = $manifestedMigrations; + $setOfMigrationFiles = array_keys($migrationFileList); + $manifestedButNoFile = array_diff($setOfManifestedMigrations, $setOfMigrationFiles); + $fileButNoManifest = array_diff($setOfMigrationFiles, $setOfManifestedMigrations); + $msgManifestedButNoFile = 'All manifested migrations have files.'; + if (count($manifestedButNoFile)) { + $msgManifestedButNoFile = 'The following migrations are manifested but have no corresponding file: '.PHP_EOL.implode(PHP_EOL, $manifestedButNoFile); + } + $msgFileButNoManifest = 'All migration files have been manifested.'; + if (count($fileButNoManifest)) { + $msgFileButNoManifest = 'The following migrations have migration files but have not been manifested: '.PHP_EOL.implode(PHP_EOL, $fileButNoManifest); } - // sort in reverse chronological order - natsort($this->migrationFiles); + $msg = <<migrationList = $sortedMigrationFileList; } public function logMessage($msg, $onlyIfVerbose = false) { - if ($this->quiet) return; - if (!$this->verbose && $onlyIfVerbose) return; - print $msg; + if ($this->quiet) { + return; + } + if (!$this->verbose && $onlyIfVerbose) { + return; + } + echo $msg; } public function getDbCon() { - if (!$this->dbCon) - { - throw new Exception("No DB connection available. Make sure to configure a DSN."); + if (!$this->dbCon) { + throw new Exception('No DB connection available. Make sure to configure a DSN.'); } + return $this->dbCon; } - + public function setDelegate($d) { - if (!is_object($d)) throw new Exception("setDelegate requires an object instance."); + if (!is_object($d)) { + throw new Exception('setDelegate requires an object instance.'); + } $this->delegate = $d; } @@ -436,6 +590,7 @@ public function getDelegate() public function setMigrationsDirectory($dir) { $this->migrationsDirectory = $dir; + return $this; } @@ -446,22 +601,28 @@ public function getMigrationsDirectory() public function setVersionProvider($vp) { - if (!($vp instanceof MigratorVersionProvider)) throw new Exception("setVersionProvider requires an object implementing MigratorVersionProvider."); + if (!($vp instanceof MigratorVersionProvider)) { + throw new Exception('setVersionProvider requires an object implementing MigratorVersionProvider.'); + } $this->versionProvider = $vp; + return $this; } + public function getVersionProvider() { return $this->versionProvider; } /** - * Get the index of the passed version number in the migrationFiles array. + * Get the index of the passed version number in the migrationList array. * * NOTE: This function does NOT accept the Migrator::VERSION_* constants. * * @param string The version number to look for - * @return integer The index of the migration in the migrationFiles array. + * + * @return int the index of the migration in the migrationList array + * * @throws object MigrationUnknownVersionException */ protected function indexOfVersion($findVersion) @@ -469,34 +630,36 @@ protected function indexOfVersion($findVersion) // normal logic for when there is 1+ migrations and we aren't at VERSION_ZERO $foundCurrent = false; $currentIndex = 0; - foreach (array_keys($this->migrationFiles) as $version) { - if ($version === $findVersion) - { + foreach (array_keys($this->migrationList) as $version) { + if ($version === $findVersion) { $foundCurrent = true; break; } - $currentIndex++; + ++$currentIndex; } - if (!$foundCurrent) - { + if (!$foundCurrent) { throw new MigrationUnknownVersionException("Version {$findVersion} is not a known migration."); } + return $currentIndex; } /** * Get the migration name of the latest version. * - * @return string The "latest" migration version. + * @return string the "latest" migration version */ public function latestVersion() { - if (empty($this->migrationFiles)) - { + if (empty($this->migrationList)) { $this->logMessage("No migrations available.\n"); + return true; } - $lastMigration = array_pop(array_keys($this->migrationFiles)); + + $migrationListKeys = array_keys($this->migrationList); + $lastMigration = array_pop($migrationListKeys); + return $lastMigration; } @@ -504,52 +667,50 @@ public function latestVersion() * Find the next migration to run in the given direction. * * @param string Current version - * @param string Direction (one of Migrator::DIRECTION_UP or Migrator::DIRECTION_DOWN). - * @return string The migration name of the "next" migration in the correct direction, or NULL if there is no "next" migration in that direction. + * @param string direction (one of Migrator::DIRECTION_UP or Migrator::DIRECTION_DOWN) + * + * @return string the migration name of the "next" migration in the correct direction, or NULL if there is no "next" migration in that direction + * * @throws */ protected function findNextMigration($currentMigration, $direction) { // special case when no migrations exist - if (count($this->migrationFiles) === 0) return NULL; + if (0 === count($this->migrationList)) { + return null; + } - $migrationVersions = array_keys($this->migrationFiles); + $migrationVersions = array_keys($this->migrationList); // special case when current == VERSION_ZERO - if ($currentMigration === Migrator::VERSION_ZERO) - { - if ($direction === Migrator::DIRECTION_UP) - { + if (self::VERSION_ZERO === $currentMigration) { + if (self::DIRECTION_UP === $direction) { return $migrationVersions[0]; - } - else - { - return NULL; // no where down from VERSION_ZERO + } else { + return null; // no where down from VERSION_ZERO } } // normal logic for when there is 1+ migrations and we aren't at VERSION_ZERO $currentIndex = $this->indexOfVersion($currentMigration); - if ($direction === Migrator::DIRECTION_UP) - { + if (self::DIRECTION_UP === $direction) { $lastIndex = count($migrationVersions) - 1; - if ($currentIndex === $lastIndex) - { - return NULL; + if ($currentIndex === $lastIndex) { + return null; } + return $migrationVersions[$currentIndex + 1]; - } - else - { - if ($currentIndex === 0) - { - return NULL; + } else { + if (0 === $currentIndex) { + return null; } + return $migrationVersions[$currentIndex - 1]; } } // ACTIONS + /** * Create a migrate stub file. * @@ -558,7 +719,7 @@ protected function findNextMigration($currentMigration, $direction) public function createMigration() { $dts = date('Ymd_His'); - $filename = $dts . '.php'; + $filename = $dts.'.php'; $tpl = <<getMigrationsDirectory() . "/{$filename}"; - if (file_exists($filePath)) throw new Exception("Migration {$dts} already exists! Aborting."); + if (is_dir($this->getMigrationsDirectory().'/'.date('Y'))) { + // Year dir exists + $filePath = $this->getMigrationsDirectory().'/'.date('Y')."/{$filename}"; + } else { + $filePath = $this->getMigrationsDirectory()."/{$filename}"; + } + + if (file_exists($filePath)) { + throw new Exception("Migration {$dts} already exists! Aborting."); + } file_put_contents($filePath, $tpl); - $this->logMessage("Created migration {$dts} at {$filePath}.\n"); + + // add migrations.json + $migrationList = $this->getMigrationsManifest(); + $migrationList[] = $dts; + $this->writeMigrationsManifest($migrationList); + $this->logMessage("Created migration {$dts} at {$filePath} and added it to the end of migrations.json.\n"); } private function instantiateMigration($migrationName) { - require_once($this->getMigrationsDirectory() . "/" . $this->migrationFiles[$migrationName]); + require_once $this->getMigrationsDirectory().'/'.$this->migrationList[$migrationName]; $migrationClassName = "Migration{$migrationName}"; + return new $migrationClassName($this); } public function listMigrations() { - $v = Migrator::VERSION_ZERO; + $v = self::VERSION_ZERO; while (true) { - $v = $this->findNextMigration($v, Migrator::DIRECTION_UP); - if ($v === NULL) break; + $v = $this->findNextMigration($v, self::DIRECTION_UP); + if (null === $v) { + break; + } $m = $this->instantiateMigration($v); - $this->logMessage($v . ': ' . $m->description() . "\n"); + $this->logMessage($v.': '.$m->description()."\n"); } } /** * Run the given migration in the specified direction. * - * @param string The migration version. - * @param string Direction. - * @return boolean TRUE if migration ran successfully, false otherwise. + * @param string the migration version + * @param string direction + * + * @return bool TRUE if migration ran successfully, false otherwise */ public function runMigration($migrationName, $direction) { - if ($direction === Migrator::DIRECTION_UP) - { + $this->migrationAuditTrail[] = "{$migrationName}:{$direction}"; + + if (self::DIRECTION_UP === $direction) { $info = array( - 'actionName' => 'Upgrade', - 'migrateF' => 'up', - 'migrateRollbackF' => 'upRollback', + 'actionName' => 'Upgrade', + 'migrateF' => 'up', + 'migrateRollbackF' => 'upRollback', ); - } - else - { + } else { $info = array( - 'actionName' => 'Downgrade', - 'migrateF' => 'down', - 'migrateRollbackF' => 'downRollback', + 'actionName' => 'Downgrade', + 'migrateF' => 'down', + 'migrateRollbackF' => 'downRollback', ); } $migration = $this->instantiateMigration($migrationName); - $this->logMessage("Running {$migrationName} {$info['actionName']}: " . $migration->description() . "\n", false); + $this->logMessage("Running {$migrationName} {$info['actionName']}: ".$migration->description()."\n", false); try { - $migration->$info['migrateF']($this); - if ($direction === Migrator::DIRECTION_UP) - { + $upOrDown = $info['migrateF']; + $migration->$upOrDown($this); + if (self::DIRECTION_UP === $direction) { $this->setVersion($migrationName); + } else { + $downgradedToVersion = $this->findNextMigration($migrationName, self::DIRECTION_DOWN); + $this->setVersion((null === $downgradedToVersion ? self::VERSION_ZERO : $downgradedToVersion)); } - else - { - $downgradedToVersion = $this->findNextMigration($migrationName, Migrator::DIRECTION_DOWN); - $this->setVersion(($downgradedToVersion === NULL ? Migrator::VERSION_ZERO : $downgradedToVersion)); - } + return true; } catch (Exception $e) { $this->logMessage("Error during {$info['actionName']} migration {$migrationName}: {$e}\n"); - if (method_exists($migration, $info['migrateRollbackF'])) - { + if (method_exists($migration, $info['migrateRollbackF'])) { try { $migration->$info['migrateRollbackF']($this); } catch (Exception $e) { $this->logMessage("Error during rollback of {$info['actionName']} migration {$migrationName}: {$e}\n"); } - } } + return false; } /** * Run the given migration as an upgrade. * - * @param string The migration version. - * @return boolean TRUE if migration ran successfully, false otherwise. + * @param string the migration version + * + * @return bool TRUE if migration ran successfully, false otherwise */ public function runUpgrade($migrationName) { - return $this->runMigration($migrationName, Migrator::DIRECTION_UP); + return $this->runMigration($migrationName, self::DIRECTION_UP); } /** * Run the given migration as a downgrade. * - * @param string The migration version. - * @return boolean TRUE if migration ran successfully, false otherwise. + * @param string the migration version + * + * @return bool TRUE if migration ran successfully, false otherwise */ public function runDowngrade($migrationName) { - return $this->runMigration($migrationName, Migrator::DIRECTION_DOWN); + return $this->runMigration($migrationName, self::DIRECTION_DOWN); } /** * Migrate to the specified version. * - * @param string The Version. - * @return boolean TRUE if migration successfully ended on specified version. + * @param string the Version + * + * @return bool TRUE if migration successfully ended on specified version */ public function migrateToVersion($toVersion) { @@ -694,45 +872,38 @@ public function migrateToVersion($toVersion) $currentVersion = $this->getVersionProvider()->getVersion($this); // unroll meta versions - if ($toVersion === Migrator::VERSION_UP) - { - $toVersion = $this->findNextMigration($currentVersion, Migrator::DIRECTION_UP); - } - else if ($toVersion === Migrator::VERSION_DOWN) - { - $toVersion = $this->findNextMigration($currentVersion, Migrator::DIRECTION_DOWN); - if (!$toVersion) - { - $toVersion = Migrator::VERSION_ZERO; + if (self::VERSION_UP === $toVersion) { + $toVersion = $this->findNextMigration($currentVersion, self::DIRECTION_UP); + } elseif (self::VERSION_DOWN === $toVersion) { + $toVersion = $this->findNextMigration($currentVersion, self::DIRECTION_DOWN); + if (!$toVersion) { + $toVersion = self::VERSION_ZERO; } - } - else if ($toVersion === Migrator::VERSION_HEAD) - { + } elseif (self::VERSION_HEAD === $toVersion) { $toVersion = $this->latestVersion(); $this->logMessage("Resolved head to {$toVersion}\n", true); } // no-op detection - if ($currentVersion === $toVersion) - { + if ($currentVersion === $toVersion) { $this->logMessage("Already at version {$currentVersion}.\n"); + return true; } // verify target version - if ($toVersion !== Migrator::VERSION_ZERO) - { + if (self::VERSION_ZERO !== $toVersion) { try { $this->indexOfVersion($toVersion); } catch (MigrationUnknownVersionException $e) { $this->logMessage("Cannot migrate to version {$toVersion} because it does not exist.\n"); + return false; } } // verify current version try { - if ($currentVersion !== Migrator::VERSION_ZERO) - { + if (self::VERSION_ZERO !== $currentVersion) { $currentVersionIndex = $this->indexOfVersion($currentVersion); } } catch (MigrationUnknownVersionException $e) { @@ -740,64 +911,52 @@ public function migrateToVersion($toVersion) } // calculate direction - if ($currentVersion === Migrator::VERSION_ZERO) - { - $direction = Migrator::DIRECTION_UP; - } - else if ($currentVersion === array_pop(array_keys($this->migrationFiles))) - { - $direction = Migrator::DIRECTION_DOWN; - } - else if ($toVersion === Migrator::VERSION_ZERO) - { - $direction = Migrator::DIRECTION_DOWN; - } - else - { + if (self::VERSION_ZERO === $currentVersion) { + $direction = self::DIRECTION_UP; + } elseif ($currentVersion === $this->latestVersion()) { + $direction = self::DIRECTION_DOWN; + } elseif (self::VERSION_ZERO === $toVersion) { + $direction = self::DIRECTION_DOWN; + } else { $currentVersionIndex = $this->indexOfVersion($currentVersion); $toVersionIndex = $this->indexOfVersion($toVersion); - $direction = ($toVersionIndex > $currentVersionIndex ? Migrator::DIRECTION_UP : Migrator::DIRECTION_DOWN); + $direction = ($toVersionIndex > $currentVersionIndex ? self::DIRECTION_UP : self::DIRECTION_DOWN); } - $actionName = ($direction === Migrator::DIRECTION_UP ? 'Upgrade' : 'Downgrade'); + $actionName = (self::DIRECTION_UP === $direction ? 'Upgrade' : 'Downgrade'); $this->logMessage("{$actionName} from version {$currentVersion} to {$toVersion}.\n"); while ($currentVersion !== $toVersion) { - if ($direction === Migrator::DIRECTION_UP) - { + if (self::DIRECTION_UP === $direction) { $nextMigration = $this->findNextMigration($currentVersion, $direction); - if (!$nextMigration) break; + if (!$nextMigration) { + break; + } - $ok = $this->runMigration($nextMigration, Migrator::DIRECTION_UP); - if (!$ok) - { + $ok = $this->runMigration($nextMigration, self::DIRECTION_UP); + if (!$ok) { break; } - } - else - { - $nextMigration = $this->findNextMigration($currentVersion, Migrator::DIRECTION_DOWN); + } else { + $nextMigration = $this->findNextMigration($currentVersion, self::DIRECTION_DOWN); $ok = $this->runMigration($currentVersion, $direction); - if (!$ok) - { + if (!$ok) { break; } - if (!$nextMigration) - { + if (!$nextMigration) { // next is 0, we are done! - $currentVersion = $nextMigration = Migrator::VERSION_ZERO; + $currentVersion = $nextMigration = self::VERSION_ZERO; } } $currentVersion = $nextMigration; $this->logMessage("Current version now {$currentVersion}\n", true); } - if ($currentVersion === $toVersion) - { + if ($currentVersion === $toVersion) { $this->logMessage("{$toVersion} {$actionName} succeeded.\n"); + return true; - } - else - { - $this->logMessage("{$toVersion} {$actionName} failed.\nRolled back to " . $this->getVersionProvider()->getVersion($this) . ".\n"); + } else { + $this->logMessage("{$toVersion} {$actionName} failed.\nRolled back to ".$this->getVersionProvider()->getVersion($this).".\n"); + return false; } } @@ -812,25 +971,20 @@ public function clean() $this->logMessage("Cleaning...\n"); // reset version number - $this->setVersion(Migrator::VERSION_ZERO); + $this->setVersion(self::VERSION_ZERO); // call delegate's clean - if ($this->delegate && method_exists($this->delegate, 'clean')) - { + if ($this->delegate && method_exists($this->delegate, 'clean')) { $this->delegate->clean($this); - } - else - { + } else { // look for migrations/clean.php, className = MigrateClean::clean() - $cleanFile = $this->getMigrationsDirectory() . '/clean.php'; - if (file_exists($cleanFile)) - { - require_once($cleanFile); + $cleanFile = $this->getMigrationsDirectory().'/clean.php'; + if (file_exists($cleanFile)) { + require_once $cleanFile; MigrateClean::clean($this); } } return $this; } - } diff --git a/README b/README.md similarity index 93% rename from README rename to README.md index 0223168..04cc7a8 100644 --- a/README +++ b/README.md @@ -1,14 +1,17 @@ -MP: Migrations for PHP +# MP: Migrations for PHP MP is a generic migrations architecture for managing migrations between versions of a web application. It can be used to migration database schemas as well as perform arbitary code during upgrades and downgrades. -INSTALLATION +## INSTALLATION -pear install apinstein.pearfarm.org/mp +```sh +composer require apinstein/mp +``` + +## HOW IT WORKS -HOW IT WORKS MP keeps track of the current version of your application. You can then request to migrate to any version. MP also has a "clean" function which allows you to reset your application to "version 0". There is a clean() callback @@ -16,16 +19,20 @@ which allows you to programmatically return your application to a pristine state By default you can implement your "clean" functionality in the MigrationClean::clean() method of migrations/clean.php: +```php public function clean($migrator) { $migrator->getDbCon()->exec("drop database foo;"); } +``` NOTE: If you prefer you can also create your baseline schema in clean. However, I usually set up the baseline schema in the first migration. Each migration for your application is defined by a class in the migrations directory. -EXAMPLE CLI USAGE +## EXAMPLE CLI USAGE + +```sh $ mp -f # Use file-based version tracking; If no args will just print version. # NOTE: First run of MP will create the migrations directory, create a stub clean script, and set the version to 0. @@ -33,34 +40,48 @@ $ mp -f -n # Create a new migration; will write a stub file $ mp -f -m # Migrate to head (latest revision) $ mp -f -m20090716_204830 # Migrate to revision 20090716_204830 $ mp -f -r # Reset to "clean" state (version 0) +``` If you need DB access in your migrations, you can bootstrap them yourself, or, if you supply a dsn like so: + +```sh $ mp -x'pgsql:dbname=mydb;user=mydbuser;host=localhost' +``` Then in your migrations you can do: +```php $this->migrator->getDbCon()->exec($sql); +``` And in the clean() function, it's: + +```php $migrator->getDbCon()->exec($sql); +``` NOTE: If you use Migrator's db connection, it is configured to throw PDOException on error. -EXAMPLE API USAGE +## EXAMPLE API USAGE + +```php $m = new Migrator(); $m->clean(); $m->upgradeToLatest(); $m->downgradeToVersion('20090716_204830'); $m->upgradeToVersion('20090716_205029'); $m->downgradeToVersion('20090716_212141'); +``` + +### NOTE FOR SOURCE CONTROL -NOTE FOR SOURCE CONTROL If you use the file-based version tracking (ie migrations/version.txt) then make sure to have your source control system *ignore* that file. You definitely don't want your system to think it's been updated when you push new code to production but before you run your migrations! Therefore it is recommended to use DB-based versioning wherever possible. -INTEGRATION +### INTEGRATION + While MP can be operated purely via the migrate command line tool, it is also designed to be implemented into your web application or with any framework. You can use the Migrator API to custom-configure MP's behavior for your application or framework. @@ -69,7 +90,8 @@ This is ideal for use with ORMs that may already have an API to manage schemas b It also works well with ORMs that don't have an API to manage schemas, as you can still integrate with them to use their DB connection for executing SQL migrations. -ROADMAP / TODO +## ROADMAP / TODO + - Add long-option support. See http://cliframework.com/, looks pretty interesting. - Automatically walk up from pwd looking for migrations/ directory so you only have to be *under* your project root to run mp successfully. - Addition of mutex protection to prevent multiple migrations from running at the same time. diff --git a/composer.json b/composer.json index d8c6d66..c86b9d7 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,13 @@ } ], "require": { - "php": ">=5.2.0" + "php": ">=7.1.0" + }, + "require-dev": { + "phpunit/phpunit": "^7" + }, + "autoload": { + "files": ["Migrator.php"] }, "bin": ["mp"] } diff --git a/mp b/mp index 877cc80..36e5eb9 100755 --- a/mp +++ b/mp @@ -23,13 +23,11 @@ * THE SOFTWARE. */ -if (strpos('@php_bin@', '@php_bin') === 0) { // not a pear install - $prefix = dirname(__FILE__); +if (file_exists($a = __DIR__.'/../../autoload.php')) { + require_once $a; } else { - $prefix = 'mp'; + require_once __DIR__.'/vendor/autoload.php'; } -require_once "{$prefix}/Migrator.php"; - // get options $longopts = array(); @@ -153,6 +151,7 @@ if (!(isset($cliOpts['r']) or isset($cliOpts['n']) or isset($cliOpts['s']) or is $cliOpts['s'] = true; } try { + $opts[Migrator::OPT_OFFER_MANIFEST_UPGRADE] = true; $m = new Migrator($opts); if (isset($cliOpts['r'])) diff --git a/pearfarm.spec b/pearfarm.spec deleted file mode 100644 index 254b845..0000000 --- a/pearfarm.spec +++ /dev/null @@ -1,17 +0,0 @@ - dirname(__FILE__))) - ->setName('mp') - ->setChannel('apinstein.pearfarm.org') - ->setSummary('MP: Migrations for PHP') - ->setDescription('A generic db migrations engine for PHP.') - ->setReleaseVersion('1.0.3') - ->setReleaseStability('stable') - ->setApiVersion('1.0.0') - ->setApiStability('stable') - ->setLicense(Pearfarm_PackageSpec::LICENSE_MIT) - ->setNotes('See http://github.com/apinstein/mp.') - ->addMaintainer('lead', 'Alan Pinstein', 'apinstein', 'apinstein@mac.com') - ->addGitFiles() - ->addExecutable('mp') - ; diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..d18f296 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,20 @@ + + + + + + + + + + ./test/ + + + diff --git a/test/MigratorManifestUpgradeTest.php b/test/MigratorManifestUpgradeTest.php new file mode 100644 index 0000000..aecb6c6 --- /dev/null +++ b/test/MigratorManifestUpgradeTest.php @@ -0,0 +1,52 @@ +migrationsDir().'/migrations.json'; + } + + public function teardown() + { + @unlink($this->manifestFile()); + } + + public function testMPRequiresManifestFile() + { + $this->expectException('MigrationNoManifestException'); + new Migrator(array( + Migrator::OPT_MIGRATIONS_DIR => $this->migrationsDir(), + Migrator::OPT_QUIET => true, + )); + } + + public function testMigrationUpgradeCreatesExpectedManifestFile() + { + $this->assertFileNotExists($this->manifestFile(), "Shouldn't be a manifest file yet."); + + new Migrator(array( + Migrator::OPT_MIGRATIONS_DIR => $this->migrationsDir(), + Migrator::OPT_QUIET => true, + Migrator::OPT_OFFER_MANIFEST_UPGRADE => true, + )); + + $this->assertFileExists($this->manifestFile(), 'Should be a manifest file now.'); + $this->assertEquals(array( + '20090719_000001', + '20090719_000002', + '20090719_000003', + '20090719_000004', + '20090719_000005', + ), json_decode(file_get_contents($this->manifestFile()))); + } +} diff --git a/test/MigratorTest.php b/test/MigratorTest.php index 04bf2e4..936d281 100644 --- a/test/MigratorTest.php +++ b/test/MigratorTest.php @@ -1,139 +1,159 @@ TEST_MIGRATIONS_DIR, + Migrator::OPT_MIGRATIONS_DIR => __DIR__.'/migrations', Migrator::OPT_QUIET => true, ); $this->migrator = new Migrator($opts); $this->migrator->getVersionProvider()->setVersion($this->migrator, 0); // hard-reset to version 0 - - global $testMigrationNumber, $testMigrationIncrementedNumber, $testMigrationHasRunCounter; - $testMigrationNumber = 0; - $testMigrationIncrementedNumber = 0; } - function testFreshMigrationsStartAtVersionZero() + public function testFreshMigrationsStartAtVersionZero() { $this->assertEquals(Migrator::VERSION_ZERO, $this->migrator->getVersion()); } - private function assertAtVersion($version, $counter, $numMigrationsRun = NULL) + private function assertAtVersion($version) { - global $testMigrationNumber, $testMigrationIncrementedNumber, $testMigrationHasRunCounter; - $this->assertEquals($version, $this->migrator->getVersion(), "At wrong version #"); - $this->assertEquals($counter, $testMigrationNumber, "testMigrationNumber wrong"); - $this->assertEquals($counter, $testMigrationIncrementedNumber, "testMigrationIncrementedNumber wrong"); - if ($numMigrationsRun !== NULL) - { - $this->assertEquals($numMigrationsRun, $testMigrationHasRunCounter); - } + $this->assertEquals($version, $this->migrator->getVersion(), 'At wrong version.'); } - function testLatestVersion() + private function assertAuditTrail($expectedAuditTrail) + { + $this->assertEquals($expectedAuditTrail, $this->migrator->getMigrationAuditTrail()); + } + + public function testLatestVersion() { $this->assertEquals('20090719_000005', $this->migrator->latestVersion()); } - function testCleanGoesToVersionZero() + public function testCleanGoesToVersionZero() { $this->migrator->clean(); - $this->assertAtVersion(Migrator::VERSION_ZERO, 0, 0); + $this->assertAtVersion(Migrator::VERSION_ZERO); + $this->assertAuditTrail(array()); } - function testMigratingToVersionZero() + public function testMigratingToVersionZero() { - $this->migrator->migrateToVersion(Migrator::VERSION_HEAD); + $this->migrator->getVersionProvider()->setVersion($this->migrator, '20090719_000005'); $this->migrator->migrateToVersion(Migrator::VERSION_ZERO); - $this->assertAtVersion(Migrator::VERSION_ZERO, 0, 10); + $this->assertAtVersion(Migrator::VERSION_ZERO); + $this->assertAuditTrail(array( + '20090719_000005:down', + '20090719_000003:down', + '20090719_000004:down', + '20090719_000002:down', + '20090719_000001:down', + )); } - function testMigratingToHead() + public function testMigratingToHead() { $this->migrator->migrateToVersion(Migrator::VERSION_HEAD); - $this->assertAtVersion('20090719_000005', 5, 5); + $this->assertAtVersion('20090719_000005'); + $this->assertAuditTrail(array( + '20090719_000001:up', + '20090719_000002:up', + '20090719_000004:up', + '20090719_000003:up', + '20090719_000005:up', + )); } - function testMigrateUp() + public function testMigrateUp() { // mock out migrator; make sure UP calls migrate to appropriate version - $this->migrator->migrateToVersion('20090719_000002'); -// $mock = $this->getMock($this->migrator); -// $mock->expects($this->once()) -// ->method('migrateToVersion') -// ->with($this->equalTo('20090719_000003')); -// $this->migrator->migrateToVersion(Migrator::VERSION_UP); + $this->migrator->getVersionProvider()->setVersion($this->migrator, '20090719_000001'); - global $testMigrationIncrementedNumber; - $expectedMigrationIncrementedNumber = $testMigrationIncrementedNumber + 1; - $this->migrator->migrateToVersion(Migrator::VERSION_UP); - $this->assertAtVersion('20090719_000003', 3, $expectedMigrationIncrementedNumber); + $this->migrator->migrateToVersion(Migrator::VERSION_UP); + $this->assertAtVersion('20090719_000002'); + $this->assertAuditTrail(array( + '20090719_000002:up', + )); } - function testMigrateDown() + public function testMigrateDown() { - // mock out migrator; make sure UP calls migrate to appropriate version - $this->migrator->migrateToVersion('20090719_000002'); -// $mock = $this->getMock($this->migrator); -// $mock->expects($this->once()) -// ->method('migrateToVersion') -// ->with($this->equalTo('20090719_000001')); -// $this->migrator->migrateToVersion(Migrator::VERSION_DOWN); - - global $testMigrationIncrementedNumber; - $expectedMigrationIncrementedNumber = $testMigrationIncrementedNumber + 1; + $this->migrator->getVersionProvider()->setVersion($this->migrator, '20090719_000002'); $this->migrator->migrateToVersion(Migrator::VERSION_DOWN); - $this->assertAtVersion('20090719_000001', 1, $expectedMigrationIncrementedNumber); + $this->assertAtVersion('20090719_000001'); + $this->assertAuditTrail(array( + '20090719_000002:down', + )); } - function testMigratingToCurrentVersionRunsNoMigrations() + public function testMigratingToCurrentVersionRunsNoMigrations() { + $this->migrator->getVersionProvider()->setVersion($this->migrator, '20090719_000002'); $this->migrator->migrateToVersion('20090719_000002'); - global $testMigrationIncrementedNumber; - $this->migrator->migrateToVersion('20090719_000002'); - $this->assertAtVersion('20090719_000002', 2, $testMigrationIncrementedNumber); + $this->assertAtVersion('20090719_000002'); + $this->assertAuditTrail(array()); } - function testMigrateToVersion1() + public function testMigrateToVersion1() { $this->migrator->migrateToVersion('20090719_000001'); - $this->assertAtVersion('20090719_000001', 1, 1); + $this->assertAtVersion('20090719_000001'); + $this->assertAuditTrail(array( + '20090719_000001:up', + )); } - function testMigrateToVersion2() + public function testMigrateToVersion2() { $this->migrator->migrateToVersion('20090719_000002'); - $this->assertAtVersion('20090719_000002', 2, 2); + $this->assertAtVersion('20090719_000002'); + $this->assertAuditTrail(array( + '20090719_000001:up', + '20090719_000002:up', + )); } - function testMigrateToVersion3() + public function testMigrateToVersion4() { - $this->migrator->migrateToVersion('20090719_000003'); - $this->assertAtVersion('20090719_000003', 3, 3); + $this->migrator->migrateToVersion('20090719_000004'); + $this->assertAtVersion('20090719_000004'); + $this->assertAuditTrail(array( + '20090719_000001:up', + '20090719_000002:up', + '20090719_000004:up', + )); } - function testMigrateToVersion4() + public function testMigrateToVersion3() { - $this->migrator->migrateToVersion('20090719_000004'); - $this->assertAtVersion('20090719_000004', 4, 4); + $this->migrator->migrateToVersion('20090719_000003'); + $this->assertAtVersion('20090719_000003'); + $this->assertAuditTrail(array( + '20090719_000001:up', + '20090719_000002:up', + '20090719_000004:up', + '20090719_000003:up', + )); } - function testMigrateToVersion5() + public function testMigrateToVersion5() { $this->migrator->migrateToVersion('20090719_000005'); - $this->assertAtVersion('20090719_000005', 5, 5); + $this->assertAtVersion('20090719_000005'); + $this->assertAuditTrail(array( + '20090719_000001:up', + '20090719_000002:up', + '20090719_000004:up', + '20090719_000003:up', + '20090719_000005:up', + )); } } diff --git a/test/MigratorYearDirsTest.php b/test/MigratorYearDirsTest.php new file mode 100644 index 0000000..3fee688 --- /dev/null +++ b/test/MigratorYearDirsTest.php @@ -0,0 +1,118 @@ + __DIR__.'/migrations-with-year-dirs', + Migrator::OPT_QUIET => true, + ); + $this->migrator = new Migrator($opts); + $this->migrator->getVersionProvider()->setVersion($this->migrator, 0); // hard-reset to version 0 + } + + public function testFreshMigrationsStartAtVersionZero() + { + $this->assertEquals(Migrator::VERSION_ZERO, $this->migrator->getVersion()); + } + + private function assertAtVersion($version) + { + $this->assertEquals($version, $this->migrator->getVersion(), 'At wrong version.'); + } + + private function assertAuditTrail($expectedAuditTrail) + { + $this->assertEquals($expectedAuditTrail, $this->migrator->getMigrationAuditTrail()); + } + + public function testLatestVersion() + { + $this->assertEquals('20180918_000001', $this->migrator->latestVersion()); + } + + public function testCleanGoesToVersionZero() + { + $this->migrator->clean(); + $this->assertAtVersion(Migrator::VERSION_ZERO); + $this->assertAuditTrail(array()); + } + + public function testMigratingToVersionZero() + { + $this->migrator->getVersionProvider()->setVersion($this->migrator, '20180918_000001'); + $this->migrator->migrateToVersion(Migrator::VERSION_ZERO); + $this->assertAtVersion(Migrator::VERSION_ZERO); + $this->assertAuditTrail(array( + '20180918_000001:down', + '20170918_000002:down', + '20170918_000001:down', + )); + } + + public function testMigratingToHead() + { + $this->migrator->migrateToVersion(Migrator::VERSION_HEAD); + $this->assertAtVersion('20180918_000001'); + $this->assertAuditTrail(array( + '20170918_000001:up', + '20170918_000002:up', + '20180918_000001:up', + )); + } + + public function testMigrateUp() + { + // mock out migrator; make sure UP calls migrate to appropriate version + $this->migrator->getVersionProvider()->setVersion($this->migrator, '20170918_000001'); + + $this->migrator->migrateToVersion(Migrator::VERSION_UP); + $this->assertAtVersion('20170918_000002'); + $this->assertAuditTrail(array( + '20170918_000002:up', + )); + } + + public function testMigrateDown() + { + $this->migrator->getVersionProvider()->setVersion($this->migrator, '20180918_000001'); + $this->migrator->migrateToVersion(Migrator::VERSION_DOWN); + $this->assertAtVersion('20170918_000002'); + $this->assertAuditTrail(array( + '20180918_000001:down', + )); + } + + public function testMigratingToCurrentVersionRunsNoMigrations() + { + $this->migrator->getVersionProvider()->setVersion($this->migrator, '20180918_000001'); + $this->migrator->migrateToVersion('20180918_000001'); + $this->assertAtVersion('20180918_000001'); + $this->assertAuditTrail(array()); + } + + public function testMigrateToVersion1() + { + $this->migrator->migrateToVersion('20170918_000001'); + $this->assertAtVersion('20170918_000001'); + $this->assertAuditTrail(array( + '20170918_000001:up', + )); + } + + public function testMigrateToVersion2() + { + $this->migrator->migrateToVersion('20180918_000001'); + $this->assertAtVersion('20180918_000001'); + $this->assertAuditTrail(array( + '20170918_000001:up', + '20170918_000002:up', + '20180918_000001:up', + )); + } +} diff --git a/test/migrations-with-year-dirs/2017/20170918_000001.php b/test/migrations-with-year-dirs/2017/20170918_000001.php new file mode 100644 index 0000000..261487f --- /dev/null +++ b/test/migrations-with-year-dirs/2017/20170918_000001.php @@ -0,0 +1,17 @@ +