diff --git a/src/CakeAdapter.php b/src/CakeAdapter.php index 56f9c173..40ee2776 100644 --- a/src/CakeAdapter.php +++ b/src/CakeAdapter.php @@ -12,6 +12,7 @@ namespace Migrations; use Cake\Database\Connection; +use PDO; use Phinx\Db\Adapter\AdapterInterface; use Phinx\Db\Table; use Phinx\Db\Table\Column; @@ -52,6 +53,10 @@ public function __construct(AdapterInterface $adapter, Connection $connection) $this->adapter = $adapter; $this->connection = $connection; $pdo = $adapter->getConnection(); + + if ($pdo->getAttribute(PDO::ATTR_ERRMODE) !== PDO::ERRMODE_EXCEPTION) { + $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + } $connection->driver()->connection($pdo); } diff --git a/src/CakeManager.php b/src/CakeManager.php index d61fe2d1..991156c3 100644 --- a/src/CakeManager.php +++ b/src/CakeManager.php @@ -186,6 +186,95 @@ public function markMigrated($version, $path) return true; } + /** + * Decides which versions it should mark as migrated + * + * @param \Symfony\Component\Console\Input\InputInterface $input Input interface from which argument and options + * will be extracted to determine which versions to be marked as migrated + * @return array Array of versions that should be marked as migrated + * @throws \InvalidArgumentException If the `--exclude` or `--only` options are used without `--target` + * or version not found + */ + public function getVersionsToMark($input) + { + $migrations = $this->getMigrations(); + $versions = array_keys($migrations); + + $versionArg = $input->getArgument('version'); + $targetArg = $input->getOption('target'); + $hasAllVersion = in_array($versionArg, ['all', '*']); + if ((empty($versionArg) && empty($targetArg)) || $hasAllVersion) { + return $versions; + } + + $version = $targetArg ?: $versionArg; + + if ($input->getOption('only') || !empty($versionArg)) { + if (!in_array($version, $versions)) { + throw new \InvalidArgumentException("Migration `$version` was not found !"); + } + + return [$version]; + } + + $lengthIncrease = $input->getOption('exclude') ? 0 : 1; + $index = array_search($version, $versions); + + if ($index === false) { + throw new \InvalidArgumentException("Migration `$version` was not found !"); + } + + return array_slice($versions, 0, $index + $lengthIncrease); + } + + /** + * Mark all migrations in $versions array found in $path as migrated + * + * It will start a transaction and rollback in case one of the operation raises an exception + * + * @param string $path Path where to look for migrations + * @param array $versions Versions which should be marked + * @param \Symfony\Component\Console\Output\OutputInterface $output OutputInterface used to store + * the command output + * @return void + */ + public function markVersionsAsMigrated($path, $versions, $output) + { + $adapter = $this->getEnvironment('default')->getAdapter(); + + if (empty($versions)) { + $output->writeln('No migrations were found. Nothing to mark as migrated.'); + return; + } + + $adapter->beginTransaction(); + foreach ($versions as $version) { + if ($this->isMigrated($version)) { + $output->writeln(sprintf('Skipping migration `%s` (already migrated).', $version)); + continue; + } + + try { + $this->markMigrated($version, $path); + $output->writeln( + sprintf('Migration `%s` successfully marked migrated !', $version) + ); + } catch (\Exception $e) { + $adapter->rollbackTransaction(); + $output->writeln( + sprintf( + 'An error occurred while marking migration `%s` as migrated : %s', + $version, + $e->getMessage() + ) + ); + $output->writeln('All marked migrations during this process were unmarked.'); + return; + } + } + $adapter->commitTransaction(); + } + /** * Resolves a migration class name based on $path * diff --git a/src/Command/MarkMigrated.php b/src/Command/MarkMigrated.php index 5d7e54d2..ebb9ca3e 100644 --- a/src/Command/MarkMigrated.php +++ b/src/Command/MarkMigrated.php @@ -122,122 +122,13 @@ protected function execute(InputInterface $input, OutputInterface $output) } try { - $versions = $this->getVersionsToMark(); + $versions = $this->getManager()->getVersionsToMark($input); } catch (InvalidArgumentException $e) { $output->writeln(sprintf("%s", $e->getMessage())); return; } - $this->markVersionsAsMigrated($path, $versions); - } - - /** - * Mark all migrations in $versions array found in $path as migrated - * - * It will start a transaction and rollback in case one of the operation raises an exception - * - * @param string $path Path where to look for migrations - * @param array $versions Versions which should be marked - * @return void - */ - protected function markVersionsAsMigrated($path, $versions) - { - $manager = $this->getManager(); - $adapter = $manager->getEnvironment('default')->getAdapter(); - $output = $this->output(); - - if (empty($versions)) { - $output->writeln('No migrations were found. Nothing to mark as migrated.'); - return; - } - - $adapter->beginTransaction(); - foreach ($versions as $version) { - if ($manager->isMigrated($version)) { - $output->writeln(sprintf('Skipping migration `%s` (already migrated).', $version)); - continue; - } - - try { - $this->getManager()->markMigrated($version, $path); - $output->writeln( - sprintf('Migration `%s` successfully marked migrated !', $version) - ); - } catch (\Exception $e) { - $adapter->rollbackTransaction(); - $output->writeln( - sprintf( - 'An error occurred while marking migration `%s` as migrated : %s', - $version, - $e->getMessage() - ) - ); - $output->writeln('All marked migrations during this process were unmarked.'); - return; - } - } - $adapter->commitTransaction(); - } - - /** - * Decides which versions it should mark as migrated - * - * @return array Array of versions that should be marked as migrated - * @throws \InvalidArgumentException If the `--exclude` or `--only` options are used without `--target` - * or version not found - */ - protected function getVersionsToMark() - { - $migrations = $this->getManager()->getMigrations(); - $versions = array_keys($migrations); - - if ($this->isAllVersion()) { - return $versions; - } - - $version = $this->getTarget(); - - if ($this->isOnly()) { - if (!in_array($version, $versions)) { - throw new InvalidArgumentException("Migration `$version` was not found !"); - } - - return [$version]; - } - - $lengthIncrease = $this->hasExclude() ? 0 : 1; - $index = array_search($version, $versions); - - if ($index === false) { - throw new \InvalidArgumentException("Migration `$version` was not found !"); - } - - return array_slice($versions, 0, $index + $lengthIncrease); - } - - /** - * Returns the target version from `--target` or from the deprecated version argument - * - * @return string Version found as target - */ - protected function getTarget() - { - $target = $this->input->getOption('target'); - return $target ? $target : $this->input->getArgument('version'); - } - - /** - * Checks if the $version is for all migrations - * - * @return bool Returns true if it should try to mark all versions - */ - protected function isAllVersion() - { - if ($this->isUsingDeprecatedVersion()) { - return false; - } - - return $this->isUsingDeprecatedAll() || $this->input->getOption('target') === null; + $this->getManager()->markVersionsAsMigrated($path, $versions, $output); } /** @@ -251,17 +142,6 @@ protected function isUsingDeprecatedAll() return $version === 'all' || $version === '*'; } - /** - * Checks if the input has the `--only` option or it is using the deprecated version argument - * - * @return bool Returns true when it is trying to mark only one migration - */ - protected function isOnly() - { - - return $this->hasOnly() || $this->isUsingDeprecatedVersion(); - } - /** * Checks if the input has the `--exclude` option * diff --git a/src/Migrations.php b/src/Migrations.php index a4970030..b131880e 100644 --- a/src/Migrations.php +++ b/src/Migrations.php @@ -193,20 +193,28 @@ public function rollback($options = []) * * @return bool Success */ - public function markMigrated($version, $options = []) + public function markMigrated($version = null, $options = []) { $this->setCommand('mark_migrated'); - $input = $this->getInput('MarkMigrated', ['version' => $version], $options); - $params = [$version]; - $isMigrated = $this->run('isMigrated', $params, $input); - if ($isMigrated) { - return true; + if (isset($options['target']) && + isset($options['exclude']) && + isset($options['only']) + ) { + $exceptionMessage = 'You should use `exclude` OR `only` (not both) along with a `target` argument'; + throw new \InvalidArgumentException($exceptionMessage); } - $params[] = $this->getConfig()->getMigrationPath(); + $input = $this->getInput('MarkMigrated', ['version' => $version], $options); + $this->setInput($input); + + $params = [ + $this->getConfig()->getMigrationPath(), + $this->getManager()->getVersionsToMark($input), + $this->output + ]; - $this->run('markMigrated', $params, $input); + $this->run('markVersionsAsMigrated', $params, $input); return true; } diff --git a/tests/TestCase/MigrationsTest.php b/tests/TestCase/MigrationsTest.php index 91f2de6b..8fb41d70 100644 --- a/tests/TestCase/MigrationsTest.php +++ b/tests/TestCase/MigrationsTest.php @@ -188,38 +188,240 @@ public function testMigrateAndRollback() } /** - * Tests that migrate returns false in case of error - * and can return a error message + * Tests calling Migrations::markMigrated without params marks everything + * as migrated * - * @expectedException Exception + * @return void */ - public function testMigrateErrors() + public function testMarkMigratedAll() { - $this->migrations->markMigrated(20150704160200); - $this->migrations->migrate(); + $markMigrated = $this->migrations->markMigrated(); + $this->assertTrue($markMigrated); + $status = $this->migrations->status(); + $expected = [ + [ + 'status' => 'up', + 'id' => '20150704160200', + 'name' => 'CreateNumbersTable' + ], + [ + 'status' => 'up', + 'id' => '20150724233100', + 'name' => 'UpdateNumbersTable' + ], + [ + 'status' => 'up', + 'id' => '20150826191400', + 'name' => 'CreateLettersTable' + ] + ]; + $this->assertEquals($expected, $status); } /** - * Tests that rollback returns false in case of error - * and can return a error message + * Tests calling Migrations::markMigrated with the argument $version as the + * string 'all' marks everything + * as migrated * - * @expectedException Exception + * @return void */ - public function testRollbackErrors() + public function testMarkMigratedAllAsVersion() { - $this->migrations->markMigrated('all'); - $this->migrations->rollback(); + $markMigrated = $this->migrations->markMigrated('all'); + $this->assertTrue($markMigrated); + $status = $this->migrations->status(); + $expected = [ + [ + 'status' => 'up', + 'id' => '20150704160200', + 'name' => 'CreateNumbersTable' + ], + [ + 'status' => 'up', + 'id' => '20150724233100', + 'name' => 'UpdateNumbersTable' + ], + [ + 'status' => 'up', + 'id' => '20150826191400', + 'name' => 'CreateLettersTable' + ] + ]; + $this->assertEquals($expected, $status); } /** - * Tests that marking migrated a non-existant migrations returns an error - * and can return a error message + * Tests calling Migrations::markMigrated with the target option will mark + * only up to that one * - * @expectedException Exception + * @return void */ - public function testMarkMigratedErrors() + public function testMarkMigratedTarget() { - $this->migrations->markMigrated(20150704000000); + $markMigrated = $this->migrations->markMigrated(null, ['target' => '20150704160200']); + $this->assertTrue($markMigrated); + $status = $this->migrations->status(); + $expected = [ + [ + 'status' => 'up', + 'id' => '20150704160200', + 'name' => 'CreateNumbersTable' + ], + [ + 'status' => 'down', + 'id' => '20150724233100', + 'name' => 'UpdateNumbersTable' + ], + [ + 'status' => 'down', + 'id' => '20150826191400', + 'name' => 'CreateLettersTable' + ] + ]; + $this->assertEquals($expected, $status); + + $markMigrated = $this->migrations->markMigrated(null, ['target' => '20150826191400']); + $this->assertTrue($markMigrated); + $status = $this->migrations->status(); + $expected[1]['status'] = $expected[2]['status'] = 'up'; + $this->assertEquals($expected, $status); + } + + /** + * Tests calling Migrations::markMigrated with the target option set to a + * non-existent target will throw an exception + * + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Migration `20150704160610` was not found ! + * @return void + */ + public function testMarkMigratedTargetError() + { + $this->migrations->markMigrated(null, ['target' => '20150704160610']); + } + + /** + * Tests calling Migrations::markMigrated with the target option with the exclude + * option will mark only up to that one, excluding it + * + * @return void + */ + public function testMarkMigratedTargetExclude() + { + $markMigrated = $this->migrations->markMigrated(null, ['target' => '20150704160200', 'exclude' => true]); + $this->assertTrue($markMigrated); + $status = $this->migrations->status(); + $expected = [ + [ + 'status' => 'down', + 'id' => '20150704160200', + 'name' => 'CreateNumbersTable' + ], + [ + 'status' => 'down', + 'id' => '20150724233100', + 'name' => 'UpdateNumbersTable' + ], + [ + 'status' => 'down', + 'id' => '20150826191400', + 'name' => 'CreateLettersTable' + ] + ]; + $this->assertEquals($expected, $status); + + $markMigrated = $this->migrations->markMigrated(null, ['target' => '20150826191400', 'exclude' => true]); + $this->assertTrue($markMigrated); + $status = $this->migrations->status(); + $expected[0]['status'] = $expected[1]['status'] = 'up'; + $this->assertEquals($expected, $status); + } + + /** + * Tests calling Migrations::markMigrated with the target option with the only + * option will mark only that specific migrations + * + * @return void + */ + public function testMarkMigratedTargetOnly() + { + $markMigrated = $this->migrations->markMigrated(null, ['target' => '20150724233100', 'only' => true]); + $this->assertTrue($markMigrated); + $status = $this->migrations->status(); + $expected = [ + [ + 'status' => 'down', + 'id' => '20150704160200', + 'name' => 'CreateNumbersTable' + ], + [ + 'status' => 'up', + 'id' => '20150724233100', + 'name' => 'UpdateNumbersTable' + ], + [ + 'status' => 'down', + 'id' => '20150826191400', + 'name' => 'CreateLettersTable' + ] + ]; + $this->assertEquals($expected, $status); + + $markMigrated = $this->migrations->markMigrated(null, ['target' => '20150826191400', 'only' => true]); + $this->assertTrue($markMigrated); + $status = $this->migrations->status(); + $expected[2]['status'] = 'up'; + $this->assertEquals($expected, $status); + } + + /** + * Tests calling Migrations::markMigrated with the target option, the only option + * and the exclude option will throw an exception + * + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage You should use `exclude` OR `only` (not both) along with a `target` argument + * @return void + */ + public function testMarkMigratedTargetExcludeOnly() + { + $this->migrations->markMigrated(null, ['target' => '20150724233100', 'only' => true, 'exclude' => true]); + } + + /** + * Tests calling Migrations::markMigrated with the target option with the exclude + * option will mark only up to that one, excluding it + * + * @return void + */ + public function testMarkMigratedVersion() + { + $markMigrated = $this->migrations->markMigrated(20150704160200); + $this->assertTrue($markMigrated); + $status = $this->migrations->status(); + $expected = [ + [ + 'status' => 'up', + 'id' => '20150704160200', + 'name' => 'CreateNumbersTable' + ], + [ + 'status' => 'down', + 'id' => '20150724233100', + 'name' => 'UpdateNumbersTable' + ], + [ + 'status' => 'down', + 'id' => '20150826191400', + 'name' => 'CreateLettersTable' + ] + ]; + $this->assertEquals($expected, $status); + + $markMigrated = $this->migrations->markMigrated(20150826191400); + $this->assertTrue($markMigrated); + $status = $this->migrations->status(); + $expected[2]['status'] = 'up'; + $this->assertEquals($expected, $status); } /** @@ -580,10 +782,13 @@ public function testMigrateSnapshots($basePath, $files) foreach ($files as $file) { list($filename, $timestamp) = $file; $copiedFileName = $timestamp . '_' . $filename . '.php'; - copy( - $basePath . $filename . '.php', - $destination . $copiedFileName - ); + + if (!file_exists($destination . $copiedFileName)) { + copy( + $basePath . $filename . '.php', + $destination . $copiedFileName + ); + } $result = $this->migrations->migrate(['source' => 'SnapshotTests']); $this->assertTrue($result); @@ -593,6 +798,39 @@ public function testMigrateSnapshots($basePath, $files) } } + /** + * Tests that migrating in case of error throws an exception + * + * @expectedException \Exception + */ + public function testMigrateErrors() + { + $this->migrations->markMigrated(20150704160200); + $this->migrations->migrate(); + } + + /** + * Tests that rolling back in case of error throws an exception + * + * @expectedException \Exception + */ + public function testRollbackErrors() + { + $this->migrations->markMigrated('all'); + $this->migrations->rollback(); + } + + /** + * Tests that marking migrated a non-existant migrations returns an error + * and can return a error message + * + * @expectedException \Exception + */ + public function testMarkMigratedErrors() + { + $this->migrations->markMigrated(20150704000000); + } + /** * provides the path to the baked migrations *