From 58d3575327d518aff6a4fb56f01d0fcc05983d66 Mon Sep 17 00:00:00 2001 From: Krasen Borisov Date: Tue, 14 Apr 2026 22:57:20 +0300 Subject: [PATCH] Refactoring using context runner and strategies --- config/services.php | 12 +- config/services_migrations.php | 29 +- config/services_orm.php | 9 +- psalm.xml.dist | 7 - src/Command/Doctrine/ContextRunner.php | 242 +++++++++++++ .../Database/CreateDatabaseCommand.php | 15 +- src/Command/Doctrine/DoctrineContextTrait.php | 334 ------------------ src/Command/Doctrine/Mapping/InfoCommand.php | 16 +- .../Doctrine/Migrations/CurrentCommand.php | 13 +- .../Doctrine/Migrations/DiffCommand.php | 13 +- .../Doctrine/Migrations/DumpSchemaCommand.php | 13 +- .../Doctrine/Migrations/ExecuteCommand.php | 13 +- .../Doctrine/Migrations/GenerateCommand.php | 13 +- .../Doctrine/Migrations/LatestCommand.php | 13 +- .../Doctrine/Migrations/ListCommand.php | 13 +- .../Doctrine/Migrations/MigrateCommand.php | 13 +- .../Doctrine/Migrations/RollupCommand.php | 13 +- .../Doctrine/Migrations/StatusCommand.php | 13 +- .../Migrations/SyncMetadataCommand.php | 13 +- .../Doctrine/Migrations/UpToDateCommand.php | 13 +- .../Doctrine/Migrations/VersionCommand.php | 13 +- .../Doctrine/Schema/CreateSchemaCommand.php | 16 +- .../Doctrine/Schema/ValidateSchemaCommand.php | 16 +- .../Strategy/CommandExecutionStrategy.php | 20 ++ .../Doctrine/Strategy/ConnectionStrategy.php | 74 ++++ .../Strategy/EntityManagerStrategy.php | 62 ++++ .../Doctrine/Strategy/MigrationStrategy.php | 77 ++++ src/DoctrineContextBundle.php | 17 + tests/Unit/CommandExecutionStrategyTest.php | 127 +++++++ tests/Unit/ContextRunnerTest.php | 121 +++++++ 30 files changed, 841 insertions(+), 522 deletions(-) create mode 100644 src/Command/Doctrine/ContextRunner.php delete mode 100644 src/Command/Doctrine/DoctrineContextTrait.php create mode 100644 src/Command/Doctrine/Strategy/CommandExecutionStrategy.php create mode 100644 src/Command/Doctrine/Strategy/ConnectionStrategy.php create mode 100644 src/Command/Doctrine/Strategy/EntityManagerStrategy.php create mode 100644 src/Command/Doctrine/Strategy/MigrationStrategy.php create mode 100644 tests/Unit/CommandExecutionStrategyTest.php create mode 100644 tests/Unit/ContextRunnerTest.php diff --git a/config/services.php b/config/services.php index 8636055..2b831fd 100644 --- a/config/services.php +++ b/config/services.php @@ -4,7 +4,9 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; +use Kraz\DoctrineContextBundle\Command\Doctrine\ContextRunner; use Kraz\DoctrineContextBundle\Command\Doctrine\Database\CreateDatabaseCommand; +use Kraz\DoctrineContextBundle\Command\Doctrine\Strategy\ConnectionStrategy; use Kraz\DoctrineContextBundle\Configuration\Configuration as DoctrineContextConfiguration; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -13,11 +15,19 @@ ->set('doctrine.doctrine_context.configuration', DoctrineContextConfiguration::class) ->public() + ->set('doctrine.doctrine_context.strategy.connection', ConnectionStrategy::class) + + ->set('doctrine.doctrine_context.context_runner', ContextRunner::class) + ->public() + ->args([ + service('doctrine.doctrine_context.configuration'), + ]) + ->set('doctrine.database_create_command.with_context', CreateDatabaseCommand::class) ->decorate('doctrine.database_create_command', null, 0, ContainerInterface::IGNORE_ON_INVALID_REFERENCE) ->args([ service('.inner'), - service('doctrine.doctrine_context.configuration'), + service('doctrine.doctrine_context.context_runner'), ]) ->tag('console.command', ['command' => 'doctrine:database:create']); }; diff --git a/config/services_migrations.php b/config/services_migrations.php index a6bf983..56ae854 100644 --- a/config/services_migrations.php +++ b/config/services_migrations.php @@ -19,6 +19,7 @@ use Kraz\DoctrineContextBundle\Command\Doctrine\Migrations\SyncMetadataCommand; use Kraz\DoctrineContextBundle\Command\Doctrine\Migrations\UpToDateCommand; use Kraz\DoctrineContextBundle\Command\Doctrine\Migrations\VersionCommand; +use Kraz\DoctrineContextBundle\Command\Doctrine\Strategy\MigrationStrategy; use Symfony\Component\DependencyInjection\ContainerInterface; return static function (ContainerConfigurator $container): void { @@ -29,11 +30,13 @@ ->set('doctrine.doctrine_context.dependency_factory', DependencyFactory::class) ->abstract() + ->set('doctrine.doctrine_context.strategy.migration', MigrationStrategy::class) + ->set('doctrine_migrations.current_command.with_context', CurrentCommand::class) ->decorate('doctrine_migrations.current_command', null, 0, ContainerInterface::IGNORE_ON_INVALID_REFERENCE) ->args([ service('.inner'), - service('doctrine.doctrine_context.configuration'), + service('doctrine.doctrine_context.context_runner'), ]) ->tag('console.command', ['command' => 'doctrine:migrations:current']) @@ -41,7 +44,7 @@ ->decorate('doctrine_migrations.diff_command', null, 0, ContainerInterface::IGNORE_ON_INVALID_REFERENCE) ->args([ service('.inner'), - service('doctrine.doctrine_context.configuration'), + service('doctrine.doctrine_context.context_runner'), ]) ->tag('console.command', ['command' => 'doctrine:migrations:diff']) @@ -49,7 +52,7 @@ ->decorate('doctrine_migrations.dump_schema_command', null, 0, ContainerInterface::IGNORE_ON_INVALID_REFERENCE) ->args([ service('.inner'), - service('doctrine.doctrine_context.configuration'), + service('doctrine.doctrine_context.context_runner'), ]) ->tag('console.command', ['command' => 'doctrine:migrations:dump-schema']) @@ -57,7 +60,7 @@ ->decorate('doctrine_migrations.execute_command', null, 0, ContainerInterface::IGNORE_ON_INVALID_REFERENCE) ->args([ service('.inner'), - service('doctrine.doctrine_context.configuration'), + service('doctrine.doctrine_context.context_runner'), ]) ->tag('console.command', ['command' => 'doctrine:migrations:execute']) @@ -65,7 +68,7 @@ ->decorate('doctrine_migrations.generate_command', null, 0, ContainerInterface::IGNORE_ON_INVALID_REFERENCE) ->args([ service('.inner'), - service('doctrine.doctrine_context.configuration'), + service('doctrine.doctrine_context.context_runner'), ]) ->tag('console.command', ['command' => 'doctrine:migrations:generate']) @@ -73,7 +76,7 @@ ->decorate('doctrine_migrations.latest_command', null, 0, ContainerInterface::IGNORE_ON_INVALID_REFERENCE) ->args([ service('.inner'), - service('doctrine.doctrine_context.configuration'), + service('doctrine.doctrine_context.context_runner'), ]) ->tag('console.command', ['command' => 'doctrine:migrations:latest']) @@ -81,7 +84,7 @@ ->decorate('doctrine_migrations.versions_command', null, 0, ContainerInterface::IGNORE_ON_INVALID_REFERENCE) ->args([ service('.inner'), - service('doctrine.doctrine_context.configuration'), + service('doctrine.doctrine_context.context_runner'), ]) ->tag('console.command', ['command' => 'doctrine:migrations:list']) @@ -89,7 +92,7 @@ ->decorate('doctrine_migrations.migrate_command', null, 0, ContainerInterface::IGNORE_ON_INVALID_REFERENCE) ->args([ service('.inner'), - service('doctrine.doctrine_context.configuration'), + service('doctrine.doctrine_context.context_runner'), ]) ->tag('console.command', ['command' => 'doctrine:migrations:migrate']) @@ -97,7 +100,7 @@ ->decorate('doctrine_migrations.rollup_command', null, 0, ContainerInterface::IGNORE_ON_INVALID_REFERENCE) ->args([ service('.inner'), - service('doctrine.doctrine_context.configuration'), + service('doctrine.doctrine_context.context_runner'), ]) ->tag('console.command', ['command' => 'doctrine:migrations:rollup']) @@ -105,7 +108,7 @@ ->decorate('doctrine_migrations.status_command', null, 0, ContainerInterface::IGNORE_ON_INVALID_REFERENCE) ->args([ service('.inner'), - service('doctrine.doctrine_context.configuration'), + service('doctrine.doctrine_context.context_runner'), ]) ->tag('console.command', ['command' => 'doctrine:migrations:status']) @@ -113,7 +116,7 @@ ->decorate('doctrine_migrations.sync_metadata_command', null, 0, ContainerInterface::IGNORE_ON_INVALID_REFERENCE) ->args([ service('.inner'), - service('doctrine.doctrine_context.configuration'), + service('doctrine.doctrine_context.context_runner'), ]) ->tag('console.command', ['command' => 'doctrine:migrations:sync-metadata-storage']) @@ -121,7 +124,7 @@ ->decorate('doctrine_migrations.up_to_date_command', null, 0, ContainerInterface::IGNORE_ON_INVALID_REFERENCE) ->args([ service('.inner'), - service('doctrine.doctrine_context.configuration'), + service('doctrine.doctrine_context.context_runner'), ]) ->tag('console.command', ['command' => 'doctrine:migrations:up-to-date']) @@ -129,7 +132,7 @@ ->decorate('doctrine_migrations.version_command', null, 0, ContainerInterface::IGNORE_ON_INVALID_REFERENCE) ->args([ service('.inner'), - service('doctrine.doctrine_context.configuration'), + service('doctrine.doctrine_context.context_runner'), ]) ->tag('console.command', ['command' => 'doctrine:migrations:version']); }; diff --git a/config/services_orm.php b/config/services_orm.php index dd221b0..973754d 100644 --- a/config/services_orm.php +++ b/config/services_orm.php @@ -7,15 +7,18 @@ use Kraz\DoctrineContextBundle\Command\Doctrine\Mapping\InfoCommand; use Kraz\DoctrineContextBundle\Command\Doctrine\Schema\CreateSchemaCommand; use Kraz\DoctrineContextBundle\Command\Doctrine\Schema\ValidateSchemaCommand; +use Kraz\DoctrineContextBundle\Command\Doctrine\Strategy\EntityManagerStrategy; use Symfony\Component\DependencyInjection\ContainerInterface; return static function (ContainerConfigurator $container): void { $container->services() + ->set('doctrine.doctrine_context.strategy.entity_manager', EntityManagerStrategy::class) + ->set('doctrine.mapping_info_command.with_context', InfoCommand::class) ->decorate('doctrine.mapping_info_command', null, 0, ContainerInterface::IGNORE_ON_INVALID_REFERENCE) ->args([ service('.inner'), - service('doctrine.doctrine_context.configuration'), + service('doctrine.doctrine_context.context_runner'), ]) ->tag('console.command', ['command' => 'doctrine:mapping:info']) @@ -23,7 +26,7 @@ ->decorate('doctrine.schema_create_command', null, 0, ContainerInterface::IGNORE_ON_INVALID_REFERENCE) ->args([ service('.inner'), - service('doctrine.doctrine_context.configuration'), + service('doctrine.doctrine_context.context_runner'), ]) ->tag('console.command', ['command' => 'doctrine:schema:create']) @@ -31,7 +34,7 @@ ->decorate('doctrine.schema_validate_command', null, 0, ContainerInterface::IGNORE_ON_INVALID_REFERENCE) ->args([ service('.inner'), - service('doctrine.doctrine_context.configuration'), + service('doctrine.doctrine_context.context_runner'), ]) ->tag('console.command', ['command' => 'doctrine:schema:validate']); }; diff --git a/psalm.xml.dist b/psalm.xml.dist index 9f95f42..6f7e884 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -37,12 +37,5 @@ - - - - - - - diff --git a/src/Command/Doctrine/ContextRunner.php b/src/Command/Doctrine/ContextRunner.php new file mode 100644 index 0000000..f316b6e --- /dev/null +++ b/src/Command/Doctrine/ContextRunner.php @@ -0,0 +1,242 @@ + */ + private array $strategies = []; + + public function __construct(private readonly Configuration $configuration) + { + } + + public function addStrategy(CommandExecutionStrategy $strategy): void + { + $this->strategies[] = $strategy; + } + + public function getConfiguration(): Configuration + { + return $this->configuration; + } + + public function configure(Command $wrapper, Command $innerCommand): void + { + $name = $wrapper->getName() ?? $innerCommand->getName(); + if ($name !== null) { + $wrapper->setName($name); + } + + $wrapper->setDescription(($wrapper->getDescription() ?: $innerCommand->getDescription()) . ' [Doctrine Context]'); + $wrapper->setAliases($wrapper->getAliases() ?: $innerCommand->getAliases()); + $wrapper->setHelp($wrapper->getHelp() ?: $innerCommand->getHelp()); + $nativeDefinition = $innerCommand->getNativeDefinition(); + $wrapper->setDefinition($nativeDefinition); + $wrapper->addOption('ctx-isolation', null, InputOption::VALUE_NONE, 'Continue with the next context, if the current one fails.'); + $wrapper->addOption('ctx-all', null, InputOption::VALUE_NONE, 'Run the command over all registered contexts (when explicit_context: true).'); + + foreach ($this->strategies as $strategy) { + if ($strategy->supports($innerCommand)) { + $strategy->configure($wrapper, $innerCommand); + break; + } + } + } + + public function run(Command $wrapper, Command $innerCommand, InputInterface $input, OutputInterface $output): int + { + foreach ($this->strategies as $strategy) { + if ($strategy->supports($innerCommand)) { + return $strategy->execute($wrapper, $innerCommand, $this, $this->configuration, $input, $output); + } + } + + throw new InvalidArgumentException(sprintf('Unsupported CLI command "%s"', $innerCommand::class)); + } + + /** @param array $list */ + public function walkDoctrineContexts(callable $callback, array $list, InputInterface $input, OutputInterface $output): int + { + $result = Command::SUCCESS; + $ui = new SymfonyStyle($input, $output)->getErrorStyle(); + $contextIsolation = $input->getOption('ctx-isolation'); + $total = count($list); + while (count($list) > 0) { + $contextName = array_key_first($list); + $dependencyFactory = array_shift($list); + if ($total > 1) { + $ui->section(sprintf('%s: %s', $this->configuration->isEntityManager($contextName) ? 'Entity Manager' : 'Connection', $contextName)); + } + + try { + $cmdResult = $callback($input, $output, $contextName, $dependencyFactory); + } catch (Throwable $e) { + $ui->error($e->getMessage()); + $cmdResult = Command::FAILURE; + } + + if ($total > 1) { + $ui->newLine(); + } + + if ($cmdResult !== Command::SUCCESS) { + $result = $cmdResult; + if ($input->isInteractive() && count($list) > 0 && $ui->confirm('Do you want to proceed with the rest of the doctrine contexts?')) { + continue; + } + + if (! $contextIsolation) { + break; + } + } + } + + return $result; + } + + /** + * @param (callable(DependencyFactory|string): bool)|null $filter + * @param string[] $targetEntityManagers + * @param string[] $targetConnectionNames + * + * @return array + */ + public function filterDoctrineContexts(callable|null $filter = null, array $targetEntityManagers = [], array $targetConnectionNames = []): array + { + $targetEntityManagers = array_values(array_filter(array_map('trim', $targetEntityManagers))); + $targetConnectionNames = array_values(array_filter(array_map('trim', $targetConnectionNames))); + + if (count($targetEntityManagers) > 0 && count($targetConnectionNames) > 0) { + throw new InvalidArgumentException('You can specify only one of the --em/--ems and --conn/--conns options.'); + } + + $targetNames = count($targetEntityManagers) > 0 ? $targetEntityManagers : $targetConnectionNames; + $isEntityManagerMode = count($targetEntityManagers) > 0; + + // Build the full context pool (dependency factories + context-only entries) + $allContexts = $this->configuration->getDependencyFactories(); + foreach ($this->configuration->getContextNames() as $name) { + if (! isset($allContexts[$name])) { + $allContexts[$name] = null; + } + } + + // Select the requested subset or take all + if (count($targetNames) > 0) { + $list = []; + foreach ($targetNames as $name) { + if (array_key_exists($name, $allContexts)) { + $list[$name] = $allContexts[$name]; + } + } + } else { + $list = $allContexts; + } + + // Apply callable filter + if ($filter !== null) { + $filteredList = []; + foreach ($list as $contextName => $dependencyFactory) { + if (! call_user_func($filter, $dependencyFactory ?? $contextName)) { + continue; + } + + $filteredList[$contextName] = $dependencyFactory; + } + + $list = $filteredList; + } + + // Throw for invalid/not-found targets + if (count($list) === 0 && count($targetNames) > 0) { + if ($isEntityManagerMode) { + throw new InvalidArgumentException(count($targetNames) === 1 + ? sprintf('Unknown doctrine entity manager "%s" or it\'s not registered as doctrine context.', $targetNames[0]) + : sprintf('Unknown doctrine entity managers "%s" or they are not registered as doctrine contexts.', implode('", "', $targetNames))); + } + + throw new InvalidArgumentException(count($targetNames) === 1 + ? sprintf('Unknown doctrine connection "%s" or it\'s not registered as doctrine context.', $targetNames[0]) + : sprintf('Unknown doctrine connections "%s" or they are not registered as doctrine contexts.', implode('", "', $targetNames))); + } + + ksort($list); + + return $list; + } + + /** @return string[] */ + public function resolveArrayOption(InputInterface $input, string $name): array + { + if (! $input->hasOption($name)) { + return []; + } + + $values = []; + foreach ((array) $input->getOption($name) as $value) { + foreach (explode(',', $value) as $part) { + $part = trim($part); + if ($part !== '') { + $values[] = $part; + } + } + } + + return array_values(array_unique($values)); + } + + /** @param array $override */ + public function createNewInput(Command $command, InputInterface $input, array $override): InputInterface + { + $definition = $command->getNativeDefinition(); + $parameters = []; + foreach ($definition->getArguments() as $argument) { + if ($input->hasArgument($argument->getName())) { + $parameters[$argument->getName()] = $input->getArgument($argument->getName()); + } + } + + foreach ($definition->getOptions() as $option) { + if ($input->hasParameterOption('--' . $option->getName())) { + $parameters['--' . $option->getName()] = $input->getOption($option->getName()); + } + } + + $parameters = array_replace($parameters, $override); + $newInput = new ArrayInput($parameters); + $newInput->setInteractive($input->isInteractive()); + + return $newInput; + } +} diff --git a/src/Command/Doctrine/Database/CreateDatabaseCommand.php b/src/Command/Doctrine/Database/CreateDatabaseCommand.php index 440e65d..432f8ec 100644 --- a/src/Command/Doctrine/Database/CreateDatabaseCommand.php +++ b/src/Command/Doctrine/Database/CreateDatabaseCommand.php @@ -5,37 +5,30 @@ namespace Kraz\DoctrineContextBundle\Command\Doctrine\Database; use Doctrine\Bundle\DoctrineBundle\Command\CreateDatabaseDoctrineCommand; -use Kraz\DoctrineContextBundle\Command\Doctrine\DoctrineContextTrait; -use Kraz\DoctrineContextBundle\Configuration\Configuration; +use Kraz\DoctrineContextBundle\Command\Doctrine\ContextRunner; use Override; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class CreateDatabaseCommand extends Command { - use DoctrineContextTrait; - public function __construct( private readonly CreateDatabaseDoctrineCommand $command, - Configuration $configuration, + private readonly ContextRunner $contextRunner, ) { - $this->configuration = $configuration; - parent::__construct($this->command->getName(), $this->command->getCode()); } #[Override] protected function configure(): void { - $this->configureAs($this->command); - $this->addOption('conn', null, InputOption::VALUE_OPTIONAL, 'The name of the connection to use (alias for --connection).'); + $this->contextRunner->configure($this, $this->command); } #[Override] protected function execute(InputInterface $input, OutputInterface $output): int { - return $this->runAs($this->command, $input, $output); + return $this->contextRunner->run($this, $this->command, $input, $output); } } diff --git a/src/Command/Doctrine/DoctrineContextTrait.php b/src/Command/Doctrine/DoctrineContextTrait.php deleted file mode 100644 index b47909b..0000000 --- a/src/Command/Doctrine/DoctrineContextTrait.php +++ /dev/null @@ -1,334 +0,0 @@ -getName() ?? $command->getName(); - if ($name !== null) { - $this->setName($name); - } - - $this->setDescription(($this->getDescription() ?: $command->getDescription()) . ' [Doctrine Context]'); - $this->setAliases($this->getAliases() ?: $command->getAliases()); - $this->setHelp($this->getHelp() ?: $command->getHelp()); - $nativeDefinition = $command->getNativeDefinition(); - $this->setDefinition($nativeDefinition); - $this->addOption('ctx-isolation', null, InputOption::VALUE_NONE, 'Continue with the next context, if the current one fails.'); - $this->addOption('ctx-all', null, InputOption::VALUE_NONE, 'Run the command over all registered contexts (when explicit_context: true).'); - - if ($nativeDefinition->hasOption('em')) { - $this->addOption('ems', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'The names of the entity managers to use (multi-value alias for --em).'); - } - - if ($nativeDefinition->hasOption('conn')) { - $this->addOption('conns', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'The names of the connections to use (multi-value alias for --conn).'); - } - - if ($nativeDefinition->hasOption('connection')) { - $this->addOption('connections', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'The names of the connections to use (multi-value alias for --connection).'); - $this->addOption('conns', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'The names of the connections to use (multi-value alias for --conn).'); - } - } - - private function runAs(Command $command, InputInterface $input, OutputInterface $output): int - { - assert($this instanceof Command); - - $ctxAll = (bool) $input->getOption('ctx-all'); - - if ($command instanceof AbstractEntityManagerCommand) { - $emOption = trim((string) $input->getOption('em')); - $emsOption = $this->resolveArrayOption($input, 'ems'); - - if ($emOption !== '' && count($emsOption) > 0) { - throw new InvalidArgumentException('You can specify only one of the --em and --ems options.'); - } - - $targetEntityManagers = $emOption !== '' ? [$emOption] : $emsOption; - - if ($this->configuration->isExplicitContext() && count($targetEntityManagers) === 0 && ! $ctxAll) { - throw new InvalidArgumentException('Explicit context is required. Specify a context via --em, --ems, or use --ctx-all to run over all contexts.'); - } - - $list = $this->filterDoctrineContexts(fn (DependencyFactory|string $ctx): bool => is_string($ctx) ? $this->configuration->isEntityManager($ctx) : $ctx->hasEntityManager(), $targetEntityManagers); - - return $this->walkDoctrineContexts(function (InputInterface $input, OutputInterface $output, string $em) use ($command) { - $command->setDefinition($this->getNativeDefinition()); - $command->setApplication($this->getApplication()); - $newInput = $this->createNewInput($command, $input, ['--em' => $em]); - - return $command->run($newInput, $output); - }, $list, $input, $output); - } - - if ($command instanceof AbstractDoctrineMigrationCommand) { - $emOption = trim((string) $input->getOption('em')); - $connOption = trim((string) $input->getOption('conn')); - $emsOption = $this->resolveArrayOption($input, 'ems'); - $connsOption = $this->resolveArrayOption($input, 'conns'); - - if ($emOption !== '' && count($emsOption) > 0) { - throw new InvalidArgumentException('You can specify only one of the --em and --ems options.'); - } - - if ($connOption !== '' && count($connsOption) > 0) { - throw new InvalidArgumentException('You can specify only one of the --conn and --conns options.'); - } - - $targetEntityManagers = $emOption !== '' ? [$emOption] : $emsOption; - $targetConnectionNames = $connOption !== '' ? [$connOption] : $connsOption; - - if ($this->configuration->isExplicitContext() && count($targetEntityManagers) === 0 && count($targetConnectionNames) === 0 && ! $ctxAll) { - throw new InvalidArgumentException('Explicit context is required. Specify a context via --em, --ems, --conn, --conns, or use --ctx-all to run over all contexts.'); - } - - $list = $this->filterDoctrineContexts(null, $targetEntityManagers, $targetConnectionNames); - - return $this->walkDoctrineContexts(function (InputInterface $input, OutputInterface $output, string $contextName, DependencyFactory $dependencyFactory) use ($command) { - $command = new ReflectionClass($command)->newInstance($dependencyFactory); - $command->setDefinition($this->getNativeDefinition()); - $command->setApplication($this->getApplication()); - - return $command->run($input, $output); - }, $list, $input, $output); - } - - if ($command->getNativeDefinition()->hasOption('connection')) { - $connectionOption = trim((string) $input->getOption('connection')); - $connOption = trim((string) $input->getOption('conn')); - $connectionsOption = $this->resolveArrayOption($input, 'connections'); - $connsOption = $this->resolveArrayOption($input, 'conns'); - - if ($connectionOption !== '' && $connOption !== '') { - throw new InvalidArgumentException('You can specify only one of the --connection and --conn options.'); - } - - if (count($connectionsOption) > 0 && count($connsOption) > 0) { - throw new InvalidArgumentException('You can specify only one of the --connections and --conns options.'); - } - - $singleTarget = $connectionOption ?: ($connOption ?: null); - $arrayTargets = count($connectionsOption) > 0 ? $connectionsOption : $connsOption; - - if ($singleTarget !== null && count($arrayTargets) > 0) { - throw new InvalidArgumentException('You can specify only one of the --connection/--conn and --connections/--conns options.'); - } - - $targetConnectionNames = $singleTarget !== null ? [$singleTarget] : $arrayTargets; - - if ($this->configuration->isExplicitContext() && count($targetConnectionNames) === 0 && ! $ctxAll) { - throw new InvalidArgumentException('Explicit context is required. Specify a context via --connection, --connections, --conn, --conns, or use --ctx-all to run over all contexts.'); - } - - $list = $this->filterDoctrineContexts(null, [], $targetConnectionNames); - - return $this->walkDoctrineContexts(function (InputInterface $input, OutputInterface $output, string $contextName) use ($command) { - $command->setDefinition($this->getNativeDefinition()); - $command->setApplication($this->getApplication()); - $newInput = $this->createNewInput($command, $input, ['--connection' => $contextName]); - - return $command->run($newInput, $output); - }, $list, $input, $output); - } - - throw new InvalidArgumentException(sprintf('Unsupported CLI command "%s"', $command::class)); - } - - /** @param array $list */ - private function walkDoctrineContexts(callable $callback, array $list, InputInterface $input, OutputInterface $output): int - { - $result = Command::SUCCESS; - $ui = new SymfonyStyle($input, $output)->getErrorStyle(); - $contextIsolation = $input->getOption('ctx-isolation'); - $total = count($list); - while (count($list) > 0) { - $contextName = array_key_first($list); - $dependencyFactory = array_shift($list); - if ($total > 1) { - $ui->section(sprintf('%s: %s', $this->configuration->isEntityManager($contextName) ? 'Entity Manager' : 'Connection', $contextName)); - } - - try { - $cmdResult = $callback($input, $output, $contextName, $dependencyFactory); - } catch (Throwable $e) { - $ui->error($e->getMessage()); - $cmdResult = Command::FAILURE; - } - - if ($total > 1) { - $ui->newLine(); - } - - if ($cmdResult !== Command::SUCCESS) { - $result = $cmdResult; - if ($input->isInteractive() && count($list) > 0 && $ui->confirm('Do you want to proceed with the rest of the doctrine contexts?')) { - continue; - } - - if (! $contextIsolation) { - break; - } - } - } - - return $result; - } - - /** - * @param callable(DependencyFactory|string): bool|null $filter - * @param string[] $targetEntityManagers - * @param string[] $targetConnectionNames - * - * @return array - */ - private function filterDoctrineContexts(callable|null $filter = null, array $targetEntityManagers = [], array $targetConnectionNames = []): array - { - $targetEntityManagers = array_values(array_filter(array_map('trim', $targetEntityManagers))); - $targetConnectionNames = array_values(array_filter(array_map('trim', $targetConnectionNames))); - - if (count($targetEntityManagers) > 0 && count($targetConnectionNames) > 0) { - throw new InvalidArgumentException('You can specify only one of the --em/--ems and --conn/--conns options.'); - } - - $targetNames = count($targetEntityManagers) > 0 ? $targetEntityManagers : $targetConnectionNames; - $isEntityManagerMode = count($targetEntityManagers) > 0; - - // Build the full context pool (dependency factories + context-only entries) - $allContexts = $this->configuration->getDependencyFactories(); - foreach ($this->configuration->getContextNames() as $name) { - if (! isset($allContexts[$name])) { - $allContexts[$name] = null; - } - } - - // Select the requested subset or take all - if (count($targetNames) > 0) { - $list = []; - foreach ($targetNames as $name) { - if (array_key_exists($name, $allContexts)) { - $list[$name] = $allContexts[$name]; - } - } - } else { - $list = $allContexts; - } - - // Apply callable filter - if ($filter !== null) { - $filteredList = []; - foreach ($list as $contextName => $dependencyFactory) { - if (! call_user_func($filter, $dependencyFactory ?? $contextName)) { - continue; - } - - $filteredList[$contextName] = $dependencyFactory; - } - - $list = $filteredList; - } - - // Throw for invalid/not-found targets - if (count($list) === 0 && count($targetNames) > 0) { - if ($isEntityManagerMode) { - throw new InvalidArgumentException(count($targetNames) === 1 - ? sprintf('Unknown doctrine entity manager "%s" or it\'s not registered as doctrine context.', $targetNames[0]) - : sprintf('Unknown doctrine entity managers "%s" or they are not registered as doctrine contexts.', implode('", "', $targetNames))); - } - - throw new InvalidArgumentException(count($targetNames) === 1 - ? sprintf('Unknown doctrine connection "%s" or it\'s not registered as doctrine context.', $targetNames[0]) - : sprintf('Unknown doctrine connections "%s" or they are not registered as doctrine contexts.', implode('", "', $targetNames))); - } - - ksort($list); - - return $list; - } - - /** @return string[] */ - private function resolveArrayOption(InputInterface $input, string $name): array - { - if (! $input->hasOption($name)) { - return []; - } - - $values = []; - foreach ((array) $input->getOption($name) as $value) { - foreach (explode(',', $value) as $part) { - $part = trim($part); - if ($part !== '') { - $values[] = $part; - } - } - } - - return array_values(array_unique($values)); - } - - /** @param array $override */ - private function createNewInput(Command $command, InputInterface $input, array $override): InputInterface - { - $definition = $command->getNativeDefinition(); - $parameters = []; - foreach ($definition->getArguments() as $argument) { - if ($input->hasArgument($argument->getName())) { - $parameters[$argument->getName()] = $input->getArgument($argument->getName()); - } - } - - foreach ($definition->getOptions() as $option) { - if ($input->hasParameterOption('--' . $option->getName())) { - $parameters['--' . $option->getName()] = $input->getOption($option->getName()); - } - } - - $parameters = array_replace($parameters, $override); - $newInput = new ArrayInput($parameters); - $newInput->setInteractive($input->isInteractive()); - - return $newInput; - } -} diff --git a/src/Command/Doctrine/Mapping/InfoCommand.php b/src/Command/Doctrine/Mapping/InfoCommand.php index ed9a9ca..d3842c2 100644 --- a/src/Command/Doctrine/Mapping/InfoCommand.php +++ b/src/Command/Doctrine/Mapping/InfoCommand.php @@ -4,9 +4,7 @@ namespace Kraz\DoctrineContextBundle\Command\Doctrine\Mapping; -use Doctrine\ORM\Tools\Console\EntityManagerProvider; -use Kraz\DoctrineContextBundle\Command\Doctrine\DoctrineContextTrait; -use Kraz\DoctrineContextBundle\Configuration\Configuration; +use Kraz\DoctrineContextBundle\Command\Doctrine\ContextRunner; use Override; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -14,28 +12,22 @@ class InfoCommand extends Command { - use DoctrineContextTrait; - public function __construct( private readonly \Doctrine\ORM\Tools\Console\Command\InfoCommand $command, - Configuration $configuration, - EntityManagerProvider|null $entityManagerProvider = null, + private readonly ContextRunner $contextRunner, ) { - $this->configuration = $configuration; - $this->entityManagerProvider = $entityManagerProvider; - parent::__construct($this->command->getName(), $this->command->getCode()); } #[Override] protected function configure(): void { - $this->configureAs($this->command); + $this->contextRunner->configure($this, $this->command); } #[Override] protected function execute(InputInterface $input, OutputInterface $output): int { - return $this->runAs($this->command, $input, $output); + return $this->contextRunner->run($this, $this->command, $input, $output); } } diff --git a/src/Command/Doctrine/Migrations/CurrentCommand.php b/src/Command/Doctrine/Migrations/CurrentCommand.php index 5c87b84..a00be76 100644 --- a/src/Command/Doctrine/Migrations/CurrentCommand.php +++ b/src/Command/Doctrine/Migrations/CurrentCommand.php @@ -4,8 +4,7 @@ namespace Kraz\DoctrineContextBundle\Command\Doctrine\Migrations; -use Kraz\DoctrineContextBundle\Command\Doctrine\DoctrineContextTrait; -use Kraz\DoctrineContextBundle\Configuration\Configuration; +use Kraz\DoctrineContextBundle\Command\Doctrine\ContextRunner; use Override; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -13,26 +12,22 @@ class CurrentCommand extends Command { - use DoctrineContextTrait; - public function __construct( private readonly \Doctrine\Migrations\Tools\Console\Command\CurrentCommand $command, - Configuration $configuration, + private readonly ContextRunner $contextRunner, ) { - $this->configuration = $configuration; - parent::__construct($this->command->getName(), $this->command->getCode()); } #[Override] protected function configure(): void { - $this->configureAs($this->command); + $this->contextRunner->configure($this, $this->command); } #[Override] protected function execute(InputInterface $input, OutputInterface $output): int { - return $this->runAs($this->command, $input, $output); + return $this->contextRunner->run($this, $this->command, $input, $output); } } diff --git a/src/Command/Doctrine/Migrations/DiffCommand.php b/src/Command/Doctrine/Migrations/DiffCommand.php index 863b0b1..3f0f8ac 100644 --- a/src/Command/Doctrine/Migrations/DiffCommand.php +++ b/src/Command/Doctrine/Migrations/DiffCommand.php @@ -4,8 +4,7 @@ namespace Kraz\DoctrineContextBundle\Command\Doctrine\Migrations; -use Kraz\DoctrineContextBundle\Command\Doctrine\DoctrineContextTrait; -use Kraz\DoctrineContextBundle\Configuration\Configuration; +use Kraz\DoctrineContextBundle\Command\Doctrine\ContextRunner; use Override; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -13,26 +12,22 @@ class DiffCommand extends Command { - use DoctrineContextTrait; - public function __construct( private readonly \Doctrine\Migrations\Tools\Console\Command\DiffCommand $command, - Configuration $configuration, + private readonly ContextRunner $contextRunner, ) { - $this->configuration = $configuration; - parent::__construct($this->command->getName(), $this->command->getCode()); } #[Override] protected function configure(): void { - $this->configureAs($this->command); + $this->contextRunner->configure($this, $this->command); } #[Override] protected function execute(InputInterface $input, OutputInterface $output): int { - return $this->runAs($this->command, $input, $output); + return $this->contextRunner->run($this, $this->command, $input, $output); } } diff --git a/src/Command/Doctrine/Migrations/DumpSchemaCommand.php b/src/Command/Doctrine/Migrations/DumpSchemaCommand.php index c8d7ed6..aaddc83 100644 --- a/src/Command/Doctrine/Migrations/DumpSchemaCommand.php +++ b/src/Command/Doctrine/Migrations/DumpSchemaCommand.php @@ -4,8 +4,7 @@ namespace Kraz\DoctrineContextBundle\Command\Doctrine\Migrations; -use Kraz\DoctrineContextBundle\Command\Doctrine\DoctrineContextTrait; -use Kraz\DoctrineContextBundle\Configuration\Configuration; +use Kraz\DoctrineContextBundle\Command\Doctrine\ContextRunner; use Override; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -13,26 +12,22 @@ class DumpSchemaCommand extends Command { - use DoctrineContextTrait; - public function __construct( private readonly \Doctrine\Migrations\Tools\Console\Command\DumpSchemaCommand $command, - Configuration $configuration, + private readonly ContextRunner $contextRunner, ) { - $this->configuration = $configuration; - parent::__construct($this->command->getName(), $this->command->getCode()); } #[Override] protected function configure(): void { - $this->configureAs($this->command); + $this->contextRunner->configure($this, $this->command); } #[Override] protected function execute(InputInterface $input, OutputInterface $output): int { - return $this->runAs($this->command, $input, $output); + return $this->contextRunner->run($this, $this->command, $input, $output); } } diff --git a/src/Command/Doctrine/Migrations/ExecuteCommand.php b/src/Command/Doctrine/Migrations/ExecuteCommand.php index 9299503..3a2a57e 100644 --- a/src/Command/Doctrine/Migrations/ExecuteCommand.php +++ b/src/Command/Doctrine/Migrations/ExecuteCommand.php @@ -4,8 +4,7 @@ namespace Kraz\DoctrineContextBundle\Command\Doctrine\Migrations; -use Kraz\DoctrineContextBundle\Command\Doctrine\DoctrineContextTrait; -use Kraz\DoctrineContextBundle\Configuration\Configuration; +use Kraz\DoctrineContextBundle\Command\Doctrine\ContextRunner; use Override; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -13,26 +12,22 @@ class ExecuteCommand extends Command { - use DoctrineContextTrait; - public function __construct( private readonly \Doctrine\Migrations\Tools\Console\Command\ExecuteCommand $command, - Configuration $configuration, + private readonly ContextRunner $contextRunner, ) { - $this->configuration = $configuration; - parent::__construct($this->command->getName(), $this->command->getCode()); } #[Override] protected function configure(): void { - $this->configureAs($this->command); + $this->contextRunner->configure($this, $this->command); } #[Override] protected function execute(InputInterface $input, OutputInterface $output): int { - return $this->runAs($this->command, $input, $output); + return $this->contextRunner->run($this, $this->command, $input, $output); } } diff --git a/src/Command/Doctrine/Migrations/GenerateCommand.php b/src/Command/Doctrine/Migrations/GenerateCommand.php index 09a7ec8..a68fc9a 100644 --- a/src/Command/Doctrine/Migrations/GenerateCommand.php +++ b/src/Command/Doctrine/Migrations/GenerateCommand.php @@ -4,8 +4,7 @@ namespace Kraz\DoctrineContextBundle\Command\Doctrine\Migrations; -use Kraz\DoctrineContextBundle\Command\Doctrine\DoctrineContextTrait; -use Kraz\DoctrineContextBundle\Configuration\Configuration; +use Kraz\DoctrineContextBundle\Command\Doctrine\ContextRunner; use Override; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -13,26 +12,22 @@ class GenerateCommand extends Command { - use DoctrineContextTrait; - public function __construct( private readonly \Doctrine\Migrations\Tools\Console\Command\GenerateCommand $command, - Configuration $configuration, + private readonly ContextRunner $contextRunner, ) { - $this->configuration = $configuration; - parent::__construct($this->command->getName(), $this->command->getCode()); } #[Override] protected function configure(): void { - $this->configureAs($this->command); + $this->contextRunner->configure($this, $this->command); } #[Override] protected function execute(InputInterface $input, OutputInterface $output): int { - return $this->runAs($this->command, $input, $output); + return $this->contextRunner->run($this, $this->command, $input, $output); } } diff --git a/src/Command/Doctrine/Migrations/LatestCommand.php b/src/Command/Doctrine/Migrations/LatestCommand.php index 24cdfff..6c11c2e 100644 --- a/src/Command/Doctrine/Migrations/LatestCommand.php +++ b/src/Command/Doctrine/Migrations/LatestCommand.php @@ -4,8 +4,7 @@ namespace Kraz\DoctrineContextBundle\Command\Doctrine\Migrations; -use Kraz\DoctrineContextBundle\Command\Doctrine\DoctrineContextTrait; -use Kraz\DoctrineContextBundle\Configuration\Configuration; +use Kraz\DoctrineContextBundle\Command\Doctrine\ContextRunner; use Override; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -13,26 +12,22 @@ class LatestCommand extends Command { - use DoctrineContextTrait; - public function __construct( private readonly \Doctrine\Migrations\Tools\Console\Command\LatestCommand $command, - Configuration $configuration, + private readonly ContextRunner $contextRunner, ) { - $this->configuration = $configuration; - parent::__construct($this->command->getName(), $this->command->getCode()); } #[Override] protected function configure(): void { - $this->configureAs($this->command); + $this->contextRunner->configure($this, $this->command); } #[Override] protected function execute(InputInterface $input, OutputInterface $output): int { - return $this->runAs($this->command, $input, $output); + return $this->contextRunner->run($this, $this->command, $input, $output); } } diff --git a/src/Command/Doctrine/Migrations/ListCommand.php b/src/Command/Doctrine/Migrations/ListCommand.php index 60be8cd..1a1936e 100644 --- a/src/Command/Doctrine/Migrations/ListCommand.php +++ b/src/Command/Doctrine/Migrations/ListCommand.php @@ -4,8 +4,7 @@ namespace Kraz\DoctrineContextBundle\Command\Doctrine\Migrations; -use Kraz\DoctrineContextBundle\Command\Doctrine\DoctrineContextTrait; -use Kraz\DoctrineContextBundle\Configuration\Configuration; +use Kraz\DoctrineContextBundle\Command\Doctrine\ContextRunner; use Override; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -13,26 +12,22 @@ class ListCommand extends Command { - use DoctrineContextTrait; - public function __construct( private readonly \Doctrine\Migrations\Tools\Console\Command\ListCommand $command, - Configuration $configuration, + private readonly ContextRunner $contextRunner, ) { - $this->configuration = $configuration; - parent::__construct($this->command->getName(), $this->command->getCode()); } #[Override] protected function configure(): void { - $this->configureAs($this->command); + $this->contextRunner->configure($this, $this->command); } #[Override] protected function execute(InputInterface $input, OutputInterface $output): int { - return $this->runAs($this->command, $input, $output); + return $this->contextRunner->run($this, $this->command, $input, $output); } } diff --git a/src/Command/Doctrine/Migrations/MigrateCommand.php b/src/Command/Doctrine/Migrations/MigrateCommand.php index 571bc65..5858fbf 100644 --- a/src/Command/Doctrine/Migrations/MigrateCommand.php +++ b/src/Command/Doctrine/Migrations/MigrateCommand.php @@ -4,8 +4,7 @@ namespace Kraz\DoctrineContextBundle\Command\Doctrine\Migrations; -use Kraz\DoctrineContextBundle\Command\Doctrine\DoctrineContextTrait; -use Kraz\DoctrineContextBundle\Configuration\Configuration; +use Kraz\DoctrineContextBundle\Command\Doctrine\ContextRunner; use Override; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -13,26 +12,22 @@ class MigrateCommand extends Command { - use DoctrineContextTrait; - public function __construct( private readonly \Doctrine\Migrations\Tools\Console\Command\MigrateCommand $command, - Configuration $configuration, + private readonly ContextRunner $contextRunner, ) { - $this->configuration = $configuration; - parent::__construct($this->command->getName(), $this->command->getCode()); } #[Override] protected function configure(): void { - $this->configureAs($this->command); + $this->contextRunner->configure($this, $this->command); } #[Override] protected function execute(InputInterface $input, OutputInterface $output): int { - return $this->runAs($this->command, $input, $output); + return $this->contextRunner->run($this, $this->command, $input, $output); } } diff --git a/src/Command/Doctrine/Migrations/RollupCommand.php b/src/Command/Doctrine/Migrations/RollupCommand.php index 03d6626..82dfe82 100644 --- a/src/Command/Doctrine/Migrations/RollupCommand.php +++ b/src/Command/Doctrine/Migrations/RollupCommand.php @@ -4,8 +4,7 @@ namespace Kraz\DoctrineContextBundle\Command\Doctrine\Migrations; -use Kraz\DoctrineContextBundle\Command\Doctrine\DoctrineContextTrait; -use Kraz\DoctrineContextBundle\Configuration\Configuration; +use Kraz\DoctrineContextBundle\Command\Doctrine\ContextRunner; use Override; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -13,26 +12,22 @@ class RollupCommand extends Command { - use DoctrineContextTrait; - public function __construct( private readonly \Doctrine\Migrations\Tools\Console\Command\RollupCommand $command, - Configuration $configuration, + private readonly ContextRunner $contextRunner, ) { - $this->configuration = $configuration; - parent::__construct($this->command->getName(), $this->command->getCode()); } #[Override] protected function configure(): void { - $this->configureAs($this->command); + $this->contextRunner->configure($this, $this->command); } #[Override] protected function execute(InputInterface $input, OutputInterface $output): int { - return $this->runAs($this->command, $input, $output); + return $this->contextRunner->run($this, $this->command, $input, $output); } } diff --git a/src/Command/Doctrine/Migrations/StatusCommand.php b/src/Command/Doctrine/Migrations/StatusCommand.php index 83ad182..c4e99e8 100644 --- a/src/Command/Doctrine/Migrations/StatusCommand.php +++ b/src/Command/Doctrine/Migrations/StatusCommand.php @@ -4,8 +4,7 @@ namespace Kraz\DoctrineContextBundle\Command\Doctrine\Migrations; -use Kraz\DoctrineContextBundle\Command\Doctrine\DoctrineContextTrait; -use Kraz\DoctrineContextBundle\Configuration\Configuration; +use Kraz\DoctrineContextBundle\Command\Doctrine\ContextRunner; use Override; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -13,26 +12,22 @@ class StatusCommand extends Command { - use DoctrineContextTrait; - public function __construct( private readonly \Doctrine\Migrations\Tools\Console\Command\StatusCommand $command, - Configuration $configuration, + private readonly ContextRunner $contextRunner, ) { - $this->configuration = $configuration; - parent::__construct($this->command->getName(), $this->command->getCode()); } #[Override] protected function configure(): void { - $this->configureAs($this->command); + $this->contextRunner->configure($this, $this->command); } #[Override] protected function execute(InputInterface $input, OutputInterface $output): int { - return $this->runAs($this->command, $input, $output); + return $this->contextRunner->run($this, $this->command, $input, $output); } } diff --git a/src/Command/Doctrine/Migrations/SyncMetadataCommand.php b/src/Command/Doctrine/Migrations/SyncMetadataCommand.php index 4cdad9b..107d6c6 100644 --- a/src/Command/Doctrine/Migrations/SyncMetadataCommand.php +++ b/src/Command/Doctrine/Migrations/SyncMetadataCommand.php @@ -4,8 +4,7 @@ namespace Kraz\DoctrineContextBundle\Command\Doctrine\Migrations; -use Kraz\DoctrineContextBundle\Command\Doctrine\DoctrineContextTrait; -use Kraz\DoctrineContextBundle\Configuration\Configuration; +use Kraz\DoctrineContextBundle\Command\Doctrine\ContextRunner; use Override; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -13,26 +12,22 @@ class SyncMetadataCommand extends Command { - use DoctrineContextTrait; - public function __construct( private readonly \Doctrine\Migrations\Tools\Console\Command\SyncMetadataCommand $command, - Configuration $configuration, + private readonly ContextRunner $contextRunner, ) { - $this->configuration = $configuration; - parent::__construct($this->command->getName(), $this->command->getCode()); } #[Override] protected function configure(): void { - $this->configureAs($this->command); + $this->contextRunner->configure($this, $this->command); } #[Override] protected function execute(InputInterface $input, OutputInterface $output): int { - return $this->runAs($this->command, $input, $output); + return $this->contextRunner->run($this, $this->command, $input, $output); } } diff --git a/src/Command/Doctrine/Migrations/UpToDateCommand.php b/src/Command/Doctrine/Migrations/UpToDateCommand.php index 57537e4..6fa3e5d 100644 --- a/src/Command/Doctrine/Migrations/UpToDateCommand.php +++ b/src/Command/Doctrine/Migrations/UpToDateCommand.php @@ -4,8 +4,7 @@ namespace Kraz\DoctrineContextBundle\Command\Doctrine\Migrations; -use Kraz\DoctrineContextBundle\Command\Doctrine\DoctrineContextTrait; -use Kraz\DoctrineContextBundle\Configuration\Configuration; +use Kraz\DoctrineContextBundle\Command\Doctrine\ContextRunner; use Override; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -13,26 +12,22 @@ class UpToDateCommand extends Command { - use DoctrineContextTrait; - public function __construct( private readonly \Doctrine\Migrations\Tools\Console\Command\UpToDateCommand $command, - Configuration $configuration, + private readonly ContextRunner $contextRunner, ) { - $this->configuration = $configuration; - parent::__construct($this->command->getName(), $this->command->getCode()); } #[Override] protected function configure(): void { - $this->configureAs($this->command); + $this->contextRunner->configure($this, $this->command); } #[Override] protected function execute(InputInterface $input, OutputInterface $output): int { - return $this->runAs($this->command, $input, $output); + return $this->contextRunner->run($this, $this->command, $input, $output); } } diff --git a/src/Command/Doctrine/Migrations/VersionCommand.php b/src/Command/Doctrine/Migrations/VersionCommand.php index f4dfd21..c76d59b 100644 --- a/src/Command/Doctrine/Migrations/VersionCommand.php +++ b/src/Command/Doctrine/Migrations/VersionCommand.php @@ -4,8 +4,7 @@ namespace Kraz\DoctrineContextBundle\Command\Doctrine\Migrations; -use Kraz\DoctrineContextBundle\Command\Doctrine\DoctrineContextTrait; -use Kraz\DoctrineContextBundle\Configuration\Configuration; +use Kraz\DoctrineContextBundle\Command\Doctrine\ContextRunner; use Override; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -13,26 +12,22 @@ class VersionCommand extends Command { - use DoctrineContextTrait; - public function __construct( private readonly \Doctrine\Migrations\Tools\Console\Command\VersionCommand $command, - Configuration $configuration, + private readonly ContextRunner $contextRunner, ) { - $this->configuration = $configuration; - parent::__construct($this->command->getName(), $this->command->getCode()); } #[Override] protected function configure(): void { - $this->configureAs($this->command); + $this->contextRunner->configure($this, $this->command); } #[Override] protected function execute(InputInterface $input, OutputInterface $output): int { - return $this->runAs($this->command, $input, $output); + return $this->contextRunner->run($this, $this->command, $input, $output); } } diff --git a/src/Command/Doctrine/Schema/CreateSchemaCommand.php b/src/Command/Doctrine/Schema/CreateSchemaCommand.php index 0d38bf3..76750d4 100644 --- a/src/Command/Doctrine/Schema/CreateSchemaCommand.php +++ b/src/Command/Doctrine/Schema/CreateSchemaCommand.php @@ -5,9 +5,7 @@ namespace Kraz\DoctrineContextBundle\Command\Doctrine\Schema; use Doctrine\ORM\Tools\Console\Command\SchemaTool\CreateCommand; -use Doctrine\ORM\Tools\Console\EntityManagerProvider; -use Kraz\DoctrineContextBundle\Command\Doctrine\DoctrineContextTrait; -use Kraz\DoctrineContextBundle\Configuration\Configuration; +use Kraz\DoctrineContextBundle\Command\Doctrine\ContextRunner; use Override; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -15,28 +13,22 @@ class CreateSchemaCommand extends Command { - use DoctrineContextTrait; - public function __construct( private readonly CreateCommand $command, - Configuration $configuration, - EntityManagerProvider|null $entityManagerProvider = null, + private readonly ContextRunner $contextRunner, ) { - $this->configuration = $configuration; - $this->entityManagerProvider = $entityManagerProvider; - parent::__construct($this->command->getName(), $this->command->getCode()); } #[Override] protected function configure(): void { - $this->configureAs($this->command); + $this->contextRunner->configure($this, $this->command); } #[Override] protected function execute(InputInterface $input, OutputInterface $output): int { - return $this->runAs($this->command, $input, $output); + return $this->contextRunner->run($this, $this->command, $input, $output); } } diff --git a/src/Command/Doctrine/Schema/ValidateSchemaCommand.php b/src/Command/Doctrine/Schema/ValidateSchemaCommand.php index d2e546c..43aa5da 100644 --- a/src/Command/Doctrine/Schema/ValidateSchemaCommand.php +++ b/src/Command/Doctrine/Schema/ValidateSchemaCommand.php @@ -4,9 +4,7 @@ namespace Kraz\DoctrineContextBundle\Command\Doctrine\Schema; -use Doctrine\ORM\Tools\Console\EntityManagerProvider; -use Kraz\DoctrineContextBundle\Command\Doctrine\DoctrineContextTrait; -use Kraz\DoctrineContextBundle\Configuration\Configuration; +use Kraz\DoctrineContextBundle\Command\Doctrine\ContextRunner; use Override; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -14,28 +12,22 @@ class ValidateSchemaCommand extends Command { - use DoctrineContextTrait; - public function __construct( private readonly \Doctrine\ORM\Tools\Console\Command\ValidateSchemaCommand $command, - Configuration $configuration, - EntityManagerProvider|null $entityManagerProvider = null, + private readonly ContextRunner $contextRunner, ) { - $this->configuration = $configuration; - $this->entityManagerProvider = $entityManagerProvider; - parent::__construct($this->command->getName(), $this->command->getCode()); } #[Override] protected function configure(): void { - $this->configureAs($this->command); + $this->contextRunner->configure($this, $this->command); } #[Override] protected function execute(InputInterface $input, OutputInterface $output): int { - return $this->runAs($this->command, $input, $output); + return $this->contextRunner->run($this, $this->command, $input, $output); } } diff --git a/src/Command/Doctrine/Strategy/CommandExecutionStrategy.php b/src/Command/Doctrine/Strategy/CommandExecutionStrategy.php new file mode 100644 index 0000000..d7035a8 --- /dev/null +++ b/src/Command/Doctrine/Strategy/CommandExecutionStrategy.php @@ -0,0 +1,20 @@ +getNativeDefinition()->hasOption('connection'); + } + + #[Override] + public function configure(Command $wrapper, Command $innerCommand): void + { + $wrapper->addOption('conn', null, InputOption::VALUE_OPTIONAL, 'The name of the connection to use (alias for --connection).'); + $wrapper->addOption('connections', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'The names of the connections to use (multi-value alias for --connection).'); + $wrapper->addOption('conns', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'The names of the connections to use (multi-value alias for --conn).'); + } + + #[Override] + public function execute(Command $wrapper, Command $innerCommand, ContextRunner $contextRunner, Configuration $configuration, InputInterface $input, OutputInterface $output): int + { + $connectionOption = trim((string) $input->getOption('connection')); + $connOption = trim((string) $input->getOption('conn')); + $connectionsOption = $contextRunner->resolveArrayOption($input, 'connections'); + $connsOption = $contextRunner->resolveArrayOption($input, 'conns'); + + if ($connectionOption !== '' && $connOption !== '') { + throw new InvalidArgumentException('You can specify only one of the --connection and --conn options.'); + } + + if (count($connectionsOption) > 0 && count($connsOption) > 0) { + throw new InvalidArgumentException('You can specify only one of the --connections and --conns options.'); + } + + $singleTarget = $connectionOption ?: ($connOption ?: null); + $arrayTargets = count($connectionsOption) > 0 ? $connectionsOption : $connsOption; + + if ($singleTarget !== null && count($arrayTargets) > 0) { + throw new InvalidArgumentException('You can specify only one of the --connection/--conn and --connections/--conns options.'); + } + + $targetConnectionNames = $singleTarget !== null ? [$singleTarget] : $arrayTargets; + + if ($configuration->isExplicitContext() && count($targetConnectionNames) === 0 && ! $input->getOption('ctx-all')) { + throw new InvalidArgumentException('Explicit context is required. Specify a context via --connection, --connections, --conn, --conns, or use --ctx-all to run over all contexts.'); + } + + $list = $contextRunner->filterDoctrineContexts(null, [], $targetConnectionNames); + + return $contextRunner->walkDoctrineContexts(static function (InputInterface $input, OutputInterface $output, string $contextName) use ($wrapper, $innerCommand, $contextRunner) { + $innerCommand->setDefinition($wrapper->getNativeDefinition()); + $innerCommand->setApplication($wrapper->getApplication()); + $newInput = $contextRunner->createNewInput($innerCommand, $input, ['--connection' => $contextName]); + + return $innerCommand->run($newInput, $output); + }, $list, $input, $output); + } +} diff --git a/src/Command/Doctrine/Strategy/EntityManagerStrategy.php b/src/Command/Doctrine/Strategy/EntityManagerStrategy.php new file mode 100644 index 0000000..01638c7 --- /dev/null +++ b/src/Command/Doctrine/Strategy/EntityManagerStrategy.php @@ -0,0 +1,62 @@ +addOption('ems', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'The names of the entity managers to use (multi-value alias for --em).'); + } + + #[Override] + public function execute(Command $wrapper, Command $innerCommand, ContextRunner $contextRunner, Configuration $configuration, InputInterface $input, OutputInterface $output): int + { + $emOption = trim((string) $input->getOption('em')); + $emsOption = $contextRunner->resolveArrayOption($input, 'ems'); + + if ($emOption !== '' && count($emsOption) > 0) { + throw new InvalidArgumentException('You can specify only one of the --em and --ems options.'); + } + + $targetEntityManagers = $emOption !== '' ? [$emOption] : $emsOption; + + if ($configuration->isExplicitContext() && count($targetEntityManagers) === 0 && ! $input->getOption('ctx-all')) { + throw new InvalidArgumentException('Explicit context is required. Specify a context via --em, --ems, or use --ctx-all to run over all contexts.'); + } + + $list = $contextRunner->filterDoctrineContexts(static fn (DependencyFactory|string $ctx): bool => is_string($ctx) ? $configuration->isEntityManager($ctx) : $ctx->hasEntityManager(), $targetEntityManagers); + + return $contextRunner->walkDoctrineContexts(static function (InputInterface $input, OutputInterface $output, string $em) use ($wrapper, $innerCommand, $contextRunner) { + $innerCommand->setDefinition($wrapper->getNativeDefinition()); + $innerCommand->setApplication($wrapper->getApplication()); + $newInput = $contextRunner->createNewInput($innerCommand, $input, ['--em' => $em]); + + return $innerCommand->run($newInput, $output); + }, $list, $input, $output); + } +} diff --git a/src/Command/Doctrine/Strategy/MigrationStrategy.php b/src/Command/Doctrine/Strategy/MigrationStrategy.php new file mode 100644 index 0000000..285a8ef --- /dev/null +++ b/src/Command/Doctrine/Strategy/MigrationStrategy.php @@ -0,0 +1,77 @@ +getNativeDefinition(); + + if ($nativeDefinition->hasOption('em')) { + $wrapper->addOption('ems', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'The names of the entity managers to use (multi-value alias for --em).'); + } + + if ($nativeDefinition->hasOption('conn')) { + $wrapper->addOption('conns', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'The names of the connections to use (multi-value alias for --conn).'); + } + } + + #[Override] + public function execute(Command $wrapper, Command $innerCommand, ContextRunner $contextRunner, Configuration $configuration, InputInterface $input, OutputInterface $output): int + { + $emOption = trim((string) $input->getOption('em')); + $connOption = trim((string) $input->getOption('conn')); + $emsOption = $contextRunner->resolveArrayOption($input, 'ems'); + $connsOption = $contextRunner->resolveArrayOption($input, 'conns'); + + if ($emOption !== '' && count($emsOption) > 0) { + throw new InvalidArgumentException('You can specify only one of the --em and --ems options.'); + } + + if ($connOption !== '' && count($connsOption) > 0) { + throw new InvalidArgumentException('You can specify only one of the --conn and --conns options.'); + } + + $targetEntityManagers = $emOption !== '' ? [$emOption] : $emsOption; + $targetConnectionNames = $connOption !== '' ? [$connOption] : $connsOption; + + if ($configuration->isExplicitContext() && count($targetEntityManagers) === 0 && count($targetConnectionNames) === 0 && ! $input->getOption('ctx-all')) { + throw new InvalidArgumentException('Explicit context is required. Specify a context via --em, --ems, --conn, --conns, or use --ctx-all to run over all contexts.'); + } + + $list = $contextRunner->filterDoctrineContexts(null, $targetEntityManagers, $targetConnectionNames); + + return $contextRunner->walkDoctrineContexts(static function (InputInterface $input, OutputInterface $output, string $contextName, DependencyFactory $dependencyFactory) use ($wrapper, $innerCommand) { + $command = new ReflectionClass($innerCommand)->newInstance($dependencyFactory); + $command->setDefinition($wrapper->getNativeDefinition()); + $command->setApplication($wrapper->getApplication()); + + return $command->run($input, $output); + }, $list, $input, $output); + } +} diff --git a/src/DoctrineContextBundle.php b/src/DoctrineContextBundle.php index 8fb8020..6a6a2dc 100644 --- a/src/DoctrineContextBundle.php +++ b/src/DoctrineContextBundle.php @@ -192,6 +192,8 @@ public function loadExtension(array $config, ContainerConfigurator $container, C $container->import('../config/services_orm.php'); } + $this->wireStrategies($builder); + $builder ->getDefinition('doctrine.doctrine_context.configuration') ->addMethodCall('setExplicitContext', [$config['explicit_context']]); @@ -213,6 +215,21 @@ public function loadExtension(array $config, ContainerConfigurator $container, C } } + private function wireStrategies(ContainerBuilder $builder): void + { + $contextRunner = $builder->getDefinition('doctrine.doctrine_context.context_runner'); + + if ($builder->hasDefinition('doctrine.doctrine_context.strategy.entity_manager')) { + $contextRunner->addMethodCall('addStrategy', [new Reference('doctrine.doctrine_context.strategy.entity_manager')]); + } + + if ($builder->hasDefinition('doctrine.doctrine_context.strategy.migration')) { + $contextRunner->addMethodCall('addStrategy', [new Reference('doctrine.doctrine_context.strategy.migration')]); + } + + $contextRunner->addMethodCall('addStrategy', [new Reference('doctrine.doctrine_context.strategy.connection')]); + } + /** @param array $config */ private function loadContextConfiguration(string $name, array $config, ContainerBuilder $builder, bool $isEntityManager): void { diff --git a/tests/Unit/CommandExecutionStrategyTest.php b/tests/Unit/CommandExecutionStrategyTest.php new file mode 100644 index 0000000..b1ed15e --- /dev/null +++ b/tests/Unit/CommandExecutionStrategyTest.php @@ -0,0 +1,127 @@ +markTestSkipped('doctrine/orm is not installed'); + } + + $strategy = new EntityManagerStrategy(); + $command = new DoctrineValidateSchemaCommand($this->createStub(EntityManagerProvider::class)); + + self::assertTrue($strategy->supports($command)); + } + + public function testEntityManagerStrategyRejectsMigrationCommand(): void + { + if (! class_exists(DoctrineMigrateCommand::class)) { + $this->markTestSkipped('doctrine/migrations is not installed'); + } + + $strategy = new EntityManagerStrategy(); + $command = new DoctrineMigrateCommand($this->createStub(DependencyFactory::class)); + + self::assertFalse($strategy->supports($command)); + } + + public function testEntityManagerStrategyRejectsPlainCommand(): void + { + $strategy = new EntityManagerStrategy(); + $command = new Command('test:plain'); + + self::assertFalse($strategy->supports($command)); + } + + public function testMigrationStrategySupportsDoctrineCommand(): void + { + if (! class_exists(DoctrineMigrateCommand::class)) { + $this->markTestSkipped('doctrine/migrations is not installed'); + } + + $strategy = new MigrationStrategy(); + $command = new DoctrineMigrateCommand($this->createStub(DependencyFactory::class)); + + self::assertTrue($strategy->supports($command)); + } + + public function testMigrationStrategyRejectsEntityManagerCommand(): void + { + if (! interface_exists(EntityManagerProvider::class)) { + $this->markTestSkipped('doctrine/orm is not installed'); + } + + $strategy = new MigrationStrategy(); + $command = new DoctrineValidateSchemaCommand($this->createStub(EntityManagerProvider::class)); + + self::assertFalse($strategy->supports($command)); + } + + public function testMigrationStrategyRejectsPlainCommand(): void + { + $strategy = new MigrationStrategy(); + $command = new Command('test:plain'); + + self::assertFalse($strategy->supports($command)); + } + + public function testConnectionStrategySupportsCommandWithConnectionOption(): void + { + $strategy = new ConnectionStrategy(); + $command = new Command('test:with-connection'); + $command->addOption('connection', null, InputOption::VALUE_OPTIONAL); + + self::assertTrue($strategy->supports($command)); + } + + public function testConnectionStrategyRejectsCommandWithoutConnectionOption(): void + { + $strategy = new ConnectionStrategy(); + $command = new Command('test:no-connection'); + + self::assertFalse($strategy->supports($command)); + } + + public function testConnectionStrategyRejectsEntityManagerCommand(): void + { + if (! interface_exists(EntityManagerProvider::class)) { + $this->markTestSkipped('doctrine/orm is not installed'); + } + + $strategy = new ConnectionStrategy(); + $command = new DoctrineValidateSchemaCommand($this->createStub(EntityManagerProvider::class)); + + self::assertFalse($strategy->supports($command)); + } + + public function testConnectionStrategyRejectsMigrationCommand(): void + { + if (! class_exists(DoctrineMigrateCommand::class)) { + $this->markTestSkipped('doctrine/migrations is not installed'); + } + + $strategy = new ConnectionStrategy(); + $command = new DoctrineMigrateCommand($this->createStub(DependencyFactory::class)); + + self::assertFalse($strategy->supports($command)); + } +} diff --git a/tests/Unit/ContextRunnerTest.php b/tests/Unit/ContextRunnerTest.php new file mode 100644 index 0000000..d033fd2 --- /dev/null +++ b/tests/Unit/ContextRunnerTest.php @@ -0,0 +1,121 @@ +expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Unsupported CLI command'); + + $runner->run($command, $command, new ArrayInput([]), new NullOutput()); + } + + public function testRunAsThrowsWhenNoStrategiesRegistered(): void + { + $runner = new ContextRunner(new Configuration()); + $command = new Command('test:any'); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Unsupported CLI command'); + + $runner->run($command, $command, new ArrayInput([]), new NullOutput()); + } + + public function testRunAsDelegatesToFirstMatchingStrategy(): void + { + $runner = new ContextRunner(new Configuration()); + + $nonMatching = $this->createMock(CommandExecutionStrategy::class); + $nonMatching->method('supports')->willReturn(false); + $nonMatching->expects($this->never())->method('execute'); + + $matching = $this->createMock(CommandExecutionStrategy::class); + $matching->method('supports')->willReturn(true); + $matching->expects($this->once())->method('execute')->willReturn(Command::SUCCESS); + + $runner->addStrategy($nonMatching); + $runner->addStrategy($matching); + + $command = new Command('test:matched'); + $result = $runner->run($command, $command, new ArrayInput([]), new NullOutput()); + + self::assertSame(Command::SUCCESS, $result); + } + + public function testFirstMatchingStrategyWinsWhenMultipleMatch(): void + { + $runner = new ContextRunner(new Configuration()); + + $first = $this->createMock(CommandExecutionStrategy::class); + $first->method('supports')->willReturn(true); + $first->expects($this->once())->method('execute')->willReturn(Command::SUCCESS); + + $second = $this->createMock(CommandExecutionStrategy::class); + $second->method('supports')->willReturn(true); + $second->expects($this->never())->method('execute'); + + $runner->addStrategy($first); + $runner->addStrategy($second); + + $command = new Command('test:both-match'); + $result = $runner->run($command, $command, new ArrayInput([]), new NullOutput()); + + self::assertSame(Command::SUCCESS, $result); + } + + public function testConfigureAsDelegatesToMatchingStrategy(): void + { + $runner = new ContextRunner(new Configuration()); + + $nonMatching = $this->createMock(CommandExecutionStrategy::class); + $nonMatching->method('supports')->willReturn(false); + $nonMatching->expects($this->never())->method('configure'); + + $matching = $this->createMock(CommandExecutionStrategy::class); + $matching->method('supports')->willReturn(true); + $matching->expects($this->once())->method('configure'); + + $runner->addStrategy($nonMatching); + $runner->addStrategy($matching); + + $wrapper = new Command('test:wrapper'); + $inner = new Command('test:inner'); + $inner->setDescription('Test description'); + + $runner->configure($wrapper, $inner); + } + + public function testConfigureAsAddsContextOptionsRegardlessOfStrategy(): void + { + $runner = new ContextRunner(new Configuration()); + + $strategy = $this->createStub(CommandExecutionStrategy::class); + $strategy->method('supports')->willReturn(true); + $runner->addStrategy($strategy); + + $wrapper = new Command('test:wrapper'); + $inner = new Command('test:inner'); + $inner->setDescription('Test description'); + + $runner->configure($wrapper, $inner); + + self::assertTrue($wrapper->getDefinition()->hasOption('ctx-isolation')); + self::assertTrue($wrapper->getDefinition()->hasOption('ctx-all')); + } +}