Merge pull request #442 from acelaya/feature/locked-migrations-command

Feature/locked migrations command
This commit is contained in:
Alejandro Celaya 2019-08-06 21:16:11 +02:00 committed by GitHub
commit acf2961f9e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 139 additions and 7 deletions

View file

@ -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

View file

@ -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

View file

@ -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',
],
],
];

View file

@ -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,
],
],

View file

@ -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,
],
],
];

View file

@ -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());
}
}

View file

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Db;
use Shlinkio\Shlink\CLI\Command\Util\LockedCommandConfig;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
class MigrateDatabaseCommand extends AbstractDatabaseCommand
{
public const NAME = 'db:migrate';
public const DOCTRINE_HELPER_SCRIPT = 'vendor/doctrine/migrations/bin/doctrine-migrations.php';
public const DOCTRINE_HELPER_COMMAND = 'migrations:migrate';
protected function configure(): void
{
$this
->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('<fg=blue>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);
}
}

View file

@ -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();

View file

@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Db;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Db\MigrateDatabaseCommand;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Helper\ProcessHelper;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Component\Lock\Factory as Locker;
use Symfony\Component\Lock\LockInterface;
use Symfony\Component\Process\PhpExecutableFinder;
class MigrateDatabaseCommandTest extends TestCase
{
/** @var CommandTester */
private $commandTester;
/** @var ObjectProphecy */
private $processHelper;
public function setUp(): void
{
$locker = $this->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];
}
}