PHP

Import di massa da CSV in PHP: transazioni, mapping e gestione errori

calendar_today personTeam EGSOFT schedule5 min di lettura
info

Disclaimer: le informazioni pubblicate in questa sezione hanno finalità di pura divulgazione tecnica. EG Software S.r.l. non si assume alcuna responsabilità per un utilizzo improprio dei contenuti, né per eventuali danni diretti o indiretti derivanti dalla loro applicazione, e non garantisce l'aggiornamento, l'accuratezza o la completezza delle informazioni riportate. Prima di utilizzare in produzione codice o procedure qui descritte, verificane sempre l'adeguatezza al proprio contesto.

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.

Altri articoli su PHP grid_viewTutte le tematiche
Scrivici su WhatsApp