Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Don't Prompt the User for Confirmation on the migrations:migrate Command When There is Nothing to Do #480

Merged
merged 7 commits into from
Nov 18, 2016
26 changes: 21 additions & 5 deletions lib/Doctrine/DBAL/Migrations/Migration.php
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,11 @@ public function setNoMigrationException($noMigrationException = false)
* @param string $to The version to migrate to.
* @param boolean $dryRun Whether or not to make this a dry run and not execute anything.
* @param boolean $timeAllQueries Measuring or not the execution time of each SQL query.
*
* @return array $sql The array of migration sql statements
*
* @param callable|null $confirm A callback to confirm whether the migrations should be executed.
* @return array|false An array of migration sql statements or false if the confirm callback denied execution
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only thing that bothers me in this PR is the return array or false here. Wouldn't it be better if it returned an empty array in this case ? @chrisguitarguy What do you think.

I can make the change tomorrow if you agree to it ?

* @throws MigrationException
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please keep these lines separating params from the return value. It makes things more readable IMO

*/
public function migrate($to = null, $dryRun = false, $timeAllQueries = false)
public function migrate($to = null, $dryRun = false, $timeAllQueries = false, callable $confirm = null)
{
/**
* If no version to migrate to is given we default to the last available one.
Expand Down Expand Up @@ -156,7 +155,11 @@ public function migrate($to = null, $dryRun = false, $timeAllQueries = false)
* to signify that there is nothing left to do.
*/
if ($from === $to && empty($migrationsToExecute) && !empty($migrations)) {
return [];
return $this->noMigrations();
}

if (!$dryRun && false === $this->migrationsCanExecute($confirm)) {
return false;
}

$output = $dryRun ? 'Executing dry run of migration' : 'Migrating';
Expand All @@ -168,6 +171,8 @@ public function migrate($to = null, $dryRun = false, $timeAllQueries = false)
*/
if (empty($migrationsToExecute) && !$this->noMigrationException) {
throw MigrationException::noMigrationsToExecute();
} elseif (empty($migrationsToExecute)) {
return $this->noMigrations();
}

$sql = [];
Expand All @@ -185,4 +190,15 @@ public function migrate($to = null, $dryRun = false, $timeAllQueries = false)

return $sql;
}

private function noMigrations()
{
$this->outputWriter->write('<comment>No migrations to execute.</comment>');
return [];
}

private function migrationsCanExecute(callable $confirm=null)
{
return null === $confirm ? true : $confirm();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ protected function configure()
public function execute(InputInterface $input, OutputInterface $output)
{
$configuration = $this->getMigrationConfiguration($input, $output);
$migration = new Migration($configuration);
$migration = $this->createMigration($configuration);

$this->outputHeader($configuration, $output);

Expand Down Expand Up @@ -124,30 +124,36 @@ public function execute(InputInterface $input, OutputInterface $output)
if ($path = $input->getOption('write-sql')) {
$path = is_bool($path) ? getcwd() : $path;
$migration->writeSqlFile($path, $version);
} else {
$dryRun = (boolean) $input->getOption('dry-run');

// warn the user if no dry run and interaction is on
if (! $dryRun) {
$question = 'WARNING! You are about to execute a database migration'
. ' that could result in schema changes and data lost.'
. ' Are you sure you wish to continue? (y/n)';
if (! $this->canExecute($question, $input, $output)) {
$output->writeln('<error>Migration cancelled!</error>');

return 1;
}
}
return 0;
}

$migration->setNoMigrationException($input->getOption('allow-no-migration'));
$sql = $migration->migrate($version, $dryRun, $timeAllqueries);
$dryRun = (boolean) $input->getOption('dry-run');

if (empty($sql)) {
$output->writeln('<comment>No migrations to execute.</comment>');
}
$migration->setNoMigrationException($input->getOption('allow-no-migration'));
$result = $migration->migrate($version, $dryRun, $timeAllqueries, function () use ($input, $output) {
$question = 'WARNING! You are about to execute a database migration'
. ' that could result in schema changes and data lost.'
. ' Are you sure you wish to continue? (y/n)';
return $this->canExecute($question, $input, $output);
});

if (false === $result) {
$output->writeln('<error>Migration cancelled!</error>');
return 1;
}
}

/**
* Create a new migration instance to execute the migrations.
*
* @param $configuration The configuration with which the migrations will be executed
* @return Migration a new migration instance
*/
protected function createMigration(Configuration $configuration)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why adding a new method for this ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Purely to make the command easier to unit test using getMockForAbstractClass to override this factory method. Ideally there'd probably be a factory object/interface or something, but that seemed like overkill.

Some of the functional tests cover the happy paths that deal with an actual Migration object, but the unit test coverage of the command was pretty slim.

{
return new Migration($configuration);
}

/**
* @param string $question
* @param InputInterface $input
Expand Down
72 changes: 71 additions & 1 deletion tests/Doctrine/DBAL/Migrations/Tests/MigrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ function realpath($path)
use Doctrine\DBAL\Migrations\Migration;
use Doctrine\DBAL\Migrations\MigrationException;
use Doctrine\DBAL\Migrations\OutputWriter;
use Doctrine\DBAL\Migrations\Tests\Stub\Functional\MigrateNotTouchingTheSchema;
use \Mockery as m;
use org\bovigo\vfs\vfsStream;
use org\bovigo\vfs\vfsStreamFile;
Expand All @@ -54,14 +55,16 @@ function realpath($path)
*/
class MigrationTest extends MigrationTestCase
{
private $conn;
/** @var Configuration */
private $config;

protected $output;

protected function setUp()
{
$this->config = new Configuration($this->getSqliteConnection());
$this->conn = $this->getSqliteConnection();
$this->config = new Configuration($this->conn);
$this->config->setMigrationsDirectory(__DIR__ . DIRECTORY_SEPARATOR . 'Stub/migration-empty-folder');
$this->config->setMigrationsNamespace('DoctrineMigrations\\');
}
Expand Down Expand Up @@ -90,10 +93,18 @@ public function testMigrateWithNoMigrationsThrowsException()

public function testMigrateWithNoMigrationsDontThrowsExceptionIfContiniousIntegrationOption()
{
$messages = [];
$output = new OutputWriter(function ($msg) use (&$messages) {
$messages[] = $msg;
});
$this->config->setOutputWriter($output);
$migration = new Migration($this->config);

$migration->setNoMigrationException(true);
$migration->migrate();

$this->assertCount(2, $messages, 'should output header and no migrations message');
$this->assertContains('No migrations', $messages[1]);
}

/**
Expand Down Expand Up @@ -186,4 +197,63 @@ public function testWriteSqlFileShouldUseStandardCommentMarkerInSql()
$this->assertRegExp('/^\s*-- Version 1/m', $sqlMigrationFile->getContent());
$this->assertNotRegExp('/^\s*#/m', $sqlMigrationFile->getContent());
}

public function testMigrateWithMigrationsAndAddTheCurrentVersionOutputsANoMigrationsMessage()
{
$messages = [];
$output = new OutputWriter(function ($msg) use (&$messages) {
$messages[] = $msg;
});
$this->config->setOutputWriter($output);
$this->config->setMigrationsDirectory(__DIR__.'/Stub/migrations-empty-folder');
$this->config->setMigrationsNamespace('DoctrineMigrations\\');
$this->config->registerMigration('20160707000000', MigrateNotTouchingTheSchema::class);
$this->config->createMigrationTable();
$this->conn->insert($this->config->getMigrationsTableName(), [
'version' => '20160707000000',
]);

$migration = new Migration($this->config);

$migration->migrate();

$this->assertCount(1, $messages, 'should output the no migrations message');
$this->assertContains('No migrations', $messages[0]);
}

public function testMigrateReturnsFalseWhenTheConfirmationIsDeclined()
{
$this->config->setMigrationsDirectory(__DIR__.'/Stub/migrations-empty-folder');
$this->config->setMigrationsNamespace('DoctrineMigrations\\');
$this->config->registerMigration('20160707000000', MigrateNotTouchingTheSchema::class);
$this->config->createMigrationTable();
$called = false;
$migration = new Migration($this->config);

$result = $migration->migrate(null, false, false, function () use (&$called) {
$called = true;
return false;
});

$this->assertFalse($result);
$this->assertTrue($called, 'should have called the confirmation callback');
}

public function testMigrateWithDryRunDoesNotCallTheConfirmationCallback()
{
$this->config->setMigrationsDirectory(__DIR__.'/Stub/migrations-empty-folder');
$this->config->setMigrationsNamespace('DoctrineMigrations\\');
$this->config->registerMigration('20160707000000', MigrateNotTouchingTheSchema::class);
$this->config->createMigrationTable();
$called = false;
$migration = new Migration($this->config);

$result = $migration->migrate(null, true, false, function () use (&$called) {
$called = true;
return false;
});

$this->assertFalse($called);
$this->assertEquals(['20160707000000' => ['SELECT 1']], $result);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace Doctrine\DBAL\Migrations\Tests\Tools\Console\Command;

use Symfony\Component\Console\Application;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Helper\DialogHelper;

trait DialogSupport
{
protected $questions, $isDialogHelper;

protected function configureDialogs(Application $app)
{
if (class_exists(QuestionHelper::class)) {
$this->isDialogHelper = false;
$this->questions = $this->getMock(QuestionHelper::class);
} else {
$this->isDialogHelper = true;
$this->questions = $this->getMock(DialogHelper::class);
}
$app->getHelperSet()->set($this->questions, $this->isDialogHelper ? 'dialog' : 'question');
}

protected function willAskConfirmationAndReturn($bool)
{
$this->questions->expects($this->once())
->method($this->isDialogHelper ? 'askConfirmation' : 'ask')
->willReturn($bool);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@

class ExecuteCommandTest extends CommandTestCase
{
use DialogSupport;

const VERSION = '20160705000000';

private $version, $questions, $isDialogHelper;
private $version;

public function testWriteSqlCommandOutputsSqlFileToTheCurrentWorkingDirectory()
{
Expand Down Expand Up @@ -94,14 +96,7 @@ protected function setUp()
->with(self::VERSION)
->willReturn($this->version);

if (class_exists(QuestionHelper::class)) {
$this->isDialogHelper = false;
$this->questions = $this->getMock(QuestionHelper::class);
} else {
$this->isDialogHelper = true;
$this->questions = $this->getMock(DialogHelper::class);
}
$this->app->getHelperSet()->set($this->questions, $this->isDialogHelper ? 'dialog' : 'question');
$this->configureDialogs($this->app);
}

protected function createCommand()
Expand All @@ -121,11 +116,4 @@ private function mockWithoutConstructor($cls)
->disableOriginalConstructor()
->getMock();
}

private function willAskConfirmationAndReturn($bool)
{
$this->questions->expects($this->once())
->method($this->isDialogHelper ? 'askConfirmation' : 'ask')
->willReturn($bool);
}
}
Loading