fix(tenant): route runtime traffic to tenant databases
Some checks failed
CI / Backend Tests (push) Has been cancelled
CI / Frontend Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Naming Conventions (push) Has been cancelled
CI / Build Check (push) Has been cancelled

Wire Doctrine's default connection to the tenant database resolved from the subdomain for HTTP requests and tenant-scoped Messenger messages while keeping master-only services on the master connection.

This removes the production inconsistency where demo data, migrations and tenant commands used the tenant database but the web runtime still read from master.
This commit is contained in:
2026-03-10 23:38:26 +01:00
parent 0f3e57c6e6
commit 8c70ed1324
16 changed files with 840 additions and 4 deletions

View File

@@ -12,9 +12,11 @@ use App\Administration\Domain\Model\User\User;
use App\Administration\Domain\Model\User\UserId;
use App\Administration\Domain\Repository\UserRepository;
use App\Shared\Domain\Clock;
use App\Shared\Infrastructure\Tenant\TenantConfig;
use App\Shared\Infrastructure\Tenant\TenantNotFoundException;
use App\Shared\Infrastructure\Tenant\TenantRegistry;
use function getenv;
use function sprintf;
use Symfony\Component\Console\Attribute\AsCommand;
@@ -23,6 +25,8 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Process\Process;
/**
* Creates an already-activated test user for E2E login tests.
@@ -41,6 +45,8 @@ final class CreateTestUserCommand extends Command
private readonly PasswordHasher $passwordHasher,
private readonly TenantRegistry $tenantRegistry,
private readonly Clock $clock,
#[Autowire('%kernel.project_dir%')]
private readonly string $projectDir,
) {
parent::__construct();
}
@@ -51,8 +57,11 @@ final class CreateTestUserCommand extends Command
->addOption('email', null, InputOption::VALUE_OPTIONAL, 'Email address', 'e2e-login@example.com')
->addOption('password', null, InputOption::VALUE_OPTIONAL, 'Password (plain text)', 'TestPassword123')
->addOption('role', null, InputOption::VALUE_OPTIONAL, 'User role (PARENT, ELEVE, PROF, ADMIN)', 'PARENT')
->addOption('firstName', null, InputOption::VALUE_OPTIONAL, 'First name', '')
->addOption('lastName', null, InputOption::VALUE_OPTIONAL, 'Last name', '')
->addOption('school', null, InputOption::VALUE_OPTIONAL, 'School name', 'École de Test')
->addOption('tenant', null, InputOption::VALUE_OPTIONAL, 'Tenant subdomain (ecole-alpha, ecole-beta)', 'ecole-alpha');
->addOption('tenant', null, InputOption::VALUE_OPTIONAL, 'Tenant subdomain (ecole-alpha, ecole-beta)', 'ecole-alpha')
->addOption('internal-run', null, InputOption::VALUE_NONE, 'Internal option to create the test user inside the tenant database');
}
protected function execute(InputInterface $input, OutputInterface $output): int
@@ -66,10 +75,15 @@ final class CreateTestUserCommand extends Command
/** @var string $roleOption */
$roleOption = $input->getOption('role');
$roleInput = strtoupper($roleOption);
/** @var string $firstName */
$firstName = $input->getOption('firstName');
/** @var string $lastName */
$lastName = $input->getOption('lastName');
/** @var string $schoolName */
$schoolName = $input->getOption('school');
/** @var string $tenantSubdomain */
$tenantSubdomain = $input->getOption('tenant');
$internalRun = $input->getOption('internal-run');
// Convert short role name to full Symfony role format
$roleName = str_starts_with($roleInput, 'ROLE_') ? $roleInput : 'ROLE_' . $roleInput;
@@ -104,6 +118,19 @@ final class CreateTestUserCommand extends Command
return Command::FAILURE;
}
if (!$internalRun) {
return $this->relaunchAgainstTenantDatabase(
email: $email,
password: $password,
roleName: $roleName,
firstName: $firstName,
lastName: $lastName,
schoolName: $schoolName,
tenantConfig: $tenantConfig,
io: $io,
);
}
$now = $this->clock->now();
// Check if user already exists
@@ -140,6 +167,8 @@ final class CreateTestUserCommand extends Command
hashedPassword: $hashedPassword,
activatedAt: $now,
consentementParental: null,
firstName: $firstName,
lastName: $lastName,
);
$this->userRepository->save($user);
@@ -161,4 +190,52 @@ final class CreateTestUserCommand extends Command
return Command::SUCCESS;
}
private function relaunchAgainstTenantDatabase(
string $email,
string $password,
string $roleName,
string $firstName,
string $lastName,
string $schoolName,
TenantConfig $tenantConfig,
SymfonyStyle $io,
): int {
$process = new Process(
command: [
'php',
'bin/console',
'app:dev:create-test-user',
'--email=' . $email,
'--password=' . $password,
'--role=' . $roleName,
'--firstName=' . $firstName,
'--lastName=' . $lastName,
'--school=' . $schoolName,
'--tenant=' . $tenantConfig->subdomain,
'--internal-run',
],
cwd: $this->projectDir,
env: [
...getenv(),
'DATABASE_URL' => $tenantConfig->databaseUrl,
],
timeout: 300,
);
$process->run(static function (string $type, string $buffer) use ($io): void {
$io->write($buffer);
});
if ($process->isSuccessful()) {
return Command::SUCCESS;
}
$io->error(sprintf(
'Failed to create test user in tenant database "%s".',
$tenantConfig->subdomain,
));
return Command::FAILURE;
}
}