diff --git a/README.md b/README.md index dd8670c..a272952 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,11 @@ [![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/migrations` commands (and optionally ORM schema commands) so that a single command can target one specific context or fan out across all of them automatically. +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 -When a project has more than one entity manager or DBAL connection, running migrations requires repeating the same command once per context, which requires each time specifying the right `--em` or `--conn` flag manually: +When a project has more than one entity manager or DBAL connection, running the same operation across all of them requires repeating the command manually once per context: ```bash php bin/console doctrine:migrations:migrate --em=shop @@ -20,21 +20,20 @@ There is also a subtle schema-pollution problem: after running migrations, `doct ## What this bundle does -- **Context-aware commands**: 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. +- **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. Accepts both `--connection` (native option) and `--conn` (context-system alias). - **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. ## Requirements -| Dependency | Version | -|---------------------------------------|---------------------| -| PHP | `>= 8.4` | -| `doctrine/dbal` | `^4.4` | -| `symfony/console` | `^8.0` | -| `doctrine/doctrine-migrations-bundle` | `^4.0` *(dev)* | -| `doctrine/orm` | `^3.6` *(optional)* | +| Dependency | Version | +|---------------------------------------|----------------------| +| PHP | `>= 8.4` | +| `doctrine/doctrine-bundle` | `^3.2` | +| `doctrine/doctrine-migrations-bundle` | `^4.0` *(optional)* | +| `doctrine/orm` | `^3.6` *(optional)* | ## Installation @@ -53,14 +52,26 @@ return [ ## Configuration -Register each entity manager or connection that should be treated as a named context. You may use `entity_managers` (requires `doctrine/orm`), `connections`, or a mix, but not both for the same name. +Register each entity manager or connection that should be treated as a named context. You may use `entity_managers` (requires `doctrine/orm`) or `connections`, but not both for the same name. -### With entity managers +The migration-related options (`migrations_paths`, `storage`, `services`, etc.) are only available when `doctrine/doctrine-migrations-bundle` is installed. Without it, a context is configured with just its name. + +### DBAL only (no ORM, no Migrations) ```yaml # config/packages/doctrine_context.yaml doctrine_context: - entity_managers: + connections: + default: ~ + shop: ~ + analytics: ~ +``` + +### With DBAL connections and Migrations + +```yaml +doctrine_context: + connections: default: migrations_paths: App\Migrations\Default: '%kernel.project_dir%/migrations/default' @@ -73,19 +84,13 @@ doctrine_context: storage: table_storage: table_name: doctrine_migration_versions - analytics: - migrations_paths: - App\Migrations\Analytics: '%kernel.project_dir%/migrations/analytics' - storage: - table_storage: - table_name: doctrine_migration_versions ``` -### With DBAL connections only +### With entity managers (ORM) and Migrations ```yaml doctrine_context: - connections: + entity_managers: default: migrations_paths: App\Migrations\Default: '%kernel.project_dir%/migrations/default' @@ -95,10 +100,31 @@ doctrine_context: shop: migrations_paths: App\Migrations\Shop: '%kernel.project_dir%/migrations/shop' + storage: + table_storage: + table_name: doctrine_migration_versions + analytics: + migrations_paths: + App\Migrations\Analytics: '%kernel.project_dir%/migrations/analytics' + storage: + table_storage: + table_name: doctrine_migration_versions +``` + +### With entity managers (ORM), no Migrations + +```yaml +doctrine_context: + entity_managers: + default: ~ + shop: ~ + analytics: ~ ``` ### Full configuration reference +The migration-related keys below are only accepted when `doctrine/doctrine-migrations-bundle` is installed. + ```yaml doctrine_context: entity_managers: # or connections: @@ -184,7 +210,13 @@ php bin/console doctrine:database:create --conn=shop ### All supported commands -Every `doctrine:migrations:*` command supports the context options: +Always available (DBAL only): + +| Command | Description | +|------------------------------|-----------------------------------------------------| +| `doctrine:database:create` | Create the database for each registered context | + +When `doctrine/doctrine-migrations-bundle` is installed: | Command | Description | |---------------------------------------------|---------------------------------------------| @@ -202,13 +234,7 @@ Every `doctrine:migrations:*` command supports the context options: | `doctrine:migrations:dump-schema` | Dump the schema for a mapping | | `doctrine:migrations:sync-metadata-storage` | Sync the metadata storage | -Always available: - -| Command | Description | -|------------------------------|-----------------------------------------------------| -| `doctrine:database:create` | Create the database for each registered context | - -When `doctrine/orm` is installed and configured: +When `doctrine/orm` is installed: | Command | Description | |----------------------------|----------------------------------------------| @@ -218,7 +244,7 @@ When `doctrine/orm` is installed and configured: ### Schema filter -The bundle automatically registers a DBAL schema filter per context that hides the migration metadata table from `doctrine:schema:update` and `doctrine:schema:validate`. This prevents those commands from flagging the migration table as an unmanaged or extra table. +When `doctrine/doctrine-migrations-bundle` is installed, the bundle automatically registers a DBAL schema filter per context that hides the migration metadata table from `doctrine:schema:update` and `doctrine:schema:validate`. This prevents those commands from flagging the migration table as an unmanaged or extra table. The filter activates only during schema update/validate commands and is otherwise transparent. diff --git a/composer.json b/composer.json index 4cea179..559d209 100644 --- a/composer.json +++ b/composer.json @@ -17,8 +17,7 @@ ], "require": { "php": ">=8.4", - "doctrine/dbal": "^4.4", - "symfony/console": "^8.0" + "doctrine/doctrine-bundle": "^3.2" }, "require-dev": { "doctrine/coding-standard": "^13.0", diff --git a/config/services.php b/config/services.php index ea1d96a..8636055 100644 --- a/config/services.php +++ b/config/services.php @@ -4,22 +4,7 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; -use Doctrine\Migrations\Configuration\Configuration as DoctrineMigrationsConfiguration; -use Doctrine\Migrations\DependencyFactory; use Kraz\DoctrineContextBundle\Command\Doctrine\Database\CreateDatabaseCommand; -use Kraz\DoctrineContextBundle\Command\Doctrine\Migrations\CurrentCommand; -use Kraz\DoctrineContextBundle\Command\Doctrine\Migrations\DiffCommand; -use Kraz\DoctrineContextBundle\Command\Doctrine\Migrations\DumpSchemaCommand; -use Kraz\DoctrineContextBundle\Command\Doctrine\Migrations\ExecuteCommand; -use Kraz\DoctrineContextBundle\Command\Doctrine\Migrations\GenerateCommand; -use Kraz\DoctrineContextBundle\Command\Doctrine\Migrations\LatestCommand; -use Kraz\DoctrineContextBundle\Command\Doctrine\Migrations\ListCommand; -use Kraz\DoctrineContextBundle\Command\Doctrine\Migrations\MigrateCommand; -use Kraz\DoctrineContextBundle\Command\Doctrine\Migrations\RollupCommand; -use Kraz\DoctrineContextBundle\Command\Doctrine\Migrations\StatusCommand; -use Kraz\DoctrineContextBundle\Command\Doctrine\Migrations\SyncMetadataCommand; -use Kraz\DoctrineContextBundle\Command\Doctrine\Migrations\UpToDateCommand; -use Kraz\DoctrineContextBundle\Command\Doctrine\Migrations\VersionCommand; use Kraz\DoctrineContextBundle\Configuration\Configuration as DoctrineContextConfiguration; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -28,122 +13,11 @@ ->set('doctrine.doctrine_context.configuration', DoctrineContextConfiguration::class) ->public() - ->set('doctrine.doctrine_context.connection_configuration', DoctrineMigrationsConfiguration::class) - ->abstract() - - ->set('doctrine.doctrine_context.dependency_factory', DependencyFactory::class) - ->abstract() - - // Commands ->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'), ]) - ->tag('console.command', ['command' => 'doctrine:database:create']) - - ->set('doctrine_migrations.current_command.with_context', CurrentCommand::class) - ->decorate('doctrine_migrations.current_command') - ->args([ - service('.inner'), - service('doctrine.doctrine_context.configuration'), - ]) - ->tag('console.command', ['command' => 'doctrine:migrations:current']) - - ->set('doctrine_migrations.diff_command.with_context', DiffCommand::class) - ->decorate('doctrine_migrations.diff_command') - ->args([ - service('.inner'), - service('doctrine.doctrine_context.configuration'), - ]) - ->tag('console.command', ['command' => 'doctrine:migrations:diff']) - - ->set('doctrine_migrations.dump_schema_command.with_context', DumpSchemaCommand::class) - ->decorate('doctrine_migrations.dump_schema_command') - ->args([ - service('.inner'), - service('doctrine.doctrine_context.configuration'), - ]) - ->tag('console.command', ['command' => 'doctrine:migrations:dump-schema']) - - ->set('doctrine_migrations.execute_command.with_context', ExecuteCommand::class) - ->decorate('doctrine_migrations.execute_command') - ->args([ - service('.inner'), - service('doctrine.doctrine_context.configuration'), - ]) - ->tag('console.command', ['command' => 'doctrine:migrations:execute']) - - ->set('doctrine_migrations.generate_command.with_context', GenerateCommand::class) - ->decorate('doctrine_migrations.generate_command') - ->args([ - service('.inner'), - service('doctrine.doctrine_context.configuration'), - ]) - ->tag('console.command', ['command' => 'doctrine:migrations:generate']) - - ->set('doctrine_migrations.latest_command.with_context', LatestCommand::class) - ->decorate('doctrine_migrations.latest_command') - ->args([ - service('.inner'), - service('doctrine.doctrine_context.configuration'), - ]) - ->tag('console.command', ['command' => 'doctrine:migrations:latest']) - - ->set('doctrine_migrations.versions_command.with_context', ListCommand::class) - ->decorate('doctrine_migrations.versions_command') - ->args([ - service('.inner'), - service('doctrine.doctrine_context.configuration'), - ]) - ->tag('console.command', ['command' => 'doctrine:migrations:list']) - - ->set('doctrine_migrations.migrate_command.with_context', MigrateCommand::class) - ->decorate('doctrine_migrations.migrate_command') - ->args([ - service('.inner'), - service('doctrine.doctrine_context.configuration'), - ]) - ->tag('console.command', ['command' => 'doctrine:migrations:migrate']) - - ->set('doctrine_migrations.rollup_command.with_context', RollupCommand::class) - ->decorate('doctrine_migrations.rollup_command') - ->args([ - service('.inner'), - service('doctrine.doctrine_context.configuration'), - ]) - ->tag('console.command', ['command' => 'doctrine:migrations:rollup']) - - ->set('doctrine_migrations.status_command.with_context', StatusCommand::class) - ->decorate('doctrine_migrations.status_command') - ->args([ - service('.inner'), - service('doctrine.doctrine_context.configuration'), - ]) - ->tag('console.command', ['command' => 'doctrine:migrations:status']) - - ->set('doctrine_migrations.sync_metadata_command.with_context', SyncMetadataCommand::class) - ->decorate('doctrine_migrations.sync_metadata_command') - ->args([ - service('.inner'), - service('doctrine.doctrine_context.configuration'), - ]) - ->tag('console.command', ['command' => 'doctrine:migrations:sync-metadata-storage']) - - ->set('doctrine_migrations.up_to_date_command.with_context', UpToDateCommand::class) - ->decorate('doctrine_migrations.up_to_date_command') - ->args([ - service('.inner'), - service('doctrine.doctrine_context.configuration'), - ]) - ->tag('console.command', ['command' => 'doctrine:migrations:up-to-date']) - - ->set('doctrine_migrations.version_command.with_context', VersionCommand::class) - ->decorate('doctrine_migrations.version_command') - ->args([ - service('.inner'), - service('doctrine.doctrine_context.configuration'), - ]) - ->tag('console.command', ['command' => 'doctrine:migrations:version']); + ->tag('console.command', ['command' => 'doctrine:database:create']); }; diff --git a/config/services_migrations.php b/config/services_migrations.php new file mode 100644 index 0000000..a6bf983 --- /dev/null +++ b/config/services_migrations.php @@ -0,0 +1,135 @@ +services() + ->set('doctrine.doctrine_context.connection_configuration', DoctrineMigrationsConfiguration::class) + ->abstract() + + ->set('doctrine.doctrine_context.dependency_factory', DependencyFactory::class) + ->abstract() + + ->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'), + ]) + ->tag('console.command', ['command' => 'doctrine:migrations:current']) + + ->set('doctrine_migrations.diff_command.with_context', DiffCommand::class) + ->decorate('doctrine_migrations.diff_command', null, 0, ContainerInterface::IGNORE_ON_INVALID_REFERENCE) + ->args([ + service('.inner'), + service('doctrine.doctrine_context.configuration'), + ]) + ->tag('console.command', ['command' => 'doctrine:migrations:diff']) + + ->set('doctrine_migrations.dump_schema_command.with_context', DumpSchemaCommand::class) + ->decorate('doctrine_migrations.dump_schema_command', null, 0, ContainerInterface::IGNORE_ON_INVALID_REFERENCE) + ->args([ + service('.inner'), + service('doctrine.doctrine_context.configuration'), + ]) + ->tag('console.command', ['command' => 'doctrine:migrations:dump-schema']) + + ->set('doctrine_migrations.execute_command.with_context', ExecuteCommand::class) + ->decorate('doctrine_migrations.execute_command', null, 0, ContainerInterface::IGNORE_ON_INVALID_REFERENCE) + ->args([ + service('.inner'), + service('doctrine.doctrine_context.configuration'), + ]) + ->tag('console.command', ['command' => 'doctrine:migrations:execute']) + + ->set('doctrine_migrations.generate_command.with_context', GenerateCommand::class) + ->decorate('doctrine_migrations.generate_command', null, 0, ContainerInterface::IGNORE_ON_INVALID_REFERENCE) + ->args([ + service('.inner'), + service('doctrine.doctrine_context.configuration'), + ]) + ->tag('console.command', ['command' => 'doctrine:migrations:generate']) + + ->set('doctrine_migrations.latest_command.with_context', LatestCommand::class) + ->decorate('doctrine_migrations.latest_command', null, 0, ContainerInterface::IGNORE_ON_INVALID_REFERENCE) + ->args([ + service('.inner'), + service('doctrine.doctrine_context.configuration'), + ]) + ->tag('console.command', ['command' => 'doctrine:migrations:latest']) + + ->set('doctrine_migrations.versions_command.with_context', ListCommand::class) + ->decorate('doctrine_migrations.versions_command', null, 0, ContainerInterface::IGNORE_ON_INVALID_REFERENCE) + ->args([ + service('.inner'), + service('doctrine.doctrine_context.configuration'), + ]) + ->tag('console.command', ['command' => 'doctrine:migrations:list']) + + ->set('doctrine_migrations.migrate_command.with_context', MigrateCommand::class) + ->decorate('doctrine_migrations.migrate_command', null, 0, ContainerInterface::IGNORE_ON_INVALID_REFERENCE) + ->args([ + service('.inner'), + service('doctrine.doctrine_context.configuration'), + ]) + ->tag('console.command', ['command' => 'doctrine:migrations:migrate']) + + ->set('doctrine_migrations.rollup_command.with_context', RollupCommand::class) + ->decorate('doctrine_migrations.rollup_command', null, 0, ContainerInterface::IGNORE_ON_INVALID_REFERENCE) + ->args([ + service('.inner'), + service('doctrine.doctrine_context.configuration'), + ]) + ->tag('console.command', ['command' => 'doctrine:migrations:rollup']) + + ->set('doctrine_migrations.status_command.with_context', StatusCommand::class) + ->decorate('doctrine_migrations.status_command', null, 0, ContainerInterface::IGNORE_ON_INVALID_REFERENCE) + ->args([ + service('.inner'), + service('doctrine.doctrine_context.configuration'), + ]) + ->tag('console.command', ['command' => 'doctrine:migrations:status']) + + ->set('doctrine_migrations.sync_metadata_command.with_context', SyncMetadataCommand::class) + ->decorate('doctrine_migrations.sync_metadata_command', null, 0, ContainerInterface::IGNORE_ON_INVALID_REFERENCE) + ->args([ + service('.inner'), + service('doctrine.doctrine_context.configuration'), + ]) + ->tag('console.command', ['command' => 'doctrine:migrations:sync-metadata-storage']) + + ->set('doctrine_migrations.up_to_date_command.with_context', UpToDateCommand::class) + ->decorate('doctrine_migrations.up_to_date_command', null, 0, ContainerInterface::IGNORE_ON_INVALID_REFERENCE) + ->args([ + service('.inner'), + service('doctrine.doctrine_context.configuration'), + ]) + ->tag('console.command', ['command' => 'doctrine:migrations:up-to-date']) + + ->set('doctrine_migrations.version_command.with_context', VersionCommand::class) + ->decorate('doctrine_migrations.version_command', null, 0, ContainerInterface::IGNORE_ON_INVALID_REFERENCE) + ->args([ + service('.inner'), + service('doctrine.doctrine_context.configuration'), + ]) + ->tag('console.command', ['command' => 'doctrine:migrations:version']); +}; diff --git a/src/Command/Doctrine/DoctrineContextTrait.php b/src/Command/Doctrine/DoctrineContextTrait.php index e819aa0..2f69bee 100644 --- a/src/Command/Doctrine/DoctrineContextTrait.php +++ b/src/Command/Doctrine/DoctrineContextTrait.php @@ -19,13 +19,15 @@ use Symfony\Component\Console\Style\SymfonyStyle; use Throwable; -use function array_filter; +use function array_key_first; use function array_replace; -use function array_search; use function array_shift; -use function asort; use function assert; +use function call_user_func; use function count; +use function in_array; +use function is_string; +use function ksort; use function sprintf; use function trim; @@ -55,7 +57,7 @@ private function runAs(Command $command, InputInterface $input, OutputInterface assert($this instanceof Command); if ($command instanceof AbstractEntityManagerCommand) { - $list = $this->filterDoctrineContexts(static fn (DependencyFactory $dependencyFactory) => $dependencyFactory->hasEntityManager(), (string) $input->getOption('em')); + $list = $this->filterDoctrineContexts(fn (DependencyFactory|string $ctx): bool => is_string($ctx) ? $this->configuration->isEntityManager($ctx) : $ctx->hasEntityManager(), (string) $input->getOption('em')); return $this->walkDoctrineContexts(function (InputInterface $input, OutputInterface $output, string $em) use ($command) { $command->setDefinition($this->getNativeDefinition()); @@ -101,18 +103,18 @@ private function runAs(Command $command, InputInterface $input, OutputInterface throw new InvalidArgumentException(sprintf('Unsupported CLI command "%s"', $command::class)); } - /** @param array $list */ + /** @param array $list */ private function walkDoctrineContexts(callable $callback, array $list, InputInterface $input, OutputInterface $output): int { - $all = $list; $result = Command::SUCCESS; $ui = new SymfonyStyle($input, $output)->getErrorStyle(); $contextIsolation = $input->getOption('ctx-isolation'); - while ($dependencyFactory = array_shift($list)) { - $contextName = array_search($dependencyFactory, $all); - assert($contextName !== false); - if (count($all) > 1) { - $ui->section(sprintf('%s: %s', $dependencyFactory->hasEntityManager() ? 'Entity Manager' : 'Connection', $contextName)); + $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 { @@ -122,7 +124,7 @@ private function walkDoctrineContexts(callable $callback, array $list, InputInte $cmdResult = Command::FAILURE; } - if (count($all) > 1) { + if ($total > 1) { $ui->newLine(); } @@ -141,7 +143,11 @@ private function walkDoctrineContexts(callable $callback, array $list, InputInte return $result; } - /** @return array */ + /** + * @param callable(DependencyFactory|string): bool|null $filter + * + * @return array + */ private function filterDoctrineContexts(callable|null $filter = null, string|null $targetEntityManager = null, string|null $targetConnectionName = null): array { $targetEntityManager = trim($targetEntityManager ?? '') ?: null; @@ -153,13 +159,33 @@ private function filterDoctrineContexts(callable|null $filter = null, string|nul $contextName = $targetEntityManager ?: $targetConnectionName; if ($contextName !== null) { $dependencyFactory = $this->configuration->findDependencyFactory($contextName); - $list = $dependencyFactory !== null ? [$contextName => $dependencyFactory] : []; + if ($dependencyFactory !== null) { + $list = [$contextName => $dependencyFactory]; + } elseif (in_array($contextName, $this->configuration->getContextNames(), true)) { + $list = [$contextName => null]; + } else { + $list = []; + } } else { $list = $this->configuration->getDependencyFactories(); + foreach ($this->configuration->getContextNames() as $name) { + if (! isset($list[$name])) { + $list[$name] = null; + } + } } if ($filter !== null) { - $list = array_filter($list, $filter); + $filteredList = []; + foreach ($list as $contextName => $dependencyFactory) { + if (! call_user_func($filter, $dependencyFactory ?? $contextName)) { + continue; + } + + $filteredList[$contextName] = $dependencyFactory; + } + + $list = $filteredList; } if (count($list) === 0 && $targetEntityManager !== null) { @@ -170,7 +196,7 @@ private function filterDoctrineContexts(callable|null $filter = null, string|nul throw new InvalidArgumentException(sprintf('Unknown doctrine connection "%s" or it\'s not registered as doctrine context.', $targetConnectionName)); } - asort($list); + ksort($list); return $list; } diff --git a/src/Configuration/Configuration.php b/src/Configuration/Configuration.php index 17cc2e3..1a066ed 100644 --- a/src/Configuration/Configuration.php +++ b/src/Configuration/Configuration.php @@ -10,11 +10,22 @@ class Configuration { + /** @var array context name => isEntityManager */ + private array $contexts = []; + /** @var array */ private array $dependencyFactories = []; + public function registerContext(string $contextName, bool $isEntityManager): self + { + $this->contexts[$contextName] = $isEntityManager; + + return $this; + } + public function addDependencyFactory(string $contextName, DependencyFactory $dependencyFactory): self { + $this->contexts[$contextName] = $dependencyFactory->hasEntityManager(); $this->dependencyFactories[$contextName] = $dependencyFactory; return $this; @@ -29,7 +40,12 @@ public function getDependencyFactories(): array /** @return string[] */ public function getContextNames(): array { - return array_keys($this->dependencyFactories); + return array_keys($this->contexts); + } + + public function isEntityManager(string $contextName): bool + { + return $this->contexts[$contextName] ?? false; } public function findDependencyFactory(string $contextName): DependencyFactory|null diff --git a/src/DoctrineContextBundle.php b/src/DoctrineContextBundle.php index a418597..8c45e50 100644 --- a/src/DoctrineContextBundle.php +++ b/src/DoctrineContextBundle.php @@ -30,6 +30,7 @@ use function array_unique; use function array_values; use function assert; +use function class_exists; use function constant; use function count; use function explode; @@ -51,111 +52,120 @@ class DoctrineContextBundle extends AbstractBundle #[Override] public function configure(DefinitionConfigurator $definition): void { - $organizeMigrationModes = $this->getOrganizeMigrationsModes(); - $contextConfiguration = static function (string $contextName) use ($organizeMigrationModes) { - $node = new ArrayNodeDefinition($contextName); - $node - ->arrayPrototype() - ->fixXmlConfig('migration', 'migrations') - ->fixXmlConfig('migrations_path', 'migrations_paths') - ->children() - ->arrayNode('migrations_paths') - ->info('A list of namespace/path pairs where to look for migrations.') - ->defaultValue([]) - ->useAttributeAsKey('namespace') - ->prototype('scalar')->end() - ->end() + if (class_exists('Doctrine\Migrations\DependencyFactory')) { + $organizeMigrationModes = $this->getOrganizeMigrationsModes(); + $contextConfiguration = static function (string $contextName) use ($organizeMigrationModes) { + $node = new ArrayNodeDefinition($contextName); + $node + ->arrayPrototype() + ->fixXmlConfig('migration', 'migrations') + ->fixXmlConfig('migrations_path', 'migrations_paths') + ->children() + ->arrayNode('migrations_paths') + ->info('A list of namespace/path pairs where to look for migrations.') + ->defaultValue([]) + ->useAttributeAsKey('namespace') + ->prototype('scalar')->end() + ->end() - ->arrayNode('services') - ->info('A set of services to pass to the underlying doctrine/migrations library, allowing to change its behaviour.') - ->useAttributeAsKey('service') - ->defaultValue([]) - ->validate() - ->ifTrue(static function ($v) { - return count(array_filter(array_keys($v), static function (string $doctrineService): bool { - return ! str_starts_with($doctrineService, 'Doctrine\Migrations\\'); - })); - }) - ->thenInvalid('Valid services for the DoctrineMigrationsBundle must be in the "Doctrine\Migrations" namespace.') + ->arrayNode('services') + ->info('A set of services to pass to the underlying doctrine/migrations library, allowing to change its behaviour.') + ->useAttributeAsKey('service') + ->defaultValue([]) + ->validate() + ->ifTrue(static function ($v) { + return count(array_filter(array_keys($v), static function (string $doctrineService): bool { + return ! str_starts_with($doctrineService, 'Doctrine\Migrations\\'); + })); + }) + ->thenInvalid('Valid services for the DoctrineMigrationsBundle must be in the "Doctrine\Migrations" namespace.') + ->end() + ->prototype('scalar')->end() ->end() - ->prototype('scalar')->end() - ->end() - ->arrayNode('factories') - ->info('A set of callables to pass to the underlying doctrine/migrations library as services, allowing to change its behaviour.') - ->useAttributeAsKey('factory') - ->defaultValue([]) - ->validate() - ->ifTrue(static function ($v) { - return count(array_filter(array_keys($v), static function (string $doctrineService): bool { - return ! str_starts_with($doctrineService, 'Doctrine\Migrations\\'); - })); - }) - ->thenInvalid('Valid callables for the DoctrineMigrationsBundle must be in the "Doctrine\Migrations" namespace.') + ->arrayNode('factories') + ->info('A set of callables to pass to the underlying doctrine/migrations library as services, allowing to change its behaviour.') + ->useAttributeAsKey('factory') + ->defaultValue([]) + ->validate() + ->ifTrue(static function ($v) { + return count(array_filter(array_keys($v), static function (string $doctrineService): bool { + return ! str_starts_with($doctrineService, 'Doctrine\Migrations\\'); + })); + }) + ->thenInvalid('Valid callables for the DoctrineMigrationsBundle must be in the "Doctrine\Migrations" namespace.') + ->end() + ->prototype('scalar')->end() ->end() - ->prototype('scalar')->end() - ->end() - ->arrayNode('storage') - ->addDefaultsIfNotSet() - ->info('Storage to use for migration status metadata.') - ->children() - ->arrayNode('table_storage') - ->addDefaultsIfNotSet() - ->info('The default metadata storage, implemented as a table in the database.') - ->children() - ->scalarNode('table_name')->defaultValue('doctrine_migration_versions')->cannotBeEmpty()->end() - ->scalarNode('version_column_name')->defaultValue(null)->end() - ->scalarNode('version_column_length')->defaultValue(null)->end() - ->scalarNode('executed_at_column_name')->defaultValue(null)->end() - ->scalarNode('execution_time_column_name')->defaultValue(null)->end() + ->arrayNode('storage') + ->addDefaultsIfNotSet() + ->info('Storage to use for migration status metadata.') + ->children() + ->arrayNode('table_storage') + ->addDefaultsIfNotSet() + ->info('The default metadata storage, implemented as a table in the database.') + ->children() + ->scalarNode('table_name')->defaultValue('doctrine_migration_versions')->cannotBeEmpty()->end() + ->scalarNode('version_column_name')->defaultValue(null)->end() + ->scalarNode('version_column_length')->defaultValue(null)->end() + ->scalarNode('executed_at_column_name')->defaultValue(null)->end() + ->scalarNode('execution_time_column_name')->defaultValue(null)->end() + ->end() ->end() ->end() ->end() - ->end() - ->arrayNode('migrations') - ->info('A list of migrations to load in addition to the one discovered via "migrations_paths".') - ->prototype('scalar')->end() - ->defaultValue([]) - ->end() - ->scalarNode('all_or_nothing') - ->info('Run all migrations in a transaction.') - ->defaultValue(false) - ->end() - ->scalarNode('check_database_platform') - ->info('Adds an extra check in the generated migrations to allow execution only on the same platform as they were initially generated on.') - ->defaultValue(true) - ->end() - ->scalarNode('custom_template') - ->info('Custom template path for generated migration classes.') - ->defaultValue(null) - ->end() - ->scalarNode('organize_migrations') - ->defaultValue(false) - ->info('Organize migrations mode. Possible values are: "BY_YEAR", "BY_YEAR_AND_MONTH", false') - ->validate() - ->ifTrue(static function ($v) use ($organizeMigrationModes): bool { - if ($v === false) { - return false; - } - - return ! is_string($v) || ! in_array(strtoupper($v), $organizeMigrationModes, true); - }) - ->thenInvalid('Invalid organize migrations mode value %s') + ->arrayNode('migrations') + ->info('A list of migrations to load in addition to the one discovered via "migrations_paths".') + ->prototype('scalar')->end() + ->defaultValue([]) + ->end() + ->scalarNode('all_or_nothing') + ->info('Run all migrations in a transaction.') + ->defaultValue(false) ->end() - ->validate() - ->ifString() - ->then(static function ($v) { - return constant('Doctrine\Migrations\Configuration\Configuration::VERSIONS_ORGANIZATION_' . strtoupper($v)); + ->scalarNode('check_database_platform') + ->info('Adds an extra check in the generated migrations to allow execution only on the same platform as they were initially generated on.') + ->defaultValue(true) + ->end() + ->scalarNode('custom_template') + ->info('Custom template path for generated migration classes.') + ->defaultValue(null) + ->end() + ->scalarNode('organize_migrations') + ->defaultValue(false) + ->info('Organize migrations mode. Possible values are: "BY_YEAR", "BY_YEAR_AND_MONTH", false') + ->validate() + ->ifTrue(static function ($v) use ($organizeMigrationModes): bool { + if ($v === false) { + return false; + } + + return ! is_string($v) || ! in_array(strtoupper($v), $organizeMigrationModes, true); }) + ->thenInvalid('Invalid organize migrations mode value %s') + ->end() + ->validate() + ->ifString() + ->then(static function ($v) { + return constant('Doctrine\Migrations\Configuration\Configuration::VERSIONS_ORGANIZATION_' . strtoupper($v)); + }) + ->end() ->end() ->end() - ->end() - ->end(); + ->end(); - return $node; - }; + return $node; + }; + } else { + $contextConfiguration = static function (string $contextName) { + $node = new ArrayNodeDefinition($contextName); + $node->arrayPrototype()->end(); + + return $node; + }; + } $definition->rootNode() ->children() @@ -170,6 +180,10 @@ public function loadExtension(array $config, ContainerConfigurator $container, C { $container->import('../config/services.php'); + if (class_exists('Doctrine\Migrations\DependencyFactory')) { + $container->import('../config/services_migrations.php'); + } + if (interface_exists('Doctrine\ORM\EntityManagerInterface')) { $container->import('../config/services_orm.php'); } @@ -194,6 +208,14 @@ public function loadExtension(array $config, ContainerConfigurator $container, C /** @param array $config */ private function loadContextConfiguration(string $name, array $config, ContainerBuilder $builder, bool $isEntityManager): void { + $builder + ->getDefinition('doctrine.doctrine_context.configuration') + ->addMethodCall('registerContext', [$name, $isEntityManager]); + + if (! class_exists(DependencyFactory::class)) { + return; + } + if (! isset($config['services'][MetadataStorage::class])) { $schemaFilterDefinition = $builder->setDefinition(sprintf('doctrine.doctrine_context.%s_events_listener.schema_filter', $name), new Definition(SchemaFilterListener::class)); $schemaFilterDefinition diff --git a/tests/Functional/DatabaseCreateDbalOnlyTest.php b/tests/Functional/DatabaseCreateDbalOnlyTest.php new file mode 100644 index 0000000..ce22a7f --- /dev/null +++ b/tests/Functional/DatabaseCreateDbalOnlyTest.php @@ -0,0 +1,74 @@ +cleanDatabases(); + + $kernel = self::bootKernel(); + $this->application = new Application($kernel); + $this->application->setAutoExit(false); + } + + #[Override] + protected function tearDown(): void + { + parent::tearDown(); + + $this->cleanDatabases(); + } + + public function testDatabaseCreateSingleContextViaConnectionOption(): void + { + $output = $this->captureOutput('doctrine:database:create --connection=alpha --if-not-exists'); + + self::assertStringNotContainsString('default', $output, 'Output should not mention untargeted connections'); + self::assertStringNotContainsString('beta', $output, 'Output should not mention untargeted connections'); + } + + public function testDatabaseCreateSingleContextViaConnOption(): void + { + $output = $this->captureOutput('doctrine:database:create --conn=alpha --if-not-exists'); + + self::assertStringNotContainsString('default', $output, 'Output should not mention untargeted connections'); + self::assertStringNotContainsString('beta', $output, 'Output should not mention untargeted connections'); + } + + public function testDatabaseCreateAllContextsFansOutAcrossAllRegisteredContexts(): void + { + $output = $this->captureOutput('doctrine:database:create --if-not-exists --ctx-isolation --no-interaction'); + + self::assertStringContainsString('default', $output, 'Output should mention the default context'); + self::assertStringContainsString('alpha', $output, 'Output should mention the alpha context'); + self::assertStringContainsString('beta', $output, 'Output should mention the beta context'); + } + + protected function databaseFilePrefix(): string + { + return 'doctrine_context_test_dbalonly_'; + } +} diff --git a/tests/Functional/SchemaCreateTest.php b/tests/Functional/SchemaCreateTest.php index 2bec91d..b5c537b 100644 --- a/tests/Functional/SchemaCreateTest.php +++ b/tests/Functional/SchemaCreateTest.php @@ -5,17 +5,14 @@ namespace Kraz\DoctrineContextBundle\Tests\Functional; use Doctrine\DBAL\Connection; -use Doctrine\ORM\EntityManagerInterface; use Doctrine\Persistence\ManagerRegistry; use Kraz\DoctrineContextBundle\Tests\InspectsSqliteDatabasesTrait; use Kraz\DoctrineContextBundle\Tests\RunsConsoleCommandsTrait; -use Kraz\DoctrineContextBundle\Tests\TestKernel; +use Kraz\DoctrineContextBundle\Tests\TestKernelOrmOnly; use Override; use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; -use function interface_exists; - class SchemaCreateTest extends KernelTestCase { use RunsConsoleCommandsTrait; @@ -24,16 +21,12 @@ class SchemaCreateTest extends KernelTestCase #[Override] protected static function getKernelClass(): string { - return TestKernel::class; + return TestKernelOrmOnly::class; } #[Override] protected function setUp(): void { - if (! interface_exists(EntityManagerInterface::class)) { - $this->markTestSkipped('doctrine/orm is not installed'); - } - $this->cleanDatabases(); $kernel = self::bootKernel(); @@ -120,4 +113,9 @@ protected function getConnection(string $name): Connection return $connection; } + + protected function databaseFilePrefix(): string + { + return 'doctrine_context_test_ormonly_'; + } } diff --git a/tests/Functional/SchemaDiffTest.php b/tests/Functional/SchemaDiffTest.php new file mode 100644 index 0000000..09d5c91 --- /dev/null +++ b/tests/Functional/SchemaDiffTest.php @@ -0,0 +1,150 @@ + migration files generated during the test, deleted in tearDown */ + private array $generatedMigrationFiles = []; + + #[Override] + protected static function getKernelClass(): string + { + return TestKernel::class; + } + + #[Override] + protected function setUp(): void + { + $this->cleanDatabases(); + $this->generatedMigrationFiles = []; + + $kernel = self::bootKernel(); + $this->application = new Application($kernel); + $this->application->setAutoExit(false); + } + + #[Override] + protected function tearDown(): void + { + parent::tearDown(); + + $this->cleanDatabases(); + + foreach ($this->generatedMigrationFiles as $file) { + if (file_exists($file)) { + unlink($file); + } + } + } + + public function testDiffDoesNotIncludeMigrationsTableForSingleContext(): void + { + // Create only the migrations metadata table — no entity tables yet. + // This simulates a fresh environment where doctrine:migrations:sync-metadata-storage + // has been run but no entity migration has been executed yet. + $this->runCommand('doctrine:migrations:sync-metadata-storage --em=alpha'); + + $this->assertTableExists('alpha', 'zzz_migrations'); + $this->assertTableNotExists('alpha', 'product'); + + $dir = __DIR__ . '/../Fixtures/Migrations/ContextA'; + $glob = glob($dir . '/Version*.php'); + $before = $glob !== false ? $glob : []; + + // Diff sees: DB has zzz_migrations only; ORM expects product. + // Without the schema filter: generates DROP zzz_migrations + CREATE product. + // With the schema filter: generates CREATE product only. + $this->runCommand('doctrine:migrations:diff --em=alpha --no-interaction'); + + $glob = glob($dir . '/Version*.php'); + $newFiles = array_values(array_diff($glob !== false ? $glob : [], $before)); + $this->generatedMigrationFiles = $newFiles; + + self::assertCount(1, $newFiles, 'A migration file should have been generated for the missing entity schema'); + + $content = (string) file_get_contents($newFiles[0]); + self::assertStringContainsString('product', $content, 'The migration should create the product table'); + self::assertStringNotContainsString( + 'zzz_migrations', + $content, + 'The migration metadata table must not appear in the generated diff migration', + ); + } + + public function testDiffDoesNotIncludeMigrationsTableForAllContexts(): void + { + // Create only the migrations metadata tables for every context. + $this->runCommand('doctrine:migrations:sync-metadata-storage --no-interaction'); + + $dirs = [ + 'default' => [__DIR__ . '/../Fixtures/Migrations/Default', 'tag'], + 'alpha' => [__DIR__ . '/../Fixtures/Migrations/ContextA', 'product'], + 'beta' => [__DIR__ . '/../Fixtures/Migrations/ContextB', 'customer'], + ]; + + $before = []; + foreach ($dirs as $ctx => [$dir, $entityTable]) { + $this->assertTableExists($ctx, 'zzz_migrations'); + $this->assertTableNotExists($ctx, $entityTable); + $glob = glob($dir . '/Version*.php'); + $before[$ctx] = $glob !== false ? $glob : []; + } + + $this->runCommand('doctrine:migrations:diff --no-interaction'); + + foreach ($dirs as $ctx => [$dir, $entityTable]) { + $glob = glob($dir . '/Version*.php'); + $newFiles = array_values(array_diff($glob !== false ? $glob : [], $before[$ctx])); + $this->generatedMigrationFiles = array_merge($this->generatedMigrationFiles, $newFiles); + + self::assertCount(1, $newFiles, "A migration file should have been generated for context '" . $ctx . "'"); + + $content = (string) file_get_contents($newFiles[0]); + self::assertStringContainsString( + $entityTable, + $content, + "The migration for context '" . $ctx . "' should create the '" . $entityTable . "' table", + ); + self::assertStringNotContainsString( + 'zzz_migrations', + $content, + "The migration metadata table must not appear in the diff for context '" . $ctx . "'", + ); + } + } + + #[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/SchemaFilterDbalTest.php b/tests/Functional/SchemaFilterDbalTest.php index 013b53a..a57b91f 100644 --- a/tests/Functional/SchemaFilterDbalTest.php +++ b/tests/Functional/SchemaFilterDbalTest.php @@ -7,7 +7,7 @@ use Doctrine\DBAL\Connection; use Kraz\DoctrineContextBundle\Tests\InspectsSqliteDatabasesTrait; use Kraz\DoctrineContextBundle\Tests\RunsConsoleCommandsTrait; -use Kraz\DoctrineContextBundle\Tests\TestKernelDbal; +use Kraz\DoctrineContextBundle\Tests\TestKernelMigrationsOnly; use Override; use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; @@ -22,7 +22,7 @@ class SchemaFilterDbalTest extends KernelTestCase #[Override] protected static function getKernelClass(): string { - return TestKernelDbal::class; + return TestKernelMigrationsOnly::class; } #[Override] diff --git a/tests/Functional/SchemaFilterTest.php b/tests/Functional/SchemaFilterTest.php index ea9aa4f..31cb8bf 100644 --- a/tests/Functional/SchemaFilterTest.php +++ b/tests/Functional/SchemaFilterTest.php @@ -5,7 +5,6 @@ namespace Kraz\DoctrineContextBundle\Tests\Functional; use Doctrine\DBAL\Connection; -use Doctrine\ORM\EntityManagerInterface; use Doctrine\Persistence\ManagerRegistry; use Kraz\DoctrineContextBundle\Tests\InspectsSqliteDatabasesTrait; use Kraz\DoctrineContextBundle\Tests\RunsConsoleCommandsTrait; @@ -14,8 +13,6 @@ use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; -use function interface_exists; - class SchemaFilterTest extends KernelTestCase { use RunsConsoleCommandsTrait; @@ -30,10 +27,6 @@ protected static function getKernelClass(): string #[Override] protected function setUp(): void { - if (! interface_exists(EntityManagerInterface::class)) { - $this->markTestSkipped('doctrine/orm is not installed'); - } - $this->cleanDatabases(); $kernel = self::bootKernel(); diff --git a/tests/Functional/SchemaValidateTest.php b/tests/Functional/SchemaValidateTest.php index fe08f90..5b39d62 100644 --- a/tests/Functional/SchemaValidateTest.php +++ b/tests/Functional/SchemaValidateTest.php @@ -4,15 +4,12 @@ namespace Kraz\DoctrineContextBundle\Tests\Functional; -use Doctrine\ORM\EntityManagerInterface; use Kraz\DoctrineContextBundle\Tests\RunsConsoleCommandsTrait; -use Kraz\DoctrineContextBundle\Tests\TestKernel; +use Kraz\DoctrineContextBundle\Tests\TestKernelOrmOnly; use Override; use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; -use function interface_exists; - class SchemaValidateTest extends KernelTestCase { use RunsConsoleCommandsTrait; @@ -20,16 +17,12 @@ class SchemaValidateTest extends KernelTestCase #[Override] protected static function getKernelClass(): string { - return TestKernel::class; + return TestKernelOrmOnly::class; } #[Override] protected function setUp(): void { - if (! interface_exists(EntityManagerInterface::class)) { - $this->markTestSkipped('doctrine/orm is not installed'); - } - $this->cleanDatabases(); $kernel = self::bootKernel(); @@ -45,6 +38,11 @@ protected function tearDown(): void $this->cleanDatabases(); } + protected function databaseFilePrefix(): string + { + return 'doctrine_context_test_ormonly_'; + } + public function testHelpListsStandardApplicationOptions(): void { $output = $this->captureOutput('doctrine:schema:validate --help'); diff --git a/tests/TestKernelDbalOnly.php b/tests/TestKernelDbalOnly.php new file mode 100644 index 0000000..76291e8 --- /dev/null +++ b/tests/TestKernelDbalOnly.php @@ -0,0 +1,86 @@ + */ + #[Override] + public function registerBundles(): iterable + { + return [ + new FrameworkBundle(), + new DoctrineBundle(), + new DoctrineContextBundle(), + ]; + } + + #[Override] + public function process(ContainerBuilder $container): void + { + } + + #[Override] + public function registerContainerConfiguration(LoaderInterface $loader): void + { + $loader->load(static function (ContainerBuilder $container): void { + $container->loadFromExtension('framework', [ + 'secret' => 'test', + 'test' => true, + 'http_method_override' => false, + ]); + + $container->loadFromExtension('doctrine', [ + 'dbal' => [ + 'default_connection' => 'default', + 'connections' => [ + 'default' => [ + 'driver' => 'pdo_sqlite', + 'path' => sys_get_temp_dir() . '/doctrine_context_test_dbalonly_default.db', + ], + 'alpha' => [ + 'driver' => 'pdo_sqlite', + 'path' => sys_get_temp_dir() . '/doctrine_context_test_dbalonly_alpha.db', + ], + 'beta' => [ + 'driver' => 'pdo_sqlite', + 'path' => sys_get_temp_dir() . '/doctrine_context_test_dbalonly_beta.db', + ], + ], + ], + ]); + + $container->loadFromExtension('doctrine_context', [ + 'connections' => [ + 'default' => [], + 'alpha' => [], + 'beta' => [], + ], + ]); + }); + } + + #[Override] + public function getProjectDir(): string + { + return dirname(__DIR__); + } +} diff --git a/tests/TestKernelDbal.php b/tests/TestKernelMigrationsOnly.php similarity index 98% rename from tests/TestKernelDbal.php rename to tests/TestKernelMigrationsOnly.php index 9524532..0933d82 100644 --- a/tests/TestKernelDbal.php +++ b/tests/TestKernelMigrationsOnly.php @@ -19,7 +19,7 @@ use function dirname; use function sys_get_temp_dir; -class TestKernelDbal extends Kernel implements CompilerPassInterface +class TestKernelMigrationsOnly extends Kernel implements CompilerPassInterface { use MicroKernelTrait; diff --git a/tests/TestKernelOrmOnly.php b/tests/TestKernelOrmOnly.php new file mode 100644 index 0000000..bc64cbe --- /dev/null +++ b/tests/TestKernelOrmOnly.php @@ -0,0 +1,124 @@ + */ + #[Override] + public function registerBundles(): iterable + { + return [ + new FrameworkBundle(), + new DoctrineBundle(), + new DoctrineContextBundle(), + ]; + } + + #[Override] + public function process(ContainerBuilder $container): void + { + } + + #[Override] + public function registerContainerConfiguration(LoaderInterface $loader): void + { + $loader->load(static function (ContainerBuilder $container): void { + $container->loadFromExtension('framework', [ + 'secret' => 'test', + 'test' => true, + 'http_method_override' => false, + ]); + + $container->loadFromExtension('doctrine', [ + 'dbal' => [ + 'default_connection' => 'default', + 'connections' => [ + 'default' => [ + 'driver' => 'pdo_sqlite', + 'path' => sys_get_temp_dir() . '/doctrine_context_test_ormonly_default.db', + ], + 'alpha' => [ + 'driver' => 'pdo_sqlite', + 'path' => sys_get_temp_dir() . '/doctrine_context_test_ormonly_alpha.db', + ], + 'beta' => [ + 'driver' => 'pdo_sqlite', + 'path' => sys_get_temp_dir() . '/doctrine_context_test_ormonly_beta.db', + ], + ], + ], + 'orm' => [ + 'default_entity_manager' => 'default', + 'entity_managers' => [ + 'default' => [ + 'connection' => 'default', + 'mappings' => [ + 'Default' => [ + 'is_bundle' => false, + 'type' => 'attribute', + 'dir' => __DIR__ . '/Fixtures/Entity/Default', + 'prefix' => 'Kraz\DoctrineContextBundle\Tests\Fixtures\Entity\Default', + ], + ], + ], + 'alpha' => [ + 'connection' => 'alpha', + 'mappings' => [ + 'ContextA' => [ + 'is_bundle' => false, + 'type' => 'attribute', + 'dir' => __DIR__ . '/Fixtures/Entity/ContextA', + 'prefix' => 'Kraz\DoctrineContextBundle\Tests\Fixtures\Entity\ContextA', + ], + ], + ], + 'beta' => [ + 'connection' => 'beta', + 'mappings' => [ + 'ContextB' => [ + 'is_bundle' => false, + 'type' => 'attribute', + 'dir' => __DIR__ . '/Fixtures/Entity/ContextB', + 'prefix' => 'Kraz\DoctrineContextBundle\Tests\Fixtures\Entity\ContextB', + ], + ], + ], + ], + ], + ]); + + $container->loadFromExtension('doctrine_context', [ + 'entity_managers' => [ + 'default' => [], + 'alpha' => [], + 'beta' => [], + ], + ]); + }); + } + + #[Override] + public function getProjectDir(): string + { + return dirname(__DIR__); + } +} diff --git a/tests/Unit/ConfigurationTest.php b/tests/Unit/ConfigurationTest.php index 3950f08..b5ff19a 100644 --- a/tests/Unit/ConfigurationTest.php +++ b/tests/Unit/ConfigurationTest.php @@ -47,4 +47,53 @@ public function testFindNonexistentReturnsNull(): void self::assertNull($configuration->findDependencyFactory('nonexistent')); } + + public function testRegisterContextTracksNameWithoutDependencyFactory(): void + { + $configuration = new Configuration(); + $configuration->registerContext('alpha', false); + $configuration->registerContext('beta', true); + + self::assertSame(['alpha', 'beta'], $configuration->getContextNames()); + self::assertEmpty($configuration->getDependencyFactories()); + } + + public function testIsEntityManagerReturnsTrueForEntityManagerContext(): void + { + $configuration = new Configuration(); + $configuration->registerContext('alpha', true); + $configuration->registerContext('beta', false); + + self::assertTrue($configuration->isEntityManager('alpha')); + self::assertFalse($configuration->isEntityManager('beta')); + } + + public function testIsEntityManagerReturnsFalseForUnknownContext(): void + { + $configuration = new Configuration(); + + self::assertFalse($configuration->isEntityManager('unknown')); + } + + public function testAddDependencyFactoryAlsoRegistersContext(): void + { + $configuration = new Configuration(); + $factory = $this->createStub(DependencyFactory::class); + + $configuration->addDependencyFactory('alpha', $factory); + + self::assertSame(['alpha'], $configuration->getContextNames()); + } + + public function testGetContextNamesIncludesContextsRegisteredWithoutDependencyFactory(): void + { + $configuration = new Configuration(); + $factory = $this->createStub(DependencyFactory::class); + + $configuration->registerContext('alpha', false); + $configuration->addDependencyFactory('beta', $factory); + + self::assertSame(['alpha', 'beta'], $configuration->getContextNames()); + self::assertSame(['beta' => $factory], $configuration->getDependencyFactories()); + } }