diff --git a/composer.json b/composer.json index 8443897ba..7889d208e 100644 --- a/composer.json +++ b/composer.json @@ -25,6 +25,7 @@ "symfony/http-kernel": "^3.4|^4.0|^5.0" }, "require-dev": { + "composer/semver": "^3.0@dev", "doctrine/doctrine-bundle": "^1.8|^2.0", "doctrine/orm": "^2.3", "friendsofphp/php-cs-fixer": "^2.8", diff --git a/src/Console/MigrationDiffFilteredOutput.php b/src/Console/MigrationDiffFilteredOutput.php new file mode 100644 index 000000000..c1f18835c --- /dev/null +++ b/src/Console/MigrationDiffFilteredOutput.php @@ -0,0 +1,137 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\MakerBundle\Console; + +use Symfony\Component\Console\Formatter\OutputFormatterInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class MigrationDiffFilteredOutput implements OutputInterface +{ + private $output; + private $buffer = ''; + private $previousLineWasRemoved = false; + + public function __construct(OutputInterface $output) + { + $this->output = $output; + } + + public function write($messages, $newline = false, $options = 0) + { + $messages = $this->filterMessages($messages, $newline); + + $this->output->write($messages, $newline, $options); + } + + public function writeln($messages, $options = 0) + { + $messages = $this->filterMessages($messages, true); + + $this->output->writeln($messages, $options); + } + + public function setVerbosity($level) + { + $this->output->setVerbosity($level); + } + + public function getVerbosity() + { + return $this->output->getVerbosity(); + } + + public function isQuiet() + { + return $this->output->isQuiet(); + } + + public function isVerbose() + { + return $this->output->isVerbose(); + } + + public function isVeryVerbose() + { + return $this->output->isVeryVerbose(); + } + + public function isDebug() + { + return $this->output->isDebug(); + } + + public function setDecorated($decorated) + { + $this->output->setDecorated($decorated); + } + + public function isDecorated() + { + return $this->output->isDecorated(); + } + + public function setFormatter(OutputFormatterInterface $formatter) + { + $this->output->setFormatter($formatter); + } + + public function getFormatter() + { + return $this->output->getFormatter(); + } + + public function fetch(): string + { + return $this->buffer; + } + + private function filterMessages($messages, bool $newLine) + { + if (!is_iterable($messages)) { + $messages = [$messages]; + } + + $hiddenPhrases = [ + 'Generated new migration class', + 'To run just this migration', + 'To revert the migration you', + ]; + + foreach ($messages as $key => $message) { + $this->buffer .= $message; + + if ($newLine) { + $this->buffer .= PHP_EOL; + } + + if ($this->previousLineWasRemoved && !trim($message)) { + // hide a blank line after a filtered line + unset($messages[$key]); + $this->previousLineWasRemoved = false; + + continue; + } + + $this->previousLineWasRemoved = false; + foreach ($hiddenPhrases as $hiddenPhrase) { + if (false !== strpos($message, $hiddenPhrase)) { + $this->previousLineWasRemoved = true; + unset($messages[$key]); + + break; + } + } + } + + return array_values($messages); + } +} diff --git a/src/ConsoleStyle.php b/src/ConsoleStyle.php index 2d816c028..6bd6758b1 100644 --- a/src/ConsoleStyle.php +++ b/src/ConsoleStyle.php @@ -11,6 +11,8 @@ namespace Symfony\Bundle\MakerBundle; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; /** @@ -19,6 +21,15 @@ */ final class ConsoleStyle extends SymfonyStyle { + private $output; + + public function __construct(InputInterface $input, OutputInterface $output) + { + $this->output = $output; + + parent::__construct($input, $output); + } + public function success($message) { $this->writeln('OK '.$message); @@ -28,4 +39,9 @@ public function comment($message) { $this->text($message); } + + public function getOutput(): OutputInterface + { + return $this->output; + } } diff --git a/src/Maker/MakeMigration.php b/src/Maker/MakeMigration.php index 1cb47c726..2453a5dba 100644 --- a/src/Maker/MakeMigration.php +++ b/src/Maker/MakeMigration.php @@ -11,8 +11,10 @@ namespace Symfony\Bundle\MakerBundle\Maker; +use Doctrine\Bundle\MigrationsBundle\Command\MigrationsDiffDoctrineCommand; use Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle; use Symfony\Bundle\MakerBundle\ApplicationAwareMakerInterface; +use Symfony\Bundle\MakerBundle\Console\MigrationDiffFilteredOutput; use Symfony\Bundle\MakerBundle\ConsoleStyle; use Symfony\Bundle\MakerBundle\DependencyBuilder; use Symfony\Bundle\MakerBundle\Generator; @@ -22,7 +24,6 @@ use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\BufferedOutput; /** * @author Amrouche Hamza @@ -56,31 +57,47 @@ public function configureCommand(Command $command, InputConfiguration $inputConf { $command ->setDescription('Creates a new migration based on database changes') - ->addOption('db', null, InputOption::VALUE_REQUIRED, 'The database connection name') - ->addOption('em', null, InputOption::VALUE_OPTIONAL, 'The entity manager name') - ->addOption('shard', null, InputOption::VALUE_REQUIRED, 'The shard connection name') ->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeMigration.txt')) ; + + if (class_exists(MigrationsDiffDoctrineCommand::class)) { + // support for DoctrineMigrationsBundle 2.x + $command + ->addOption('db', null, InputOption::VALUE_REQUIRED, 'The database connection name') + ->addOption('em', null, InputOption::VALUE_OPTIONAL, 'The entity manager name') + ->addOption('shard', null, InputOption::VALUE_REQUIRED, 'The shard connection name') + ; + } } public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator) { $options = ['doctrine:migrations:diff']; - if (null !== $input->getOption('db')) { + + // DoctrineMigrationsBundle 2.x support + if ($input->hasOption('db') && null !== $input->getOption('db')) { $options[] = '--db='.$input->getOption('db'); } - if (null !== $input->getOption('em')) { + if ($input->hasOption('em') && null !== $input->getOption('em')) { $options[] = '--em='.$input->getOption('em'); } - if (null !== $input->getOption('shard')) { + if ($input->hasOption('shard') && null !== $input->getOption('shard')) { $options[] = '--shard='.$input->getOption('shard'); } + // end 2.x support $generateMigrationCommand = $this->application->find('doctrine:migrations:diff'); - $commandOutput = new BufferedOutput($io->getVerbosity()); + $commandOutput = new MigrationDiffFilteredOutput($io->getOutput()); try { - $generateMigrationCommand->run(new ArgvInput($options), $commandOutput); + $returnCode = $generateMigrationCommand->run(new ArgvInput($options), $commandOutput); + + // non-zero code would ideally mean the internal command has already printed an errror + // this happens if you "decline" generating a migration when you already + // have some available + if (0 !== $returnCode) { + return $returnCode; + } $migrationOutput = $commandOutput->fetch(); diff --git a/src/Test/MakerTestCase.php b/src/Test/MakerTestCase.php index a9a4a85af..7691f405a 100644 --- a/src/Test/MakerTestCase.php +++ b/src/Test/MakerTestCase.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\MakerBundle\Test; +use Composer\Semver\Semver; use PHPUnit\Framework\TestCase; use Symfony\Bundle\MakerBundle\MakerInterface; use Symfony\Bundle\MakerBundle\Str; @@ -40,6 +41,10 @@ protected function executeMakerCommand(MakerTestDetails $testDetails) // prepare environment to test $testEnv->prepare(); + if (!$this->hasRequiredDependencyVersions($testDetails, $testEnv)) { + $this->markTestSkipped('Some dependencies versions are too low'); + } + // run tests $makerTestProcess = $testEnv->runMaker(); $files = $testEnv->getGeneratedFilesFromOutputText(); @@ -95,4 +100,32 @@ protected function getMakerInstance(string $makerClass): MakerInterface return $this->kernel->getContainer()->get($serviceId); } + + private function hasRequiredDependencyVersions(MakerTestDetails $testDetails, MakerTestEnvironment $testEnv): bool + { + if (empty($testDetails->getRequiredPackageVersions())) { + return true; + } + + $installedPackages = json_decode($testEnv->readFile('vendor/composer/installed.json'), true); + $packageVersions = []; + foreach ($installedPackages as $installedPackage) { + $packageVersions[$installedPackage['name']] = $installedPackage['version_normalized']; + } + + foreach ($testDetails->getRequiredPackageVersions() as $requiredPackageData) { + $name = $requiredPackageData['name']; + $versionConstraint = $requiredPackageData['version_constraint']; + + if (!isset($packageVersions[$name])) { + throw new \Exception(sprintf('Package "%s" is required in the test project at version "%s" but it is not installed?', $name, $versionConstraint)); + } + + if (!Semver::satisfies($packageVersions[$name], $versionConstraint)) { + return false; + } + } + + return true; + } } diff --git a/src/Test/MakerTestDetails.php b/src/Test/MakerTestDetails.php index 2d39f5431..39ead8e10 100644 --- a/src/Test/MakerTestDetails.php +++ b/src/Test/MakerTestDetails.php @@ -44,6 +44,8 @@ final class MakerTestDetails private $requiredPhpVersion; + private $requiredPackageVersions = []; + private $guardAuthenticators = []; /** @@ -216,6 +218,13 @@ public function setRequiredPhpVersion(int $version): self return $this; } + public function addRequiredPackageVersion(string $packageName, string $versionConstraint): self + { + $this->requiredPackageVersions[] = ['name' => $packageName, 'version_constraint' => $versionConstraint]; + + return $this; + } + public function setGuardAuthenticator(string $firewallName, string $id): self { $this->guardAuthenticators[$firewallName] = $id; @@ -316,4 +325,9 @@ public function getGuardAuthenticators(): array { return $this->guardAuthenticators; } + + public function getRequiredPackageVersions(): array + { + return $this->requiredPackageVersions; + } } diff --git a/src/Test/MakerTestEnvironment.php b/src/Test/MakerTestEnvironment.php index 179bfd182..9e7bf23f2 100644 --- a/src/Test/MakerTestEnvironment.php +++ b/src/Test/MakerTestEnvironment.php @@ -69,6 +69,15 @@ public function getPath(): string return $this->path; } + public function readFile(string $path): string + { + if (!file_exists($this->path.'/'.$path)) { + throw new \InvalidArgumentException(sprintf('Cannot find file "%s"', $path)); + } + + return file_get_contents($this->path.'/'.$path); + } + private function changeRootNamespaceIfNeeded() { if ('App' === ($rootNamespace = $this->testDetails->getRootNamespace())) { diff --git a/tests/Maker/MakeMigrationTest.php b/tests/Maker/MakeMigrationTest.php index c37fef9c3..502377fa9 100644 --- a/tests/Maker/MakeMigrationTest.php +++ b/tests/Maker/MakeMigrationTest.php @@ -51,8 +51,8 @@ public function getTestDetails() $this->getMakerInstance(MakeMigration::class), [/* no input */]) ->setFixtureFilesPath(__DIR__.'/../fixtures/MakeMigration') - ->configureDatabase() // sync the database, so no changes are needed + ->configureDatabase() ->addExtraDependencies('doctrine/orm:@stable') ->assert(function (string $output, string $directory) { $this->assertNotContains('Success', $output); @@ -60,5 +60,41 @@ public function getTestDetails() $this->assertStringContainsString('No database changes were detected', $output); }), ]; + + yield 'migration_with_previous_migration_question' => [MakerTestDetails::createTest( + $this->getMakerInstance(MakeMigration::class), + [ + // confirm migration + 'y', + ]) + ->setFixtureFilesPath(__DIR__.'/../fixtures/MakeMigration') + ->configureDatabase(false) + ->addRequiredPackageVersion('doctrine/doctrine-migrations-bundle', '>=3') + ->addExtraDependencies('doctrine/orm:@stable') + // generate a migration first + ->addPreMakeCommand('php bin/console make:migration') + ->assert(function (string $output, string $directory) { + $this->assertStringContainsString('You have 1 available migrations to execute', $output); + $this->assertStringContainsString('Success', $output); + $this->assertCount(14, explode("\n", $output), 'Asserting that very specific output is shown - some should be hidden'); + }), + ]; + + yield 'migration_with_previous_migration_decline_question' => [MakerTestDetails::createTest( + $this->getMakerInstance(MakeMigration::class), + [ + // no to confirm + 'n', + ]) + ->setFixtureFilesPath(__DIR__.'/../fixtures/MakeMigration') + ->configureDatabase(false) + ->addRequiredPackageVersion('doctrine/doctrine-migrations-bundle', '>=3') + ->addExtraDependencies('doctrine/orm:@stable') + // generate a migration first + ->addPreMakeCommand('php bin/console make:migration') + ->assert(function (string $output, string $directory) { + $this->assertStringNotContainsString('Success', $output); + }), + ]; } }