Skip to content

Commit 80dfa5c

Browse files
committed
Add execute migration command
1 parent 406f9b3 commit 80dfa5c

File tree

14 files changed

+369
-116
lines changed

14 files changed

+369
-116
lines changed

src/Console/CommandHelperTrait.php

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the FiveLab Migrator package
5+
*
6+
* (c) FiveLab
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code
10+
*/
11+
12+
declare(strict_types = 1);
13+
14+
namespace FiveLab\Component\Migrator\Console;
15+
16+
use FiveLab\Component\Migrator\MigrateDirection;
17+
use FiveLab\Component\Migrator\MigrationExecutedState;
18+
use FiveLab\Component\Migrator\MigrationResult;
19+
use Symfony\Component\Console\Command\Command;
20+
use Symfony\Component\Console\Input\InputArgument;
21+
use Symfony\Component\Console\Input\InputInterface;
22+
use Symfony\Component\Console\Input\InputOption;
23+
use Symfony\Component\Console\Output\OutputInterface;
24+
use Symfony\Component\Console\Style\SymfonyStyle;
25+
26+
trait CommandHelperTrait
27+
{
28+
protected function configureMigrateInput(Command $command, bool $requireVersion = false): void
29+
{
30+
$command
31+
->addArgument('group', InputArgument::REQUIRED, 'The group in which to run migrations. ')
32+
->addArgument('version', $requireVersion ? InputArgument::REQUIRED : InputArgument::OPTIONAL, 'The version to run migrations.')
33+
->addOption('down', null, InputOption::VALUE_NONE, 'Down migrations.');
34+
}
35+
36+
/**
37+
* Read input data
38+
*
39+
* @param InputInterface $input
40+
*
41+
* @return array{"0": string, "1": string, "2": MigrateDirection}
42+
*/
43+
protected function readInput(InputInterface $input): array
44+
{
45+
return [
46+
$input->getArgument('group'),
47+
$input->getArgument('version'),
48+
$input->getOption('down') ? MigrateDirection::Down : MigrateDirection::Up,
49+
];
50+
}
51+
52+
protected function confirmExecuteMigration(InputInterface $input, OutputInterface $output, string $question): bool
53+
{
54+
$style = new SymfonyStyle($input, $output);
55+
56+
if ($input->isInteractive() && !$style->confirm($question, false)) {
57+
$style->error('Migration canceled.');
58+
59+
return false;
60+
}
61+
62+
return true;
63+
}
64+
65+
protected function outputMigrationResult(OutputInterface $output, MigrationResult $result): void
66+
{
67+
if ($result->state === MigrationExecutedState::Skipped) {
68+
$output->writeln(\sprintf(
69+
'Skip migration <comment>%s</comment>.',
70+
$result->metadata->class->name
71+
), OutputInterface::VERBOSITY_DEBUG);
72+
} else {
73+
$output->writeln(\sprintf(
74+
'Executed migration <comment>%s</comment> in %.2f seconds.',
75+
$result->metadata->class->name,
76+
$result->executeTime
77+
), OutputInterface::VERBOSITY_NORMAL);
78+
}
79+
}
80+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the FiveLab Migrator package
5+
*
6+
* (c) FiveLab
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code
10+
*/
11+
12+
declare(strict_types = 1);
13+
14+
namespace FiveLab\Component\Migrator\Console;
15+
16+
use FiveLab\Component\Migrator\MigratorRegistry;
17+
use Symfony\Component\Console\Attribute\AsCommand;
18+
use Symfony\Component\Console\Command\Command;
19+
use Symfony\Component\Console\Input\InputInterface;
20+
use Symfony\Component\Console\Output\OutputInterface;
21+
22+
#[AsCommand(name: 'migrations:execute', description: 'Execute specific migration by version.')]
23+
class ExecuteMigrationCommand extends Command
24+
{
25+
use CommandHelperTrait;
26+
27+
public function __construct(private readonly MigratorRegistry $registry)
28+
{
29+
parent::__construct();
30+
}
31+
32+
protected function configure(): void
33+
{
34+
$this->configureMigrateInput($this, true);
35+
}
36+
37+
protected function execute(InputInterface $input, OutputInterface $output): int
38+
{
39+
[$group, $version, $direction] = $this->readInput($input);
40+
41+
$question = \sprintf(
42+
'<comment>WARNING!</comment> You are about to execute a <comment>%s</comment> migration by version <comment>%s</comment> (<comment>%s</comment>). Are you sure you wish to continue?',
43+
$group,
44+
$version,
45+
$direction->name
46+
);
47+
48+
if (!$this->confirmExecuteMigration($input, $output, $question)) {
49+
return self::FAILURE;
50+
}
51+
52+
$migrator = $this->registry->get($group);
53+
54+
$result = $migrator->execute($direction, $version);
55+
56+
$this->outputMigrationResult($output, $result);
57+
58+
return self::SUCCESS;
59+
}
60+
}

src/Console/MigrateCommand.php

Lines changed: 9 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -13,68 +13,45 @@
1313

1414
namespace FiveLab\Component\Migrator\Console;
1515

16-
use FiveLab\Component\Migrator\MigrateDirection;
17-
use FiveLab\Component\Migrator\MigrationExecutedState;
1816
use FiveLab\Component\Migrator\MigratorRegistry;
1917
use Symfony\Component\Console\Attribute\AsCommand;
2018
use Symfony\Component\Console\Command\Command;
21-
use Symfony\Component\Console\Input\InputArgument;
2219
use Symfony\Component\Console\Input\InputInterface;
23-
use Symfony\Component\Console\Input\InputOption;
2420
use Symfony\Component\Console\Output\OutputInterface;
25-
use Symfony\Component\Console\Style\SymfonyStyle;
2621

2722
#[AsCommand(name: 'migrations:migrate', description: 'Run migrations.')]
2823
class MigrateCommand extends Command
2924
{
25+
use CommandHelperTrait;
26+
3027
public function __construct(private readonly MigratorRegistry $registry)
3128
{
32-
parent::__construct(null);
29+
parent::__construct();
3330
}
3431

3532
protected function configure(): void
3633
{
37-
$this
38-
->addArgument('group', InputArgument::REQUIRED, 'The group in which to run migrations. ')
39-
->addArgument('version', InputArgument::OPTIONAL, 'The version to run migrations.')
40-
->addOption('down', null, InputOption::VALUE_NONE, 'Down migrations.');
34+
$this->configureMigrateInput($this);
4135
}
4236

4337
protected function execute(InputInterface $input, OutputInterface $output): int
4438
{
45-
$style = new SymfonyStyle($input, $output);
46-
47-
$group = $input->getArgument('group');
48-
$toVersion = $input->getArgument('version');
49-
$direction = $input->getOption('down') ? MigrateDirection::Down : MigrateDirection::Up;
39+
[$group, $toVersion, $direction] = $this->readInput($input);
5040

51-
$question = sprintf(
52-
'<comment>WARNING!</comment> You are about to execute a migration <comment>%s</comment> (<comment>%s</comment>). Are you sure you wish to continue?',
41+
$question = \sprintf(
42+
'<comment>WARNING!</comment> You are about to run a <comment>%s</comment> migrations (<comment>%s</comment>). Are you sure you wish to continue?',
5343
$group,
5444
$direction->name
5545
);
5646

57-
if ($input->isInteractive() && !$style->confirm($question, false)) {
58-
$style->error('Migration canceled.');
59-
47+
if (!$this->confirmExecuteMigration($input, $output, $question)) {
6048
return self::FAILURE;
6149
}
6250

6351
$migrator = $this->registry->get($group);
6452

6553
foreach ($migrator->migrate($direction, $toVersion) as $result) {
66-
if ($result->state === MigrationExecutedState::Skipped) {
67-
$output->writeln(\sprintf(
68-
'Skip migration <comment>%s</comment>.',
69-
$result->metadata->class->name
70-
), OutputInterface::VERBOSITY_DEBUG);
71-
} else {
72-
$output->writeln(\sprintf(
73-
'Executed migration <comment>%s</comment> in %.2f seconds.',
74-
$result->metadata->class->name,
75-
$result->executeTime
76-
), OutputInterface::VERBOSITY_NORMAL);
77-
}
54+
$this->outputMigrationResult($output, $result);
7855
}
7956

8057
return self::SUCCESS;

src/DependencyInjection/services.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
declare(strict_types = 1);
1313

14+
use FiveLab\Component\Migrator\Console\ExecuteMigrationCommand;
1415
use FiveLab\Component\Migrator\Console\MigrateCommand;
1516
use FiveLab\Component\Migrator\MigratorRegistry;
1617
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
@@ -25,6 +26,12 @@
2526
])
2627

2728
->set('migrations.console.migrate', MigrateCommand::class)
29+
->args([
30+
service('migrations.migrator_registry'),
31+
])
32+
->tag('console.command')
33+
34+
->set('migrations.console.execute_migration', ExecuteMigrationCommand::class)
2835
->args([
2936
service('migrations.migrator_registry'),
3037
])

src/Migrator.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,21 @@ public function migrate(MigrateDirection $direction, ?string $toVersion): iterab
4545

4646
return $results;
4747
}
48+
49+
public function execute(MigrateDirection $direction, string $version): MigrationResult
50+
{
51+
$locator = $this->locator;
52+
$locator = new FilterVersionsLocator($locator, $version, '=');
53+
54+
$versions = \iterator_to_array($locator->locate($direction));
55+
56+
if (!\count($versions)) {
57+
throw new \RuntimeException(\sprintf(
58+
'The version "%s" does not exists.',
59+
$version
60+
));
61+
}
62+
63+
return $this->executor->execute($versions[0], $direction);
64+
}
4865
}

src/MigratorInterface.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,14 @@ interface MigratorInterface
2424
* @return iterable<MigrationResult>
2525
*/
2626
public function migrate(MigrateDirection $direction, ?string $toVersion): iterable;
27+
28+
/**
29+
* Execute specific migration (up/down).
30+
*
31+
* @param MigrateDirection $direction
32+
* @param string $version
33+
*
34+
* @return MigrationResult
35+
*/
36+
public function execute(MigrateDirection $direction, string $version): MigrationResult;
2737
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the FiveLab Migrator package
5+
*
6+
* (c) FiveLab
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code
10+
*/
11+
12+
declare(strict_types = 1);
13+
14+
namespace FiveLab\Component\Migrator\Tests\Functional\Console;
15+
16+
use FiveLab\Component\Migrator\Tests\Functional\MySqlTestTrait;
17+
use PHPUnit\Framework\TestCase;
18+
use Symfony\Component\Console\Command\Command;
19+
use Symfony\Component\Console\Output\OutputInterface;
20+
use Symfony\Component\Console\Output\StreamOutput;
21+
use Symfony\Component\Console\Tester\CommandTester;
22+
23+
abstract class CommandTestCase extends TestCase
24+
{
25+
protected ?Command $command = null;
26+
27+
use MySqlTestTrait {
28+
setUp as protected setUpMySql;
29+
}
30+
31+
protected function executeCommand(array $args, bool $interactive = false): CommandTester
32+
{
33+
if (!$this->command) {
34+
throw new \RuntimeException('The command not initialized. Please inject you command to $command property.');
35+
}
36+
37+
$tester = new CommandTester($this->command);
38+
39+
$tester->execute($args, [
40+
'interactive' => $interactive,
41+
]);
42+
43+
return $tester;
44+
}
45+
46+
protected function getOutputString(OutputInterface $output): string
47+
{
48+
if (!$output instanceof StreamOutput) {
49+
throw new \InvalidArgumentException(\sprintf(
50+
'Only StreamOutput supported for get output, but "%s" given.',
51+
\get_class($output)
52+
));
53+
}
54+
55+
$stream = $output->getStream();
56+
57+
\rewind($stream);
58+
59+
return \stream_get_contents($stream);
60+
}
61+
}

0 commit comments

Comments
 (0)