Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ 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.
- **ORM command integration** *(requires `doctrine/orm`)*: `doctrine:schema:validate` and `doctrine:mapping:info` receive the same fan-out behaviour.
- **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.

Expand Down Expand Up @@ -170,6 +171,17 @@ By default, a failure in one context stops execution when executed in non-intera
php bin/console doctrine:migrations:migrate --no-interaction --ctx-isolation
```

### Create databases

```bash
# All contexts
php bin/console doctrine:database:create

# Specific context – both flags are equivalent
php bin/console doctrine:database:create --connection=shop
php bin/console doctrine:database:create --conn=shop
```

### All supported commands

Every `doctrine:migrations:*` command supports the context options:
Expand All @@ -190,10 +202,17 @@ 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:

| Command | Description |
|----------------------------|----------------------------------------------|
| `doctrine:schema:create` | Create schema across all entity managers |
| `doctrine:schema:validate` | Validate schema across all entity managers |
| `doctrine:mapping:info` | Show mapping info across all entity managers |

Expand Down
10 changes: 10 additions & 0 deletions config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

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;
Expand All @@ -20,6 +21,7 @@
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;

return static function (ContainerConfigurator $container): void {
$container->services()
Expand All @@ -33,6 +35,14 @@
->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([
Expand Down
9 changes: 9 additions & 0 deletions config/services_orm.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use Kraz\DoctrineContextBundle\Command\Doctrine\Mapping\InfoCommand;
use Kraz\DoctrineContextBundle\Command\Doctrine\Schema\CreateSchemaCommand;
use Kraz\DoctrineContextBundle\Command\Doctrine\Schema\ValidateSchemaCommand;
use Symfony\Component\DependencyInjection\ContainerInterface;

Expand All @@ -18,6 +19,14 @@
])
->tag('console.command', ['command' => 'doctrine:mapping:info'])

->set('doctrine.schema_create_command.with_context', CreateSchemaCommand::class)
->decorate('doctrine.schema_create_command', null, 0, ContainerInterface::IGNORE_ON_INVALID_REFERENCE)
->args([
service('.inner'),
service('doctrine.doctrine_context.configuration'),
])
->tag('console.command', ['command' => 'doctrine:schema:create'])

->set('doctrine.schema_validate_command.with_context', ValidateSchemaCommand::class)
->decorate('doctrine.schema_validate_command', null, 0, ContainerInterface::IGNORE_ON_INVALID_REFERENCE)
->args([
Expand Down
41 changes: 41 additions & 0 deletions src/Command/Doctrine/Database/CreateDatabaseCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace Kraz\DoctrineContextBundle\Command\Doctrine\Database;

use Doctrine\Bundle\DoctrineBundle\Command\CreateDatabaseDoctrineCommand;
use Kraz\DoctrineContextBundle\Command\Doctrine\DoctrineContextTrait;
use Kraz\DoctrineContextBundle\Configuration\Configuration;
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,
) {
$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).');
}

#[Override]
protected function execute(InputInterface $input, OutputInterface $output): int
{
return $this->runAs($this->command, $input, $output);
}
}
29 changes: 28 additions & 1 deletion src/Command/Doctrine/DoctrineContextTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Throwable;

use function array_filter;
use function array_replace;
Expand Down Expand Up @@ -77,6 +78,26 @@ private function runAs(Command $command, InputInterface $input, OutputInterface
}, $list, $input, $output);
}

if ($command->getNativeDefinition()->hasOption('connection')) {
$connectionOption = trim((string) $input->getOption('connection'));
$connOption = trim((string) $input->getOption('conn'));

if ($connectionOption !== '' && $connOption !== '') {
throw new InvalidArgumentException('You can specify only one of the --connection and --conn options.');
}

$targetConnection = $connectionOption ?: ($connOption ?: null);
$list = $this->filterDoctrineContexts(null, null, $targetConnection);

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

Expand All @@ -94,7 +115,13 @@ private function walkDoctrineContexts(callable $callback, array $list, InputInte
$ui->section(sprintf('%s: %s', $dependencyFactory->hasEntityManager() ? 'Entity Manager' : 'Connection', $contextName));
}

$cmdResult = $callback($input, $output, $contextName, $dependencyFactory);
try {
$cmdResult = $callback($input, $output, $contextName, $dependencyFactory);
} catch (Throwable $e) {
$ui->error($e->getMessage());
$cmdResult = Command::FAILURE;
}

if (count($all) > 1) {
$ui->newLine();
}
Expand Down
42 changes: 42 additions & 0 deletions src/Command/Doctrine/Schema/CreateSchemaCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

declare(strict_types=1);

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 Override;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class CreateSchemaCommand extends Command
{
use DoctrineContextTrait;

public function __construct(
private readonly CreateCommand $command,
Configuration $configuration,
EntityManagerProvider|null $entityManagerProvider = null,
) {
$this->configuration = $configuration;
$this->entityManagerProvider = $entityManagerProvider;

parent::__construct($this->command->getName(), $this->command->getCode());
}

#[Override]
protected function configure(): void
{
$this->configureAs($this->command);
}

#[Override]
protected function execute(InputInterface $input, OutputInterface $output): int
{
return $this->runAs($this->command, $input, $output);
}
}
94 changes: 94 additions & 0 deletions tests/Functional/DatabaseCreateTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php

declare(strict_types=1);

namespace Kraz\DoctrineContextBundle\Tests\Functional;

use Kraz\DoctrineContextBundle\Command\Doctrine\Database\CreateDatabaseCommand;
use Kraz\DoctrineContextBundle\Tests\RunsConsoleCommandsTrait;
use Kraz\DoctrineContextBundle\Tests\TestKernel;
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 DatabaseCreateTest extends KernelTestCase
{
use RunsConsoleCommandsTrait;

#[Override]
protected static function getKernelClass(): string
{
return TestKernel::class;
}

#[Override]
protected function setUp(): void
{
$this->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');

// No section header is emitted for a single context, so the connection name does not
// appear in the output — but untargeted connections must not be processed at all.
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');

// No section header is emitted for a single context, so the connection name does not
// appear in the output — but untargeted connections must not be processed at all.
self::assertStringNotContainsString('default', $output, 'Output should not mention untargeted connections');
self::assertStringNotContainsString('beta', $output, 'Output should not mention untargeted connections');
}

public function testDatabaseCreateAllContextsFansOutAcrossAllRegisteredContexts(): void
{
// SQLite does not support createDatabase, so every context fails.
// --ctx-isolation keeps the loop going after each failure instead of breaking.
// --no-interaction suppresses the interactive "continue?" prompt that would
// otherwise block waiting on stdin (walkDoctrineContexts asks before --ctx-isolation
// is consulted as the fallback).
$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');
}

public function testDatabaseCreateFailsWhenBothConnectionAndConnAreSpecified(): void
{
// Run through the command directly (bypassing Application) so that Symfony's
// ConsoleEvents::ERROR listener never fires and no output is printed to stdout.
$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 and --conn options.');

$input = new ArrayInput(['--connection' => 'alpha', '--conn' => 'alpha'], $command->getDefinition());
$command->run($input, new BufferedOutput());
}
}
Loading
Loading