From 40c684eeb5612027c9fbd60bbd893dff2f839dfe Mon Sep 17 00:00:00 2001 From: Alan Pinstein Date: Wed, 12 Nov 2014 11:50:59 -0600 Subject: [PATCH 01/14] Add PHPUnit to require-dev. --- .gitignore | 1 + composer.json | 3 +++ 2 files changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index b5f8d30..1923bbf 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ test/migrations/version.txt *.swp mp-*.tgz package.xml +vendor/ diff --git a/composer.json b/composer.json index d8c6d66..9ae75e0 100644 --- a/composer.json +++ b/composer.json @@ -15,5 +15,8 @@ "require": { "php": ">=5.2.0" }, + "require-dev": { + "phpunit/phpunit": "4.3.5" + }, "bin": ["mp"] } From ae994e02b23de727eca0d1f92f7d46d4aad1c449 Mon Sep 17 00:00:00 2001 From: Alan Pinstein Date: Wed, 12 Nov 2014 11:52:02 -0600 Subject: [PATCH 02/14] Refactor sorting of migration list into separate function in preparation for allowing a manifest file to override sort order. --- Migrator.php | 61 +++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 53 insertions(+), 8 deletions(-) diff --git a/Migrator.php b/Migrator.php index 1d5b89f..4fa5953 100644 --- a/Migrator.php +++ b/Migrator.php @@ -3,8 +3,8 @@ /** * @package Migrator * @copyright Copyright (c) 2005 Alan Pinstein. All Rights Reserved. - * @author Alan Pinstein - * + * @author Alan Pinstein + * * Copyright (c) 2009 Alan Pinstein * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -97,7 +97,7 @@ public function __construct($opts = array()) 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) { @@ -198,7 +198,7 @@ class MigrationUnknownVersionException extends Exception {} abstract class MigratorDelegate { /** - * You can provide a custom {@link MigratorVersionProvider} + * You can provide a custom {@link MigratorVersionProvider} * * @return object MigratorVersionProvider */ @@ -391,19 +391,64 @@ public function setVersion($v) return $this->getVersionProvider()->setVersion($this, $v); } + /** + * @return array An array of the migrations which are manifested by the migrations.json file. + */ + protected function getMigrationsManifest() + { + $manifestFile = "{$this->getMigrationsDirectory()}/migrations.json"; + if (!file_exists($manifestFile)) return NULL; + + $data = file_get_contents($manifestFile); + if ($data === false) 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 createDefaultMigrationsManifest($migrationList) + { + // sort in reverse chronological order + natsort($migrationList); + $migrationList = array_values($migrationList); + + // OPTIONAL? + //$manifestFile = "{$this->getMigrationsDirectory()}/migrations.json"; + //$ok = file_put_contents($manifestFile, json_encode($migrationList)); + //if (!$ok) throw new Exception("Error creating {$manifestFile}."); + + return $migrationList; + } + 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(); + $migrationFileList[$matches[1]] = $file->getFilename(); } - // sort in reverse chronological order - natsort($this->migrationFiles); } + + $manifestedMigrations = $this->getMigrationsManifest(); + if (!$manifestedMigrations) + { + // bootstrap upgrade file + $manifestedMigrations = $this->createDefaultMigrationsManifest(array_keys($migrationFileList)); + } + + // sort migrationFileList by manifested keys + $sortedMigrationFileList = array(); + foreach ($manifestedMigrations as $m) { + $sortedMigrationFileList[$m] = $migrationFileList[$m]; + } + + $this->migrationFiles = $sortedMigrationFileList; } public function logMessage($msg, $onlyIfVerbose = false) @@ -421,7 +466,7 @@ public function getDbCon() } return $this->dbCon; } - + public function setDelegate($d) { if (!is_object($d)) throw new Exception("setDelegate requires an object instance."); From 751c0c091f1b9c94f677453a680a9203a5d2c6a4 Mon Sep 17 00:00:00 2001 From: Alan Pinstein Date: Wed, 12 Nov 2014 13:02:29 -0600 Subject: [PATCH 03/14] Auto-upgrade to migrations.json manifest format when using CLI tool. Add tests. --- Migrator.php | 101 +++++++++++------- mp | 1 + test/MigratorManifestUpgradeTest.php | 49 +++++++++ .../20090719_000001.php | 22 ++++ .../20090719_000002.php | 22 ++++ .../20090719_000003.php | 22 ++++ .../20090719_000004.php | 22 ++++ .../20090719_000005.php | 22 ++++ test/migrations-without-manifest/clean.php | 9 ++ test/migrations-without-manifest/version.txt | 1 + test/migrations/migrations.json | 7 ++ 11 files changed, 241 insertions(+), 37 deletions(-) create mode 100644 test/MigratorManifestUpgradeTest.php create mode 100644 test/migrations-without-manifest/20090719_000001.php create mode 100644 test/migrations-without-manifest/20090719_000002.php create mode 100644 test/migrations-without-manifest/20090719_000003.php create mode 100644 test/migrations-without-manifest/20090719_000004.php create mode 100644 test/migrations-without-manifest/20090719_000005.php create mode 100644 test/migrations-without-manifest/clean.php create mode 100644 test/migrations-without-manifest/version.txt create mode 100644 test/migrations/migrations.json diff --git a/Migrator.php b/Migrator.php index 4fa5953..a5104ac 100644 --- a/Migrator.php +++ b/Migrator.php @@ -192,8 +192,10 @@ 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 { @@ -231,6 +233,7 @@ class Migrator 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'; @@ -267,7 +270,7 @@ class Migrator /** * @var array An array of all migrations installed for this app. */ - protected $migrationFiles = array(); + protected $migrationList = array(); /** * Create a migrator instance. @@ -278,12 +281,13 @@ class Migrator 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, + 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, + Migrator::OPT_OFFER_MANIFEST_UPGRADE => false, ), $opts); // set up initial data @@ -329,13 +333,21 @@ 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[Migrator::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("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) + if (count($this->migrationList) && $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" . @@ -391,13 +403,18 @@ public function setVersion($v) 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->getMigrationsDirectory()}/migrations.json"; - if (!file_exists($manifestFile)) return NULL; + $manifestFile = $this->getMigrationsManifestFile(); + if (!file_exists($manifestFile)) throw new MigrationNoManifestException("No manifest file found: {$manifestFile}"); $data = file_get_contents($manifestFile); if ($data === false) throw new Exception("Error reading migrations.json manifest file."); @@ -408,18 +425,25 @@ protected function getMigrationsManifest() return $migrationList; } - protected function createDefaultMigrationsManifest($migrationList) + protected function upgradeToMigrationsManifest($migrationList) { - // sort in reverse chronological order + $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); - // OPTIONAL? - //$manifestFile = "{$this->getMigrationsDirectory()}/migrations.json"; - //$ok = file_put_contents($manifestFile, json_encode($migrationList)); - //if (!$ok) throw new Exception("Error creating {$manifestFile}."); - - return $migrationList; + // upgrade to manifest-based migration ordering + $indent = " "; + $eol = PHP_EOL; + $quote = '"'; + $ok = file_put_contents($manifestFile, "[{$eol}{$indent}{$quote}" . join("{$quote},{$eol}{$indent}{$quote}", $migrationList) . "{$quote}{$eol}]{$eol}"); + if (!$ok) throw new Exception("Error creating {$manifestFile}."); } protected function collectMigrationFiles() @@ -435,12 +459,15 @@ protected function collectMigrationFiles() } } + return $migrationFileList; + } + + protected function generateMigrationList($migrationFileList) + { $manifestedMigrations = $this->getMigrationsManifest(); - if (!$manifestedMigrations) - { - // bootstrap upgrade file - $manifestedMigrations = $this->createDefaultMigrationsManifest(array_keys($migrationFileList)); - } + $migrationFileCount = count($migrationFileList); + $migrationManifestCount = count($manifestedMigrations); + if ($migrationManifestCount !== $migrationFileCount) throw new MigrationManifestMismatchException("There are {$migrationManifestCount} migrations manifested, but there are {$migrationFileCount} migration files. Please verify your migrations.json manifest."); // sort migrationFileList by manifested keys $sortedMigrationFileList = array(); @@ -448,7 +475,7 @@ protected function collectMigrationFiles() $sortedMigrationFileList[$m] = $migrationFileList[$m]; } - $this->migrationFiles = $sortedMigrationFileList; + $this->migrationList = $sortedMigrationFileList; } public function logMessage($msg, $onlyIfVerbose = false) @@ -501,12 +528,12 @@ public function getVersionProvider() } /** - * 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 integer The index of the migration in the migrationList array. * @throws object MigrationUnknownVersionException */ protected function indexOfVersion($findVersion) @@ -514,7 +541,7 @@ 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) { + foreach (array_keys($this->migrationList) as $version) { if ($version === $findVersion) { $foundCurrent = true; @@ -536,12 +563,12 @@ protected function indexOfVersion($findVersion) */ 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)); + $lastMigration = array_pop(array_keys($this->migrationList)); return $lastMigration; } @@ -556,9 +583,9 @@ public function latestVersion() protected function findNextMigration($currentMigration, $direction) { // special case when no migrations exist - if (count($this->migrationFiles) === 0) return NULL; + if (count($this->migrationList) === 0) return NULL; - $migrationVersions = array_keys($this->migrationFiles); + $migrationVersions = array_keys($this->migrationList); // special case when current == VERSION_ZERO if ($currentMigration === Migrator::VERSION_ZERO) @@ -634,7 +661,7 @@ public function description() 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); } @@ -789,7 +816,7 @@ public function migrateToVersion($toVersion) { $direction = Migrator::DIRECTION_UP; } - else if ($currentVersion === array_pop(array_keys($this->migrationFiles))) + else if ($currentVersion === array_pop(array_keys($this->migrationList))) { $direction = Migrator::DIRECTION_DOWN; } diff --git a/mp b/mp index 877cc80..36058b9 100755 --- a/mp +++ b/mp @@ -153,6 +153,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/test/MigratorManifestUpgradeTest.php b/test/MigratorManifestUpgradeTest.php new file mode 100644 index 0000000..e918df6 --- /dev/null +++ b/test/MigratorManifestUpgradeTest.php @@ -0,0 +1,49 @@ +migrationsDir() . '/migrations.json'; + } + + function teardown() + { + @unlink($this->manifestFile()); + } + + function testMPRequiresManifestFile() + { + $this->setExpectedException('MigrationNoManifestException'); + new Migrator(array( + Migrator::OPT_MIGRATIONS_DIR => $this->migrationsDir(), + Migrator::OPT_QUIET => true, + )); + } + + function testMigrationUpgradeCreatesExpectedManifestFile() + { + $this->assertFalse(file_exists($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->assertTrue(file_exists($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/migrations-without-manifest/20090719_000001.php b/test/migrations-without-manifest/20090719_000001.php new file mode 100644 index 0000000..49c4bf8 --- /dev/null +++ b/test/migrations-without-manifest/20090719_000001.php @@ -0,0 +1,22 @@ + Date: Wed, 12 Nov 2014 14:11:49 -0600 Subject: [PATCH 04/14] Re-factor tests to be much cleaner and rely on internal migration audit trail. --- Migrator.php | 11 ++ test/MigratorTest.php | 134 ++++++++++-------- .../20090719_000001.php | 16 +-- .../20090719_000002.php | 16 +-- .../20090719_000003.php | 16 +-- .../20090719_000004.php | 16 +-- .../20090719_000005.php | 16 +-- test/migrations-without-manifest/version.txt | 2 +- test/migrations/20090719_000001.php | 16 +-- test/migrations/20090719_000002.php | 16 +-- test/migrations/20090719_000003.php | 16 +-- test/migrations/20090719_000004.php | 16 +-- test/migrations/20090719_000005.php | 16 +-- test/migrations/migrations.json | 2 +- 14 files changed, 109 insertions(+), 200 deletions(-) diff --git a/Migrator.php b/Migrator.php index a5104ac..757f7f6 100644 --- a/Migrator.php +++ b/Migrator.php @@ -271,6 +271,10 @@ class Migrator * @var array An array of all migrations installed for this app. */ protected $migrationList = array(); + /** + * @var array An audit trail of the migrations applied during this invocation. + */ + protected $migrationAuditTrail = array(); /** * Create a migrator instance. @@ -360,6 +364,11 @@ public function __construct($opts = array()) } } + public function getMigrationAuditTrail() + { + return $this->migrationAuditTrail; + } + protected function initializeMigrationsDir() { // initialize migrations dir @@ -686,6 +695,8 @@ public function listMigrations() */ public function runMigration($migrationName, $direction) { + $this->migrationAuditTrail[] = "{$migrationName}:{$direction}"; + if ($direction === Migrator::DIRECTION_UP) { $info = array( diff --git a/test/MigratorTest.php b/test/MigratorTest.php index 04bf2e4..35cadf8 100644 --- a/test/MigratorTest.php +++ b/test/MigratorTest.php @@ -2,12 +2,6 @@ require_once 'Migrator.php'; -define('TEST_MIGRATIONS_DIR', './test/migrations'); - -$testMigrationNumber = 0; -$testMigrationIncrementedNumber = 0; -$testMigrationHasRunCounter = 0; - class MigratorTest extends PHPUnit_Framework_TestCase { protected $migrator; @@ -15,15 +9,11 @@ class MigratorTest extends PHPUnit_Framework_TestCase function setup() { $opts = array( - Migrator::OPT_MIGRATIONS_DIR => TEST_MIGRATIONS_DIR, - Migrator::OPT_QUIET => true, + 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() @@ -31,16 +21,14 @@ function testFreshMigrationsStartAtVersionZero() $this->assertEquals(Migrator::VERSION_ZERO, $this->migrator->getVersion()); } - private function assertAtVersion($version, $counter, $numMigrationsRun = NULL) + private function assertAtVersion($version) + { + $this->assertEquals($version, $this->migrator->getVersion(), "At wrong version."); + } + + private function assertAuditTrail($expectedAuditTrail) { - 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($expectedAuditTrail, $this->migrator->getMigrationAuditTrail()); } function testLatestVersion() @@ -51,89 +39,119 @@ function testLatestVersion() function testCleanGoesToVersionZero() { $this->migrator->clean(); - $this->assertAtVersion(Migrator::VERSION_ZERO, 0, 0); + $this->assertAtVersion(Migrator::VERSION_ZERO); + $this->assertAuditTrail(array()); } 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() { $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() { // 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() { - // 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() { + $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() { $this->migrator->migrateToVersion('20090719_000001'); - $this->assertAtVersion('20090719_000001', 1, 1); + $this->assertAtVersion('20090719_000001'); + $this->assertAuditTrail(array( + '20090719_000001:up', + )); } 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() + 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() + 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() { $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/migrations-without-manifest/20090719_000001.php b/test/migrations-without-manifest/20090719_000001.php index 49c4bf8..1a2768a 100644 --- a/test/migrations-without-manifest/20090719_000001.php +++ b/test/migrations-without-manifest/20090719_000001.php @@ -1,20 +1,8 @@ Date: Wed, 12 Nov 2014 14:51:46 -0600 Subject: [PATCH 05/14] Add new migrations to end of manifest file. --- Migrator.php | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/Migrator.php b/Migrator.php index 757f7f6..1e5347e 100644 --- a/Migrator.php +++ b/Migrator.php @@ -448,11 +448,18 @@ protected function upgradeToMigrationsManifest($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}" . join("{$quote},{$eol}{$indent}{$quote}", $migrationList) . "{$quote}{$eol}]{$eol}"); - if (!$ok) throw new Exception("Error creating {$manifestFile}."); + if (!$ok) throw new Exception("Error writing {$manifestFile}."); } protected function collectMigrationFiles() @@ -665,7 +672,12 @@ public function description() $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) From d24dbd26e1f4afd4308ace8171676b46485e1674 Mon Sep 17 00:00:00 2001 From: Alan Pinstein Date: Tue, 25 Nov 2014 15:56:32 -0600 Subject: [PATCH 06/14] Add helpful debugging information when manifest and migration file list do not match. --- Migrator.php | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/Migrator.php b/Migrator.php index 1e5347e..1f84ea0 100644 --- a/Migrator.php +++ b/Migrator.php @@ -483,7 +483,33 @@ protected function generateMigrationList($migrationFileList) $manifestedMigrations = $this->getMigrationsManifest(); $migrationFileCount = count($migrationFileList); $migrationManifestCount = count($manifestedMigrations); - if ($migrationManifestCount !== $migrationFileCount) throw new MigrationManifestMismatchException("There are {$migrationManifestCount} migrations manifested, but there are {$migrationFileCount} migration files. Please verify your migrations.json manifest."); + 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 . join(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 . join(PHP_EOL, $fileButNoManifest); + } + $msg =<< Date: Fri, 30 Mar 2018 09:35:12 -0400 Subject: [PATCH 07/14] PHP7 compat fix array to string conversion --- Migrator.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Migrator.php b/Migrator.php index 1f84ea0..6d4d4a8 100644 --- a/Migrator.php +++ b/Migrator.php @@ -754,7 +754,8 @@ public function runMigration($migrationName, $direction) $migration = $this->instantiateMigration($migrationName); $this->logMessage("Running {$migrationName} {$info['actionName']}: " . $migration->description() . "\n", false); try { - $migration->$info['migrateF']($this); + $upOrDown = $info['migrateF']; + $migration->$upOrDown($this); if ($direction === Migrator::DIRECTION_UP) { $this->setVersion($migrationName); From 7982bad2c9c1c57fd09a86faa3aca0178a79a64a Mon Sep 17 00:00:00 2001 From: Jason Bouffard Date: Mon, 20 Aug 2018 14:41:42 -0400 Subject: [PATCH 08/14] a little future proofing --- .gitignore | 1 + Migrator.php | 13 ++++++++----- composer.json | 4 ++-- pearfarm.spec | 17 ----------------- test/MigratorManifestUpgradeTest.php | 6 ++++-- test/MigratorTest.php | 6 ++++-- test/migrations/clean.php | 4 ++-- 7 files changed, 21 insertions(+), 30 deletions(-) delete mode 100644 pearfarm.spec diff --git a/.gitignore b/.gitignore index 1923bbf..b5e6e4e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ test/migrations/version.txt mp-*.tgz package.xml vendor/ +composer.lock diff --git a/Migrator.php b/Migrator.php index 6d4d4a8..f73db57 100644 --- a/Migrator.php +++ b/Migrator.php @@ -610,7 +610,10 @@ public function latestVersion() $this->logMessage("No migrations available.\n"); return true; } - $lastMigration = array_pop(array_keys($this->migrationList)); + + $migrationListKeys = array_keys($this->migrationList); + $lastMigration = array_pop($migrationListKeys); + return $lastMigration; } @@ -820,7 +823,7 @@ public function migrateToVersion($toVersion) { $toVersion = $this->findNextMigration($currentVersion, Migrator::DIRECTION_UP); } - else if ($toVersion === Migrator::VERSION_DOWN) + elseif ($toVersion === Migrator::VERSION_DOWN) { $toVersion = $this->findNextMigration($currentVersion, Migrator::DIRECTION_DOWN); if (!$toVersion) @@ -828,7 +831,7 @@ public function migrateToVersion($toVersion) $toVersion = Migrator::VERSION_ZERO; } } - else if ($toVersion === Migrator::VERSION_HEAD) + elseif ($toVersion === Migrator::VERSION_HEAD) { $toVersion = $this->latestVersion(); $this->logMessage("Resolved head to {$toVersion}\n", true); @@ -866,11 +869,11 @@ public function migrateToVersion($toVersion) { $direction = Migrator::DIRECTION_UP; } - else if ($currentVersion === array_pop(array_keys($this->migrationList))) + elseif ($currentVersion === $this->latestVersion()) { $direction = Migrator::DIRECTION_DOWN; } - else if ($toVersion === Migrator::VERSION_ZERO) + elseif ($toVersion === Migrator::VERSION_ZERO) { $direction = Migrator::DIRECTION_DOWN; } diff --git a/composer.json b/composer.json index 9ae75e0..e48de65 100644 --- a/composer.json +++ b/composer.json @@ -13,10 +13,10 @@ } ], "require": { - "php": ">=5.2.0" + "php": ">=7.1.0" }, "require-dev": { - "phpunit/phpunit": "4.3.5" + "phpunit/phpunit": "^7" }, "bin": ["mp"] } 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/test/MigratorManifestUpgradeTest.php b/test/MigratorManifestUpgradeTest.php index e918df6..eb6e6d0 100644 --- a/test/MigratorManifestUpgradeTest.php +++ b/test/MigratorManifestUpgradeTest.php @@ -2,7 +2,9 @@ require_once __DIR__ . '/../Migrator.php'; -class MigratorManifestUpgradeTest extends PHPUnit_Framework_TestCase +use PHPUnit\Framework\TestCase; + +class MigratorManifestUpgradeTest extends TestCase { private function migrationsDir() { @@ -20,7 +22,7 @@ function teardown() function testMPRequiresManifestFile() { - $this->setExpectedException('MigrationNoManifestException'); + $this->expectException('MigrationNoManifestException'); new Migrator(array( Migrator::OPT_MIGRATIONS_DIR => $this->migrationsDir(), Migrator::OPT_QUIET => true, diff --git a/test/MigratorTest.php b/test/MigratorTest.php index 35cadf8..ac4ba15 100644 --- a/test/MigratorTest.php +++ b/test/MigratorTest.php @@ -1,8 +1,10 @@ Date: Tue, 21 Aug 2018 08:44:51 -0400 Subject: [PATCH 09/14] adds php-cs-fixer --- .gitignore | 3 +- .php_cs.dist | 18 + Migrator.php | 556 +++++++++--------- phpunit.xml.dist | 20 + test/MigratorManifestUpgradeTest.php | 37 +- test/MigratorTest.php | 40 +- .../20090719_000001.php | 13 +- .../20090719_000002.php | 13 +- .../20090719_000003.php | 13 +- .../20090719_000004.php | 13 +- .../20090719_000005.php | 13 +- test/migrations-without-manifest/clean.php | 2 +- test/migrations/20090719_000001.php | 13 +- test/migrations/20090719_000002.php | 13 +- test/migrations/20090719_000003.php | 13 +- test/migrations/20090719_000004.php | 13 +- test/migrations/20090719_000005.php | 13 +- 17 files changed, 462 insertions(+), 344 deletions(-) create mode 100644 .php_cs.dist create mode 100644 phpunit.xml.dist diff --git a/.gitignore b/.gitignore index b5e6e4e..4df88fe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ +vendor/ migrations/* test/migrations/version.txt *.swp mp-*.tgz package.xml -vendor/ 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 f73db57..41b9be8 100644 --- a/Migrator.php +++ b/Migrator.php @@ -1,7 +1,7 @@ * @@ -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,26 +73,26 @@ 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) @@ -99,14 +100,13 @@ 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}';"; $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,39 +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 MigrationNoManifestException extends Exception {} -class MigrationManifestMismatchException 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. @@ -222,29 +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 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'; + 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; /** @@ -256,79 +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 $migrationList = array(); /** - * @var array An audit trail of the migrations applied during this invocation. + * @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, - Migrator::OPT_OFFER_MANIFEST_UPGRADE => 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()); } } @@ -339,26 +356,24 @@ public function __construct($opts = array()) $this->initializeMigrationsDir(); $migrationFileList = $this->collectMigrationFiles(); - if (!file_exists($this->getMigrationsManifestFile()) && $opts[Migrator::OPT_OFFER_MANIFEST_UPGRADE]) - { + 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->migrationList) . " migrations: " . print_r($this->migrationList, 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->migrationList) && $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" ); } @@ -373,8 +388,7 @@ 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); } } @@ -401,14 +415,14 @@ 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); } @@ -423,13 +437,17 @@ protected function getMigrationsManifestFile() protected function getMigrationsManifest() { $manifestFile = $this->getMigrationsManifestFile(); - if (!file_exists($manifestFile)) throw new MigrationNoManifestException("No manifest file found: {$manifestFile}"); - + if (!file_exists($manifestFile)) { + throw new MigrationNoManifestException("No manifest file found: {$manifestFile}"); + } $data = file_get_contents($manifestFile); - if ($data === false) throw new Exception("Error reading migrations.json manifest file."); - + 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."); + if (!$migrationList) { + throw new Exception('Error decoding migrations.json manifest.'); + } return $migrationList; } @@ -437,8 +455,7 @@ protected function getMigrationsManifest() protected function upgradeToMigrationsManifest($migrationList) { $manifestFile = $this->getMigrationsManifestFile(); - if (file_exists($manifestFile)) - { + if (file_exists($manifestFile)) { return; } @@ -455,22 +472,27 @@ protected function writeMigrationsManifest($migrationList) { $manifestFile = $this->getMigrationsManifestFile(); - $indent = " "; + $indent = ' '; $eol = PHP_EOL; $quote = '"'; - $ok = file_put_contents($manifestFile, "[{$eol}{$indent}{$quote}" . join("{$quote},{$eol}{$indent}{$quote}", $migrationList) . "{$quote}{$eol}]{$eol}"); - if (!$ok) throw new Exception("Error writing {$manifestFile}."); + $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; + if ($file->isDot()) { + continue; + } + if ($file->isDir()) { + continue; + } $matches = array(); - if (preg_match('/^([0-9]{8}_[0-9]{6}).php$/', $file->getFilename(), $matches)) - { + if (preg_match('/^([0-9]{8}_[0-9]{6}).php$/', $file->getFilename(), $matches)) { $migrationFileList[$matches[1]] = $file->getFilename(); } } @@ -483,23 +505,20 @@ protected function generateMigrationList($migrationFileList) $manifestedMigrations = $this->getMigrationsManifest(); $migrationFileCount = count($migrationFileList); $migrationManifestCount = count($manifestedMigrations); - if ($migrationManifestCount !== $migrationFileCount) - { + 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 . join(PHP_EOL, $manifestedButNoFile); + $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 . join(PHP_EOL, $fileButNoManifest); + $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); } - $msg =<<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; } @@ -550,6 +575,7 @@ public function getDelegate() public function setMigrationsDirectory($dir) { $this->migrationsDirectory = $dir; + return $this; } @@ -560,10 +586,14 @@ 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; @@ -575,7 +605,9 @@ public function getVersionProvider() * 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 migrationList array. + * + * @return int the index of the migration in the migrationList array + * * @throws object MigrationUnknownVersionException */ protected function indexOfVersion($findVersion) @@ -584,30 +616,29 @@ protected function indexOfVersion($findVersion) $foundCurrent = false; $currentIndex = 0; foreach (array_keys($this->migrationList) as $version) { - if ($version === $findVersion) - { + 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->migrationList)) - { + if (empty($this->migrationList)) { $this->logMessage("No migrations available.\n"); + return true; } @@ -621,52 +652,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->migrationList) === 0) return NULL; + if (0 === count($this->migrationList)) { + return null; + } $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. * @@ -675,7 +704,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."); + $filePath = $this->getMigrationsDirectory()."/{$filename}"; + if (file_exists($filePath)) { + throw new Exception("Migration {$dts} already exists! Aborting."); + } file_put_contents($filePath, $tpl); // add migrations.json @@ -711,106 +742,107 @@ public function description() private function instantiateMigration($migrationName) { - require_once($this->getMigrationsDirectory() . "/" . $this->migrationList[$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) { $this->migrationAuditTrail[] = "{$migrationName}:{$direction}"; - if ($direction === Migrator::DIRECTION_UP) - { + 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 { $upOrDown = $info['migrateF']; $migration->$upOrDown($this); - if ($direction === Migrator::DIRECTION_UP) - { + 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) { @@ -819,45 +851,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); - } - elseif ($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; } - } - elseif ($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) { @@ -865,64 +890,52 @@ public function migrateToVersion($toVersion) } // calculate direction - if ($currentVersion === Migrator::VERSION_ZERO) - { - $direction = Migrator::DIRECTION_UP; - } - elseif ($currentVersion === $this->latestVersion()) - { - $direction = Migrator::DIRECTION_DOWN; - } - elseif ($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; } } @@ -937,25 +950,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/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 index eb6e6d0..aecb6c6 100644 --- a/test/MigratorManifestUpgradeTest.php +++ b/test/MigratorManifestUpgradeTest.php @@ -1,6 +1,6 @@ migrationsDir() . '/migrations.json'; + return $this->migrationsDir().'/migrations.json'; } - function teardown() + public function teardown() { @unlink($this->manifestFile()); } - function testMPRequiresManifestFile() + public function testMPRequiresManifestFile() { $this->expectException('MigrationNoManifestException'); new Migrator(array( - Migrator::OPT_MIGRATIONS_DIR => $this->migrationsDir(), - Migrator::OPT_QUIET => true, + Migrator::OPT_MIGRATIONS_DIR => $this->migrationsDir(), + Migrator::OPT_QUIET => true, )); } - function testMigrationUpgradeCreatesExpectedManifestFile() + public function testMigrationUpgradeCreatesExpectedManifestFile() { - $this->assertFalse(file_exists($this->manifestFile()), "Shouldn't be a manifest file yet."); + $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, + Migrator::OPT_MIGRATIONS_DIR => $this->migrationsDir(), + Migrator::OPT_QUIET => true, + Migrator::OPT_OFFER_MANIFEST_UPGRADE => true, )); - $this->assertTrue(file_exists($this->manifestFile()), "Should be a manifest file now."); + $this->assertFileExists($this->manifestFile(), 'Should be a manifest file now.'); $this->assertEquals(array( - "20090719_000001", - "20090719_000002", - "20090719_000003", - "20090719_000004", - "20090719_000005", + '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 ac4ba15..936d281 100644 --- a/test/MigratorTest.php +++ b/test/MigratorTest.php @@ -1,6 +1,6 @@ __DIR__ . '/migrations', - Migrator::OPT_QUIET => true, + 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 } - function testFreshMigrationsStartAtVersionZero() + public function testFreshMigrationsStartAtVersionZero() { $this->assertEquals(Migrator::VERSION_ZERO, $this->migrator->getVersion()); } private function assertAtVersion($version) { - $this->assertEquals($version, $this->migrator->getVersion(), "At wrong version."); + $this->assertEquals($version, $this->migrator->getVersion(), 'At wrong version.'); } private function assertAuditTrail($expectedAuditTrail) @@ -33,19 +33,19 @@ private function assertAuditTrail($expectedAuditTrail) $this->assertEquals($expectedAuditTrail, $this->migrator->getMigrationAuditTrail()); } - function testLatestVersion() + public function testLatestVersion() { $this->assertEquals('20090719_000005', $this->migrator->latestVersion()); } - function testCleanGoesToVersionZero() + public function testCleanGoesToVersionZero() { $this->migrator->clean(); $this->assertAtVersion(Migrator::VERSION_ZERO); $this->assertAuditTrail(array()); } - function testMigratingToVersionZero() + public function testMigratingToVersionZero() { $this->migrator->getVersionProvider()->setVersion($this->migrator, '20090719_000005'); $this->migrator->migrateToVersion(Migrator::VERSION_ZERO); @@ -59,7 +59,7 @@ function testMigratingToVersionZero() )); } - function testMigratingToHead() + public function testMigratingToHead() { $this->migrator->migrateToVersion(Migrator::VERSION_HEAD); $this->assertAtVersion('20090719_000005'); @@ -72,7 +72,7 @@ function testMigratingToHead() )); } - function testMigrateUp() + public function testMigrateUp() { // mock out migrator; make sure UP calls migrate to appropriate version $this->migrator->getVersionProvider()->setVersion($this->migrator, '20090719_000001'); @@ -80,21 +80,21 @@ function testMigrateUp() $this->migrator->migrateToVersion(Migrator::VERSION_UP); $this->assertAtVersion('20090719_000002'); $this->assertAuditTrail(array( - '20090719_000002:up' + '20090719_000002:up', )); } - function testMigrateDown() + public function testMigrateDown() { $this->migrator->getVersionProvider()->setVersion($this->migrator, '20090719_000002'); $this->migrator->migrateToVersion(Migrator::VERSION_DOWN); $this->assertAtVersion('20090719_000001'); $this->assertAuditTrail(array( - '20090719_000002:down' + '20090719_000002:down', )); } - function testMigratingToCurrentVersionRunsNoMigrations() + public function testMigratingToCurrentVersionRunsNoMigrations() { $this->migrator->getVersionProvider()->setVersion($this->migrator, '20090719_000002'); $this->migrator->migrateToVersion('20090719_000002'); @@ -102,7 +102,7 @@ function testMigratingToCurrentVersionRunsNoMigrations() $this->assertAuditTrail(array()); } - function testMigrateToVersion1() + public function testMigrateToVersion1() { $this->migrator->migrateToVersion('20090719_000001'); $this->assertAtVersion('20090719_000001'); @@ -111,7 +111,7 @@ function testMigrateToVersion1() )); } - function testMigrateToVersion2() + public function testMigrateToVersion2() { $this->migrator->migrateToVersion('20090719_000002'); $this->assertAtVersion('20090719_000002'); @@ -121,7 +121,7 @@ function testMigrateToVersion2() )); } - function testMigrateToVersion4() + public function testMigrateToVersion4() { $this->migrator->migrateToVersion('20090719_000004'); $this->assertAtVersion('20090719_000004'); @@ -132,7 +132,7 @@ function testMigrateToVersion4() )); } - function testMigrateToVersion3() + public function testMigrateToVersion3() { $this->migrator->migrateToVersion('20090719_000003'); $this->assertAtVersion('20090719_000003'); @@ -144,7 +144,7 @@ function testMigrateToVersion3() )); } - function testMigrateToVersion5() + public function testMigrateToVersion5() { $this->migrator->migrateToVersion('20090719_000005'); $this->assertAtVersion('20090719_000005'); diff --git a/test/migrations-without-manifest/20090719_000001.php b/test/migrations-without-manifest/20090719_000001.php index 1a2768a..e5a9fac 100644 --- a/test/migrations-without-manifest/20090719_000001.php +++ b/test/migrations-without-manifest/20090719_000001.php @@ -1,10 +1,17 @@ Date: Tue, 18 Sep 2018 09:54:35 -0400 Subject: [PATCH 10/14] support year directories in migrations dir --- Migrator.php | 25 +++- composer.json | 3 + test/MigratorYearDirsTest.php | 118 ++++++++++++++++++ .../2017/20170918_000001.php | 17 +++ .../2017/20170918_000002.php | 17 +++ .../2018/20180918_000001.php | 17 +++ .../migrations-with-year-dirs/migrations.json | 5 + test/migrations-with-year-dirs/version.txt | 1 + 8 files changed, 198 insertions(+), 5 deletions(-) create mode 100644 test/MigratorYearDirsTest.php create mode 100644 test/migrations-with-year-dirs/2017/20170918_000001.php create mode 100644 test/migrations-with-year-dirs/2017/20170918_000002.php create mode 100644 test/migrations-with-year-dirs/2018/20180918_000001.php create mode 100644 test/migrations-with-year-dirs/migrations.json create mode 100644 test/migrations-with-year-dirs/version.txt diff --git a/Migrator.php b/Migrator.php index 41b9be8..414b400 100644 --- a/Migrator.php +++ b/Migrator.php @@ -489,11 +489,26 @@ protected function collectMigrationFiles() continue; } if ($file->isDir()) { - continue; - } - $matches = array(); - if (preg_match('/^([0-9]{8}_[0-9]{6}).php$/', $file->getFilename(), $matches)) { - $migrationFileList[$matches[1]] = $file->getFilename(); + 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(); + } } } diff --git a/composer.json b/composer.json index e48de65..c86b9d7 100644 --- a/composer.json +++ b/composer.json @@ -18,5 +18,8 @@ "require-dev": { "phpunit/phpunit": "^7" }, + "autoload": { + "files": ["Migrator.php"] + }, "bin": ["mp"] } 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 @@ + Date: Tue, 18 Sep 2018 09:57:32 -0400 Subject: [PATCH 11/14] do not need this file --- test/migrations-with-year-dirs/version.txt | 1 - 1 file changed, 1 deletion(-) delete mode 100644 test/migrations-with-year-dirs/version.txt diff --git a/test/migrations-with-year-dirs/version.txt b/test/migrations-with-year-dirs/version.txt deleted file mode 100644 index ffb0102..0000000 --- a/test/migrations-with-year-dirs/version.txt +++ /dev/null @@ -1 +0,0 @@ -20180918_000001 \ No newline at end of file From eb8c448dc7969ae958925d7034d559b48e7dd985 Mon Sep 17 00:00:00 2001 From: Jason Bouffard Date: Tue, 18 Sep 2018 10:03:58 -0400 Subject: [PATCH 12/14] create migration detects year dir --- Migrator.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Migrator.php b/Migrator.php index 414b400..f233c25 100644 --- a/Migrator.php +++ b/Migrator.php @@ -742,7 +742,13 @@ public function description() } } END; - $filePath = $this->getMigrationsDirectory()."/{$filename}"; + 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."); } From 2056b2a426d6e5d02299874d589e9f7f33ee9313 Mon Sep 17 00:00:00 2001 From: Jason Bouffard Date: Tue, 18 Sep 2018 10:38:03 -0400 Subject: [PATCH 13/14] use composer autoload --- mp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/mp b/mp index 36058b9..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(); From 481aed45040b1cc7ffb9d340a3346a109035637b Mon Sep 17 00:00:00 2001 From: Jason Bouffard Date: Tue, 18 Sep 2018 15:37:27 -0400 Subject: [PATCH 14/14] markdown readme --- README => README.md | 40 +++++++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 9 deletions(-) rename README => README.md (93%) 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.