Chi lavora con anagrafiche italiane — comuni, CAP, codici ISTAT, listini fornitori — prima o poi si trova a dover importare un file CSV di partenza in un database. Spesso sono file da decine di migliaia di righe, con encoding e separatori non sempre standard. Vediamo un pattern collaudato per importare CSV di grandi dimensioni in PHP, usando le transazioni di PDO per garantire la coerenza dei dati anche quando qualche riga è malformata.
Leggere il CSV riga per riga senza saturare la memoria
Per file di dimensioni consistenti non ha senso caricare l'intero contenuto in memoria con file() o simili: la funzione fgetcsv() legge una riga alla volta direttamente dal file handle, restituendo già un array di colonne. È importante specificare il separatore corretto — nei file di anagrafiche italiane è spesso il punto e virgola — e saltare la riga di intestazione prima del ciclo principale.
Una transazione per tutto l'import, non una per riga
Racchiudere l'intero import in un'unica transazione (beginTransaction / commit) offre due vantaggi: le prestazioni sono nettamente migliori rispetto a un commit per ogni singola riga, e in caso di errore grave — ad esempio la connessione al database che cade a metà import — è possibile annullare tutto con rollBack() invece di lasciare il database in uno stato parziale e incoerente.
<?php
declare(strict_types=1);
function importaCsv(PDO $pdo, string $percorsoFile): array
{
$handle = fopen($percorsoFile, 'rb');
if ($handle === false) {
throw new RuntimeException('Impossibile aprire il file CSV');
}
// Salta la riga di intestazione
fgetcsv($handle, 1000, ';');
$stmt = $pdo->prepare(
'INSERT INTO comuni (provincia, istat, citta, cap, regione, latitudine, longitudine)
VALUES (:provincia, :istat, :citta, :cap, :regione, :lat, :lon)'
);
$totali = 0;
$importati = 0;
$pdo->beginTransaction();
try {
while (($riga = fgetcsv($handle, 1000, ';')) !== false) {
$totali++;
$parametri = [
':istat' => $riga[0] ?? '',
':citta' => $riga[2] ?? '',
':cap' => $riga[4] ?? '',
':provincia' => $riga[5] ?? '',
':regione' => $riga[9] ?? '',
// il CSV usa la virgola come separatore decimale
':lat' => str_replace(',', '.', $riga[14] ?? '0'),
':lon' => str_replace(',', '.', $riga[15] ?? '0'),
];
try {
$stmt->execute($parametri);
$importati++;
} catch (PDOException $e) {
// riga duplicata o non valida: la registriamo e proseguiamo
error_log('Riga CSV scartata: ' . $e->getMessage());
}
}
$pdo->commit();
} catch (Throwable $e) {
$pdo->rollBack();
throw $e;
} finally {
fclose($handle);
}
return ['totali' => $totali, 'importati' => $importati];
}
Gestire gli errori riga per riga senza fermare l'intero import
Un CSV di provenienza esterna contiene quasi sempre qualche riga problematica: un duplicato che viola un vincolo UNIQUE, un valore fuori formato, un campo mancante. Interrompere l'intero import per una singola riga non valida è quasi sempre la scelta sbagliata: conviene intercettare la PDOException a livello di singola riga, registrarla con error_log() e proseguire, tenendo un contatore delle righe totali e di quelle effettivamente importate da mostrare all'utente a fine processo.
Un dettaglio che si dimentica spesso: i numeri decimali
I file di dati italiani usano frequentemente la virgola come separatore decimale, ad esempio nelle coordinate geografiche. Se questi valori finiscono in colonne numeriche del database, vanno convertiti con str_replace(',', '.', $valore) prima dell'inserimento: altrimenti il driver del database rischia di troncare il valore al primo carattere non numerico, con risultati silenziosamente sbagliati.