src/Infrastructure/Stocks/StockSyncProcessor.php
php
<?php
declare(strict_types=1);
namespace App\Infrastructure\Stocks;
// AVANT : 35 000 INSERT/UPDATE individuels, transactions longues qui
// verrouillaient la table pendant les heures ouvrées.
//
// APRÈS : pagination par lots de 500, un seul upsert SQL par lot,
// signature de payload pour ignorer les rows inchangées.
final class StockSyncProcessor
{
public function __construct(
private readonly \Doctrine\DBAL\Connection $db,
private readonly StockSourceClient $client,
) {}
public function syncBatch(string $promoterId, int $offset, int $limit = 500): SyncReport
{
$units = $this->client->fetchUnits($promoterId, $offset, $limit);
if ($units === []) {
return SyncReport::empty();
}
// INSERT ... ON CONFLICT DO UPDATE : un seul aller-retour SQL
// pour 500 lignes. Idempotent grâce à la PK (programme, lot).
$sql = <<<SQL
INSERT INTO programme_units (programme_id, lot_id, status, price_cents, signature, synced_at)
VALUES {$this->placeholders(count($units))}
ON CONFLICT (programme_id, lot_id) DO UPDATE SET
status = EXCLUDED.status,
price_cents = EXCLUDED.price_cents,
signature = EXCLUDED.signature,
synced_at = EXCLUDED.synced_at
WHERE programme_units.signature IS DISTINCT FROM EXCLUDED.signature
SQL;
$written = $this->db->executeStatement($sql, $this->flatten($units));
return SyncReport::of(processed: count($units), written: $written);
}
}
Sync quotidienne des stocks : 35 000 lots traités par paquets de 500 via un upsert SQL atomique. Idempotent (relance sans doublons), exécution en arrière-plan via Symfony Messenger.