diff --git a/CHANGELOG.md b/CHANGELOG.md index 651bb6db..cf066994 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,10 +43,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this * [#440](https://github.com/shlinkio/shlink/pull/440) Created `db:create` command, which improves how the shlink database is created, with these benefits: - * It sets up a lock which prevents the command to be run multiple times. + * It sets up a lock which prevents the command to be run concurrently. * It checks of the database does not exist, and creates it in that case. * It checks if the database tables already exist, exiting gracefully in that case. + * [#442](https://github.com/shlinkio/shlink/pull/442) Created `db:migrate` command, which improves doctrine's migrations command by generating a lock, preventing it to be run concurrently. + #### Changed * [#430](https://github.com/shlinkio/shlink/issues/430) Updated to [shlinkio/php-coding-standard](https://github.com/shlinkio/php-coding-standard) 1.2.2 diff --git a/README.md b/README.md index f2c13057..861988cb 100644 --- a/README.md +++ b/README.md @@ -270,6 +270,7 @@ Available commands: config:generate-secret [DEPRECATED] Generates a random secret string that can be used for JWT token encryption db db:create Creates the database needed for shlink to work. It will do nothing if the database already exists + db:migrate Runs database migrations, which will ensure the shlink database is up to date. short-url short-url:delete [short-code:delete] Deletes a short URL short-url:generate [shortcode:generate|short-code:generate] Generates a short URL for provided long URL and returns it diff --git a/config/autoload/installer.global.php b/config/autoload/installer.global.php index 7739ae4c..fa5648fe 100644 --- a/config/autoload/installer.global.php +++ b/config/autoload/installer.global.php @@ -40,9 +40,9 @@ return [ 'db_create_schema' => [ 'command' => 'bin/cli db:create', ], -// 'db_migrate' => [ -// 'command' => 'bin/cli db:migrate', -// ], + 'db_migrate' => [ + 'command' => 'bin/cli db:migrate', + ], ], ]; diff --git a/module/CLI/config/cli.config.php b/module/CLI/config/cli.config.php index 512090c2..f562ddde 100644 --- a/module/CLI/config/cli.config.php +++ b/module/CLI/config/cli.config.php @@ -30,6 +30,7 @@ return [ Command\Tag\DeleteTagsCommand::NAME => Command\Tag\DeleteTagsCommand::class, Command\Db\CreateDatabaseCommand::NAME => Command\Db\CreateDatabaseCommand::class, + Command\Db\MigrateDatabaseCommand::NAME => Command\Db\MigrateDatabaseCommand::class, ], ], diff --git a/module/CLI/config/dependencies.config.php b/module/CLI/config/dependencies.config.php index 69ace4b3..10870e08 100644 --- a/module/CLI/config/dependencies.config.php +++ b/module/CLI/config/dependencies.config.php @@ -51,6 +51,7 @@ return [ Command\Tag\DeleteTagsCommand::class => ConfigAbstractFactory::class, Command\Db\CreateDatabaseCommand::class => ConfigAbstractFactory::class, + Command\Db\MigrateDatabaseCommand::class => ConfigAbstractFactory::class, ], ], @@ -88,6 +89,11 @@ return [ Connection::class, NoDbNameConnectionFactory::SERVICE_NAME, ], + Command\Db\MigrateDatabaseCommand::class => [ + Locker::class, + SymfonyCli\Helper\ProcessHelper::class, + PhpExecutableFinder::class, + ], ], ]; diff --git a/module/CLI/src/Command/Db/AbstractDatabaseCommand.php b/module/CLI/src/Command/Db/AbstractDatabaseCommand.php index 99eb5c36..d593d6f6 100644 --- a/module/CLI/src/Command/Db/AbstractDatabaseCommand.php +++ b/module/CLI/src/Command/Db/AbstractDatabaseCommand.php @@ -28,6 +28,6 @@ abstract class AbstractDatabaseCommand extends AbstractLockedCommand protected function runPhpCommand(OutputInterface $output, array $command): void { array_unshift($command, $this->phpBinary); - $this->processHelper->run($output, $command); + $this->processHelper->run($output, $command, null, null, $output->getVerbosity()); } } diff --git a/module/CLI/src/Command/Db/MigrateDatabaseCommand.php b/module/CLI/src/Command/Db/MigrateDatabaseCommand.php new file mode 100644 index 00000000..a610705a --- /dev/null +++ b/module/CLI/src/Command/Db/MigrateDatabaseCommand.php @@ -0,0 +1,40 @@ +setName(self::NAME) + ->setDescription('Runs database migrations, which will ensure the shlink database is up to date.'); + } + + protected function lockedExecute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + + $io->writeln('Migrating database...'); + $this->runPhpCommand($output, [self::DOCTRINE_HELPER_SCRIPT, self::DOCTRINE_HELPER_COMMAND]); + $io->success('Database properly migrated!'); + + return ExitCodes::EXIT_SUCCESS; + } + + protected function getLockConfig(): LockedCommandConfig + { + return new LockedCommandConfig($this->getName(), true); + } +} diff --git a/module/CLI/test/Command/Db/CreateDatabaseCommandTest.php b/module/CLI/test/Command/Db/CreateDatabaseCommandTest.php index 813a5d69..90ef45c5 100644 --- a/module/CLI/test/Command/Db/CreateDatabaseCommandTest.php +++ b/module/CLI/test/Command/Db/CreateDatabaseCommandTest.php @@ -107,7 +107,7 @@ class CreateDatabaseCommandTest extends TestCase } /** @test */ - public function tablesAreCreatedIfDatabaseIsEMpty(): void + public function tablesAreCreatedIfDatabaseIsEmpty(): void { $shlinkDatabase = 'shlink_database'; $getDatabase = $this->regularConn->getDatabase()->willReturn($shlinkDatabase); @@ -119,7 +119,7 @@ class CreateDatabaseCommandTest extends TestCase '/usr/local/bin/php', CreateDatabaseCommand::DOCTRINE_HELPER_SCRIPT, CreateDatabaseCommand::DOCTRINE_HELPER_COMMAND, - ]); + ], Argument::cetera()); $this->commandTester->execute([]); $output = $this->commandTester->getDisplay(); diff --git a/module/CLI/test/Command/Db/MigrateDatabaseCommandTest.php b/module/CLI/test/Command/Db/MigrateDatabaseCommandTest.php new file mode 100644 index 00000000..69d3ec9f --- /dev/null +++ b/module/CLI/test/Command/Db/MigrateDatabaseCommandTest.php @@ -0,0 +1,82 @@ +prophesize(Locker::class); + $lock = $this->prophesize(LockInterface::class); + $lock->acquire(Argument::any())->willReturn(true); + $lock->release()->will(function () { + }); + $locker->createLock(Argument::cetera())->willReturn($lock->reveal()); + + $phpExecutableFinder = $this->prophesize(PhpExecutableFinder::class); + $phpExecutableFinder->find(false)->willReturn('/usr/local/bin/php'); + + $this->processHelper = $this->prophesize(ProcessHelper::class); + + $command = new MigrateDatabaseCommand( + $locker->reveal(), + $this->processHelper->reveal(), + $phpExecutableFinder->reveal() + ); + $app = new Application(); + $app->add($command); + + $this->commandTester = new CommandTester($command); + } + + /** + * @test + * @dataProvider provideVerbosities + */ + public function migrationsCommandIsRunWithProperVerbosity(int $verbosity): void + { + $runCommand = $this->processHelper->run(Argument::type(OutputInterface::class), [ + '/usr/local/bin/php', + MigrateDatabaseCommand::DOCTRINE_HELPER_SCRIPT, + MigrateDatabaseCommand::DOCTRINE_HELPER_COMMAND, + ], null, null, $verbosity); + + $this->commandTester->execute([], [ + 'verbosity' => $verbosity, + ]); + $output = $this->commandTester->getDisplay(); + + if ($verbosity >= OutputInterface::VERBOSITY_VERBOSE) { + $this->assertStringContainsString('Migrating database...', $output); + $this->assertStringContainsString('Database properly migrated!', $output); + } + $runCommand->shouldHaveBeenCalledOnce(); + } + + public function provideVerbosities(): iterable + { + yield 'debug' => [OutputInterface::VERBOSITY_DEBUG]; + yield 'normal' => [OutputInterface::VERBOSITY_NORMAL]; + yield 'quiet' => [OutputInterface::VERBOSITY_QUIET]; + yield 'verbose' => [OutputInterface::VERBOSITY_VERBOSE]; + yield 'very verbose' => [OutputInterface::VERBOSITY_VERY_VERBOSE]; + } +}