From 78b09f506f8da4de811f1b718b06dc14444ff5be Mon Sep 17 00:00:00 2001 From: Krasen Borisov Date: Tue, 14 Apr 2026 16:08:35 +0300 Subject: [PATCH] (feat) Introduced context lists support --- README.md | 47 ++++- src/Command/Doctrine/DoctrineContextTrait.php | 172 +++++++++++++----- tests/Functional/DatabaseCreateTest.php | 78 ++++++++ tests/Functional/ExplicitContextTest.php | 2 +- .../Functional/MigrationsConnsOptionTest.php | 97 ++++++++++ tests/Functional/MigrationsEmsOptionTest.php | 101 ++++++++++ tests/Functional/SchemaCreateTest.php | 40 ++++ 7 files changed, 485 insertions(+), 52 deletions(-) create mode 100644 tests/Functional/MigrationsConnsOptionTest.php create mode 100644 tests/Functional/MigrationsEmsOptionTest.php diff --git a/README.md b/README.md index f8c229d..f8822bd 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Packagist Version](https://img.shields.io/packagist/v/kraz/doctrine-context-bundle)](https://packagist.org/packages/kraz/doctrine-context-bundle) [![GitHub license](https://img.shields.io/github/license/kraz/doctrine-context-bundle)](LICENSE) -A Symfony bundle that makes working with multiple Doctrine entity managers or DBAL connections painless. It wraps the standard Doctrine commands so that a single command can target one specific context or fan out across all of them automatically. The only hard dependency is `doctrine/dbal` — ORM and Migrations support are both optional. +A Symfony bundle that makes working with multiple Doctrine entity managers or DBAL connections painless. It wraps the standard Doctrine commands so that a single command can target one specific context or fan out across all of them automatically. The only hard dependency is `doctrine/dbal` - ORM and Migrations support are both optional. ## The problem @@ -20,13 +20,13 @@ There is also a subtle schema-pollution problem: after running migrations, `doct ## What this bundle does -- **Database command integration**: `doctrine:database:create` fans out across all registered contexts. Works with DBAL alone — no ORM or Migrations required. Accepts both `--connection` (native option) and `--conn` (context-system alias). -- **Migrations command integration** *(requires `doctrine/doctrine-migrations-bundle`)*: every `doctrine:migrations:*` command gains `--em` and `--conn` options. Pass one to target a specific context, or omit both to run across all registered contexts in sequence. -- **ORM command integration** *(requires `doctrine/orm`)*: `doctrine:schema:create`, `doctrine:schema:validate`, and `doctrine:mapping:info` receive the same fan-out behaviour. +- **Database command integration**: `doctrine:database:create` fans out across all registered contexts. Works with DBAL alone - no ORM or Migrations required. Accepts `--connection` / `--conn` to target a single context and `--connections` / `--conns` to target a specific subset. +- **Migrations command integration** *(requires `doctrine/doctrine-migrations-bundle`)*: every `doctrine:migrations:*` command gains `--em` / `--ems` and `--conn` / `--conns` options. Pass one to target a single context or a subset, or omit all to run across every registered context in sequence. +- **ORM command integration** *(requires `doctrine/orm`)*: `doctrine:schema:create`, `doctrine:schema:validate`, and `doctrine:mapping:info` receive the same fan-out behaviour, including `--em` / `--ems` for subset selection. - **Schema filter**: automatically hides the migration metadata table from `doctrine:schema:update` and `doctrine:schema:validate`, so those commands never see it as unmanaged. - **`--ctx-isolation`**: an extra flag added to every wrapped command. When set, a failure in one context does not abort the remaining contexts. - **`--ctx-all`**: an extra flag added to every wrapped command. Explicitly runs the command over all registered contexts. Required when `explicit_context` is enabled and no specific context is given. -- **`explicit_context`** *(bundle config)*: when `true`, every wrapped command requires an explicit context via `--em`, `--conn`, `--connection`, or `--ctx-all`. Prevents accidental fan-out in production environments. +- **`explicit_context`** *(bundle config)*: when `true`, every wrapped command requires an explicit context via `--em`, `--ems`, `--conn`, `--conns`, `--connection`, `--connections`, or `--ctx-all`. Prevents accidental fan-out in production environments. ## Requirements @@ -139,7 +139,7 @@ doctrine_context: With this option active, the following is an error: ```bash -# Error: Explicit context is required. Specify a context via --em or use --ctx-all to run over all contexts. +# Error: Explicit context is required. Specify a context via --em, --ems, --conn, --conns, or use --ctx-all to run over all contexts. php bin/console doctrine:migrations:migrate --no-interaction ``` @@ -149,6 +149,9 @@ Provide a context or opt into all: # Single context php bin/console doctrine:migrations:migrate --em=shop --no-interaction +# Subset of contexts +php bin/console doctrine:migrations:migrate --ems=shop,analytics --no-interaction + # All contexts, intentionally php bin/console doctrine:migrations:migrate --ctx-all --no-interaction ``` @@ -159,7 +162,7 @@ The migration-related keys below are only accepted when `doctrine/doctrine-migra ```yaml doctrine_context: - explicit_context: false # when true, --em/--conn/--connection or --ctx-all is required + explicit_context: false # when true, --em/--ems/--conn/--conns/--connection/--connections or --ctx-all is required entity_managers: # or connections: : migrations_paths: @@ -222,6 +225,30 @@ php bin/console doctrine:migrations:migrate --em=shop --no-interaction php bin/console doctrine:migrations:migrate --conn=shop --no-interaction ``` +### Target a subset of contexts + +Use the plural form of each option to pass multiple context names. Values can be supplied as separate arguments or as a comma-separated list - both forms are equivalent and may be combined: + +```bash +# Separate arguments +php bin/console doctrine:migrations:migrate --ems=shop --ems=analytics --no-interaction + +# Comma-separated (same result) +php bin/console doctrine:migrations:migrate --ems=shop,analytics --no-interaction + +# DBAL connections +php bin/console doctrine:migrations:migrate --conns=shop,analytics --no-interaction + +# ORM schema commands +php bin/console doctrine:schema:create --ems=shop,analytics +``` + +| Option | Plural / multi-value form | Applicable commands | +|----------------|---------------------------|---------------------------------| +| `--em` | `--ems` | ORM and migration commands | +| `--conn` | `--conns` | Migration and database commands | +| `--connection` | `--connections` | Database commands | + ### Continue past failures with `--ctx-isolation` By default, a failure in one context stops execution when executed in non-interactive mode. Use `--ctx-isolation` to continue with the remaining contexts regardless: @@ -248,9 +275,13 @@ php bin/console doctrine:database:create # All contexts, explicitly php bin/console doctrine:database:create --ctx-all -# Specific context – both flags are equivalent +# Specific context – all four flags are equivalent php bin/console doctrine:database:create --connection=shop php bin/console doctrine:database:create --conn=shop + +# Subset of contexts – all four flags are equivalent +php bin/console doctrine:database:create --connections=shop,analytics +php bin/console doctrine:database:create --conns=shop,analytics ``` ### All supported commands diff --git a/src/Command/Doctrine/DoctrineContextTrait.php b/src/Command/Doctrine/DoctrineContextTrait.php index b7a2a2e..b47909b 100644 --- a/src/Command/Doctrine/DoctrineContextTrait.php +++ b/src/Command/Doctrine/DoctrineContextTrait.php @@ -19,13 +19,19 @@ use Symfony\Component\Console\Style\SymfonyStyle; use Throwable; +use function array_filter; +use function array_key_exists; use function array_key_first; +use function array_map; use function array_replace; use function array_shift; +use function array_unique; +use function array_values; use function assert; use function call_user_func; use function count; -use function in_array; +use function explode; +use function implode; use function is_string; use function ksort; use function sprintf; @@ -48,9 +54,23 @@ private function configureAs(Command $command): void $this->setDescription(($this->getDescription() ?: $command->getDescription()) . ' [Doctrine Context]'); $this->setAliases($this->getAliases() ?: $command->getAliases()); $this->setHelp($this->getHelp() ?: $command->getHelp()); - $this->setDefinition($command->getNativeDefinition()); + $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 @@ -60,13 +80,20 @@ private function runAs(Command $command, InputInterface $input, OutputInterface $ctxAll = (bool) $input->getOption('ctx-all'); if ($command instanceof AbstractEntityManagerCommand) { - $emOption = trim((string) $input->getOption('em')); + $emOption = trim((string) $input->getOption('em')); + $emsOption = $this->resolveArrayOption($input, 'ems'); - if ($this->configuration->isExplicitContext() && $emOption === '' && ! $ctxAll) { - throw new InvalidArgumentException('Explicit context is required. Specify a context via --em or use --ctx-all to run over all contexts.'); + if ($emOption !== '' && count($emsOption) > 0) { + throw new InvalidArgumentException('You can specify only one of the --em and --ems options.'); } - $list = $this->filterDoctrineContexts(fn (DependencyFactory|string $ctx): bool => is_string($ctx) ? $this->configuration->isEntityManager($ctx) : $ctx->hasEntityManager(), $emOption !== '' ? $emOption : null); + $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()); @@ -78,14 +105,27 @@ private function runAs(Command $command, InputInterface $input, OutputInterface } if ($command instanceof AbstractDoctrineMigrationCommand) { - $emOption = trim((string) $input->getOption('em')); - $connOption = trim((string) $input->getOption('conn')); + $emOption = trim((string) $input->getOption('em')); + $connOption = trim((string) $input->getOption('conn')); + $emsOption = $this->resolveArrayOption($input, 'ems'); + $connsOption = $this->resolveArrayOption($input, 'conns'); - if ($this->configuration->isExplicitContext() && $emOption === '' && $connOption === '' && ! $ctxAll) { - throw new InvalidArgumentException('Explicit context is required. Specify a context via --em, --conn, or use --ctx-all to run over all contexts.'); + if ($emOption !== '' && count($emsOption) > 0) { + throw new InvalidArgumentException('You can specify only one of the --em and --ems options.'); } - $list = $this->filterDoctrineContexts(null, $emOption !== '' ? $emOption : null, $connOption !== '' ? $connOption : null); + 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); @@ -97,20 +137,33 @@ private function runAs(Command $command, InputInterface $input, OutputInterface } if ($command->getNativeDefinition()->hasOption('connection')) { - $connectionOption = trim((string) $input->getOption('connection')); - $connOption = trim((string) $input->getOption('conn')); + $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.'); } - $targetConnection = $connectionOption ?: ($connOption ?: null); + 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() && $targetConnection === null && ! $ctxAll) { - throw new InvalidArgumentException('Explicit context is required. Specify a context via --connection, --conn, or use --ctx-all to run over all contexts.'); + 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, null, $targetConnection); + $list = $this->filterDoctrineContexts(null, [], $targetConnectionNames); return $this->walkDoctrineContexts(function (InputInterface $input, OutputInterface $output, string $contextName) use ($command) { $command->setDefinition($this->getNativeDefinition()); @@ -166,36 +219,44 @@ private function walkDoctrineContexts(callable $callback, array $list, InputInte /** * @param callable(DependencyFactory|string): bool|null $filter + * @param string[] $targetEntityManagers + * @param string[] $targetConnectionNames * * @return array */ - private function filterDoctrineContexts(callable|null $filter = null, string|null $targetEntityManager = null, string|null $targetConnectionName = null): array + private function filterDoctrineContexts(callable|null $filter = null, array $targetEntityManagers = [], array $targetConnectionNames = []): array { - $targetEntityManager = trim($targetEntityManager ?? '') ?: null; - $targetConnectionName = trim($targetConnectionName ?? '') ?: null; - if ($targetEntityManager !== null && $targetConnectionName !== null) { - throw new InvalidArgumentException('You can specify only one of the --em and --conn options.'); - } - - $contextName = $targetEntityManager ?: $targetConnectionName; - if ($contextName !== null) { - $dependencyFactory = $this->configuration->findDependencyFactory($contextName); - if ($dependencyFactory !== null) { - $list = [$contextName => $dependencyFactory]; - } elseif (in_array($contextName, $this->configuration->getContextNames(), true)) { - $list = [$contextName => null]; - } else { - $list = []; + $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; } - } else { - $list = $this->configuration->getDependencyFactories(); - foreach ($this->configuration->getContextNames() as $name) { - if (! isset($list[$name])) { - $list[$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) { @@ -209,12 +270,17 @@ private function filterDoctrineContexts(callable|null $filter = null, string|nul $list = $filteredList; } - if (count($list) === 0 && $targetEntityManager !== null) { - throw new InvalidArgumentException(sprintf('Unknown doctrine entity manager "%s" or it\'s not registered as doctrine context.', $targetEntityManager)); - } + // 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))); + } - if (count($list) === 0 && $targetConnectionName !== null) { - throw new InvalidArgumentException(sprintf('Unknown doctrine connection "%s" or it\'s not registered as doctrine context.', $targetConnectionName)); + 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); @@ -222,6 +288,26 @@ private function filterDoctrineContexts(callable|null $filter = null, string|nul 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 { diff --git a/tests/Functional/DatabaseCreateTest.php b/tests/Functional/DatabaseCreateTest.php index 3ce8651..366e018 100644 --- a/tests/Functional/DatabaseCreateTest.php +++ b/tests/Functional/DatabaseCreateTest.php @@ -76,6 +76,84 @@ public function testDatabaseCreateAllContextsFansOutAcrossAllRegisteredContexts( self::assertStringContainsString('beta', $output, 'Output should mention the beta context'); } + public function testDatabaseCreateWithConnectionsOptionRunsSubsetOfContexts(): void + { + $output = $this->captureOutput('doctrine:database:create --connections=alpha --connections=beta --if-not-exists --ctx-isolation --no-interaction'); + + self::assertStringContainsString('alpha', $output, 'Output should mention the alpha context'); + self::assertStringContainsString('beta', $output, 'Output should mention the beta context'); + self::assertStringNotContainsString('default', $output, 'Output should not mention the default context'); + } + + public function testDatabaseCreateWithConnectionsOptionAcceptsCommaSeparatedValues(): void + { + $output = $this->captureOutput('doctrine:database:create --connections=alpha,beta --if-not-exists --ctx-isolation --no-interaction'); + + self::assertStringContainsString('alpha', $output, 'Output should mention the alpha context'); + self::assertStringContainsString('beta', $output, 'Output should mention the beta context'); + self::assertStringNotContainsString('default', $output, 'Output should not mention the default context'); + } + + public function testDatabaseCreateWithConnsOptionRunsSubsetOfContexts(): void + { + $output = $this->captureOutput('doctrine:database:create --conns=alpha --conns=beta --if-not-exists --ctx-isolation --no-interaction'); + + self::assertStringContainsString('alpha', $output, 'Output should mention the alpha context'); + self::assertStringContainsString('beta', $output, 'Output should mention the beta context'); + self::assertStringNotContainsString('default', $output, 'Output should not mention the default context'); + } + + public function testDatabaseCreateWithConnsOptionAcceptsCommaSeparatedValues(): void + { + $output = $this->captureOutput('doctrine:database:create --conns=alpha,beta --if-not-exists --ctx-isolation --no-interaction'); + + self::assertStringContainsString('alpha', $output, 'Output should mention the alpha context'); + self::assertStringContainsString('beta', $output, 'Output should mention the beta context'); + self::assertStringNotContainsString('default', $output, 'Output should not mention the default context'); + } + + public function testDatabaseCreateFailsWhenConnectionAndConnectionsAreSpecified(): void + { + $command = self::getContainer()->get('doctrine.database_create_command.with_context'); + self::assertInstanceOf(CreateDatabaseCommand::class, $command); + /** @psalm-suppress InternalMethod */ + $command->mergeApplicationDefinition(); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('You can specify only one of the --connection/--conn and --connections/--conns options.'); + + $input = new ArrayInput(['--connection' => 'alpha', '--connections' => ['beta'], '--if-not-exists' => true], $command->getDefinition()); + $command->run($input, new BufferedOutput()); + } + + public function testDatabaseCreateFailsWhenConnectionsAndConnsAreSpecified(): void + { + $command = self::getContainer()->get('doctrine.database_create_command.with_context'); + self::assertInstanceOf(CreateDatabaseCommand::class, $command); + /** @psalm-suppress InternalMethod */ + $command->mergeApplicationDefinition(); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('You can specify only one of the --connections and --conns options.'); + + $input = new ArrayInput(['--connections' => ['alpha'], '--conns' => ['beta'], '--if-not-exists' => true], $command->getDefinition()); + $command->run($input, new BufferedOutput()); + } + + public function testDatabaseCreateFailsWhenConnAndConnsAreSpecified(): void + { + $command = self::getContainer()->get('doctrine.database_create_command.with_context'); + self::assertInstanceOf(CreateDatabaseCommand::class, $command); + /** @psalm-suppress InternalMethod */ + $command->mergeApplicationDefinition(); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('You can specify only one of the --connection/--conn and --connections/--conns options.'); + + $input = new ArrayInput(['--conn' => 'alpha', '--conns' => ['beta'], '--if-not-exists' => true], $command->getDefinition()); + $command->run($input, new BufferedOutput()); + } + public function testDatabaseCreateFailsWhenBothConnectionAndConnAreSpecified(): void { // Run through the command directly (bypassing Application) so that Symfony's diff --git a/tests/Functional/ExplicitContextTest.php b/tests/Functional/ExplicitContextTest.php index f4b869b..13b7dca 100644 --- a/tests/Functional/ExplicitContextTest.php +++ b/tests/Functional/ExplicitContextTest.php @@ -55,7 +55,7 @@ public function testExplicitContextRequiresContextOptionOrCtxAll(): void $command->mergeApplicationDefinition(); $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Explicit context is required. Specify a context via --connection, --conn, or use --ctx-all to run over all contexts.'); + $this->expectExceptionMessage('Explicit context is required. Specify a context via --connection, --connections, --conn, --conns, or use --ctx-all to run over all contexts.'); $input = new ArrayInput(['--if-not-exists' => true], $command->getDefinition()); $command->run($input, new BufferedOutput()); diff --git a/tests/Functional/MigrationsConnsOptionTest.php b/tests/Functional/MigrationsConnsOptionTest.php new file mode 100644 index 0000000..3207026 --- /dev/null +++ b/tests/Functional/MigrationsConnsOptionTest.php @@ -0,0 +1,97 @@ +cleanDatabases(); + + $kernel = self::bootKernel(); + $this->application = new Application($kernel); + $this->application->setAutoExit(false); + } + + #[Override] + protected function tearDown(): void + { + parent::tearDown(); + + $this->cleanDatabases(); + } + + protected function databaseFilePrefix(): string + { + return 'doctrine_context_test_dbal_'; + } + + public function testSyncMetadataWithConnsRunsOnlySelectedContexts(): void + { + $this->runCommand('doctrine:migrations:sync-metadata-storage --conns=alpha --conns=beta'); + + $this->assertTableExists('alpha', 'zzz_migrations'); + $this->assertTableExists('beta', 'zzz_migrations'); + $this->assertTableNotExists('default', 'zzz_migrations'); + } + + public function testSyncMetadataWithConnsAcceptsCommaSeparatedValues(): void + { + $this->runCommand('doctrine:migrations:sync-metadata-storage --conns=alpha,beta'); + + $this->assertTableExists('alpha', 'zzz_migrations'); + $this->assertTableExists('beta', 'zzz_migrations'); + $this->assertTableNotExists('default', 'zzz_migrations'); + } + + public function testSyncMetadataFailsWhenBothConnAndConnsAreSpecified(): void + { + $command = self::getContainer()->get('doctrine_migrations.sync_metadata_command.with_context'); + self::assertInstanceOf(SyncMetadataCommand::class, $command); + /** @psalm-suppress InternalMethod */ + $command->mergeApplicationDefinition(); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('You can specify only one of the --conn and --conns options.'); + + $input = new ArrayInput(['--conn' => 'alpha', '--conns' => ['beta']], $command->getDefinition()); + $command->run($input, new BufferedOutput()); + } + + #[Override] + protected function getConnection(string $name): Connection + { + $connection = self::getContainer()->get('doctrine.dbal.' . $name . '_connection'); + self::assertInstanceOf(Connection::class, $connection); + + return $connection; + } +} diff --git a/tests/Functional/MigrationsEmsOptionTest.php b/tests/Functional/MigrationsEmsOptionTest.php new file mode 100644 index 0000000..a164a91 --- /dev/null +++ b/tests/Functional/MigrationsEmsOptionTest.php @@ -0,0 +1,101 @@ +cleanDatabases(); + + $kernel = self::bootKernel(); + $this->application = new Application($kernel); + $this->application->setAutoExit(false); + } + + #[Override] + protected function tearDown(): void + { + parent::tearDown(); + + $this->cleanDatabases(); + } + + protected function databaseFilePrefix(): string + { + return 'doctrine_context_test_'; + } + + public function testSyncMetadataWithEmsRunsOnlySelectedContexts(): void + { + $this->runCommand('doctrine:migrations:sync-metadata-storage --ems=alpha --ems=beta'); + + $this->assertTableExists('alpha', 'zzz_migrations'); + $this->assertTableExists('beta', 'zzz_migrations'); + $this->assertTableNotExists('default', 'zzz_migrations'); + } + + public function testSyncMetadataWithEmsAcceptsCommaSeparatedValues(): void + { + $this->runCommand('doctrine:migrations:sync-metadata-storage --ems=alpha,beta'); + + $this->assertTableExists('alpha', 'zzz_migrations'); + $this->assertTableExists('beta', 'zzz_migrations'); + $this->assertTableNotExists('default', 'zzz_migrations'); + } + + public function testSyncMetadataFailsWhenBothEmAndEmsAreSpecified(): void + { + $command = self::getContainer()->get('doctrine_migrations.sync_metadata_command.with_context'); + self::assertInstanceOf(SyncMetadataCommand::class, $command); + /** @psalm-suppress InternalMethod */ + $command->mergeApplicationDefinition(); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('You can specify only one of the --em and --ems options.'); + + $input = new ArrayInput(['--em' => 'alpha', '--ems' => ['beta']], $command->getDefinition()); + $command->run($input, new BufferedOutput()); + } + + #[Override] + protected function getConnection(string $name): Connection + { + $registry = self::getContainer()->get('doctrine'); + self::assertInstanceOf(ManagerRegistry::class, $registry); + + $connection = $registry->getConnection($name); + self::assertInstanceOf(Connection::class, $connection); + + return $connection; + } +} diff --git a/tests/Functional/SchemaCreateTest.php b/tests/Functional/SchemaCreateTest.php index b5c537b..c9e5e99 100644 --- a/tests/Functional/SchemaCreateTest.php +++ b/tests/Functional/SchemaCreateTest.php @@ -6,12 +6,16 @@ use Doctrine\DBAL\Connection; use Doctrine\Persistence\ManagerRegistry; +use Kraz\DoctrineContextBundle\Command\Doctrine\Schema\CreateSchemaCommand; use Kraz\DoctrineContextBundle\Tests\InspectsSqliteDatabasesTrait; use Kraz\DoctrineContextBundle\Tests\RunsConsoleCommandsTrait; use Kraz\DoctrineContextBundle\Tests\TestKernelOrmOnly; use Override; use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Output\BufferedOutput; class SchemaCreateTest extends KernelTestCase { @@ -93,6 +97,42 @@ public function testSchemaCreateDoesNotCreateMigrationTableInTargetDatabase(): v $this->assertTableNotExists('alpha', 'zzz_migrations'); } + public function testSchemaCreateWithEmsRunsOnlySelectedContexts(): void + { + $exitCode = $this->runCommand('doctrine:schema:create --ems=alpha --ems=beta'); + + self::assertSame(0, $exitCode); + + $this->assertTableExists('alpha', 'product'); + $this->assertTableExists('beta', 'customer'); + $this->assertTableNotExists('default', 'tag'); + } + + public function testSchemaCreateWithEmsAcceptsCommaSeparatedValues(): void + { + $exitCode = $this->runCommand('doctrine:schema:create --ems=alpha,beta'); + + self::assertSame(0, $exitCode); + + $this->assertTableExists('alpha', 'product'); + $this->assertTableExists('beta', 'customer'); + $this->assertTableNotExists('default', 'tag'); + } + + public function testSchemaCreateFailsWhenBothEmAndEmsAreSpecified(): void + { + $command = self::getContainer()->get('doctrine.schema_create_command.with_context'); + self::assertInstanceOf(CreateSchemaCommand::class, $command); + /** @psalm-suppress InternalMethod */ + $command->mergeApplicationDefinition(); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('You can specify only one of the --em and --ems options.'); + + $input = new ArrayInput(['--em' => 'alpha', '--ems' => ['beta']], $command->getDefinition()); + $command->run($input, new BufferedOutput()); + } + public function testSchemaValidateSucceedsAfterSchemaCreate(): void { $this->runCommand('doctrine:schema:create');