feat: Persister les utilisateurs en PostgreSQL avec cache-aside Redis
Les utilisateurs étaient stockés uniquement dans Redis (CacheUserRepository), ce qui exposait à une perte totale des comptes en cas de restart Redis, FLUSHDB ou perte du volume Docker. Les tables student_guardians et teacher_assignments référençaient des user IDs sans FK réelle. PostgreSQL devient la source de vérité via DoctrineUserRepository (DBAL, upsert ON CONFLICT). CachedUserRepository décore l'interface existante avec le pattern cache-aside : lectures Redis d'abord → miss → PostgreSQL → populate Redis ; écritures PostgreSQL d'abord → mise à jour Redis. Si Redis est indisponible, l'application continue via PostgreSQL seul. Une commande de migration (app:migrate-users-to-postgres) permet de copier les données Redis existantes vers PostgreSQL de manière idempotente.
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Infrastructure\Console;
|
||||
|
||||
use App\Administration\Domain\Repository\UserRepository;
|
||||
use App\Shared\Infrastructure\Tenant\TenantRegistry;
|
||||
|
||||
use function count;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
/**
|
||||
* Migrates existing users from Redis cache to PostgreSQL.
|
||||
*
|
||||
* This command reads all users stored in Redis (via CacheUserRepository)
|
||||
* and persists them to PostgreSQL (via DoctrineUserRepository).
|
||||
* It is idempotent: re-running it will upsert existing records.
|
||||
*/
|
||||
#[AsCommand(
|
||||
name: 'app:migrate-users-to-postgres',
|
||||
description: 'Migrate existing users from Redis cache to PostgreSQL',
|
||||
)]
|
||||
final class MigrateUsersToPostgresCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
private readonly UserRepository $source,
|
||||
private readonly UserRepository $target,
|
||||
private readonly TenantRegistry $tenantRegistry,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$io->title('Migration des utilisateurs Redis → PostgreSQL');
|
||||
|
||||
$tenants = $this->tenantRegistry->getAllConfigs();
|
||||
$totalMigrated = 0;
|
||||
|
||||
foreach ($tenants as $tenant) {
|
||||
$tenantId = $tenant->tenantId;
|
||||
$users = $this->source->findAllByTenant($tenantId);
|
||||
|
||||
if ($users === []) {
|
||||
$io->text("Tenant {$tenant->subdomain}: aucun utilisateur");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($users as $user) {
|
||||
$this->target->save($user);
|
||||
++$totalMigrated;
|
||||
}
|
||||
|
||||
$io->text("Tenant {$tenant->subdomain}: " . count($users) . ' utilisateur(s) migré(s)');
|
||||
}
|
||||
|
||||
$io->success("Migration terminée : {$totalMigrated} utilisateur(s) migré(s) vers PostgreSQL");
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user