PHP e MySQL/MariaDB: importazione sicura di dati da file CSV

by theArchitect
SHARE
PHP e MySQL/MariaDB: importazione sicura di dati da file CSV
© Guida-HTML5.it

Introduzione

Quando si lavora con PHP e MySQL/MariaDB, uno dei compiti più utili e frequenti è importare dati da un file CSV in una tabella del database. Questo scenario è comune in applicazioni gestionali, e-commerce, CRM e strumenti di amministrazione: pensiamo all’importazione di prodotti, utenti, ordini, listini o anagrafiche fornite da altri sistemi.

Questo tutorial si concentra su un aspetto pratico spesso trascurato: come importare un CSV in modo sicuro, controllato e robusto, evitando errori comuni come duplicati, dati malformati e problemi di encoding. Useremo PDO con query preparate, così da mantenere il codice pulito e protetto.

L’obiettivo non è solo “caricare righe nel database”, ma farlo con un approccio professionale: validazione dei dati, gestione delle intestazioni, conversione dei formati e inserimento efficiente.

Codice completo

<?php
declare(strict_types=1);

/**
 * Importazione di un file CSV nella tabella products.
 * Colonne attese nel CSV:
 * sku,name,price,stock
 */

$dsn = ´mysql:host=localhost;dbname=shop;charset=utf8mb4´;
$user = ´root´;
$password = ´´;

try {
    $pdo = new PDO($dsn, $user, $password, [
        PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE  => PDO::FETCH_ASSOC,
        PDO::ATTR_EMULATE_PREPARES    => false,
    ]);

    // Percorso del file CSV
    $csvFile = __DIR__ . ´/import/products.csv´;

    if (!is_readable($csvFile)) {
        throw new RuntimeException(´Il file CSV non è leggibile o non esiste.´);
    }

    $file = fopen($csvFile, ´r´);
    if ($file === false) {
        throw new RuntimeException(´Impossibile aprire il file CSV.´);
    }

    // Legge la prima riga come intestazione
    $headers = fgetcsv($file, 0, ´,´);
    if ($headers === false) {
        throw new RuntimeException(´Il CSV è vuoto o non contiene intestazioni.´);
    }

    // Normalizza le intestazioni
    $headers = array_map(´trim´, $headers);

    $requiredColumns = [´sku´, ´name´, ´price´, ´stock´];
    $missingColumns = array_diff($requiredColumns, $headers);

    if (!empty($missingColumns)) {
        throw new RuntimeException(
            ´Mancano colonne obbligatorie nel CSV: ´ . implode(´, ´, $missingColumns)
        );
    }

    // Mappa le colonne per accedere ai valori in modo affidabile
    $headerMap = array_flip($headers);

    $pdo->beginTransaction();

    $sql = "
        INSERT INTO products (sku, name, price, stock)
        VALUES (:sku, :name, :price, :stock)
        ON DUPLICATE KEY UPDATE
            name = VALUES(name),
            price = VALUES(price),
            stock = VALUES(stock)
    ";

    $stmt = $pdo->prepare($sql);

    $imported = 0;
    $skipped = 0;
    $lineNumber = 1; // la riga 1 è l´intestazione

    while (($row = fgetcsv($file, 0, ´,´)) !== false) {
        $lineNumber++;

        // Salta righe vuote
        if ($row === [null] || count(array_filter($row, fn($v) => trim((string)$v) !== ´´)) === 0) {
            $skipped++;
            continue;
        }

        // Verifica che la riga abbia abbastanza colonne
        if (count($row) < count($headers)) {
            $skipped++;
            continue;
        }

        $sku   = trim((string)$row[$headerMap[´sku´]]);
        $name  = trim((string)$row[$headerMap[´name´]]);
        $price = trim((string)$row[$headerMap[´price´]]);
        $stock = trim((string)$row[$headerMap[´stock´]]);

        // Validazione minima
        if ($sku === ´´ || $name === ´´) {
            $skipped++;
            continue;
        }

        if (!is_numeric($price) || !is_numeric($stock)) {
            $skipped++;
            continue;
        }

        $stmt->execute([
            ´:sku´   => $sku,
            ´:name´  => $name,
            ´:price´ => (float)$price,
            ´:stock´ => (int)$stock,
        ]);

        $imported++;
    }

    fclose($file);
    $pdo->commit();

    echo "Importazione completata. Righe importate: {$imported}, righe saltate: {$skipped}";

} catch (Throwable $e) {
    if (isset($pdo) && $pdo->inTransaction()) {
        $pdo->rollBack();
    }

    if (isset($file) && is_resource($file)) {
        fclose($file);
    }

    echo ´Errore durante l´importazione: ´ . $e->getMessage();
}

Spiegazione

Vediamo il codice passo per passo, perché il vero valore di un’importazione CSV sta nei dettagli.

1. Connessione a MySQL/MariaDB con PDO

Usiamo una connessione PDO con charset utf8mb4, così da gestire correttamente anche caratteri speciali, emoji e testi internazionali. Inoltre impostiamo:

  • PDO::ERRMODE_EXCEPTION per trasformare gli errori in eccezioni;
  • PDO::ATTR_EMULATE_PREPARES = false per usare prepared statements reali;
  • PDO::FETCH_ASSOC come modalità predefinita di fetch, utile in altri contesti.

2. Controllo del file CSV

Prima di aprire il file, verifichiamo che sia leggibile. È una buona abitudine: evita warning inutili e ci permette di mostrare messaggi chiari in caso di problemi.

3. Lettura dell’intestazione

La prima riga del CSV viene letta come intestazione. Questo approccio è molto più robusto rispetto all’uso di colonne “fisse” senza nomi, perché rende l’importazione più resistente a cambiamenti di formato.

Con array_flip() costruiamo una mappa delle colonne, così possiamo recuperare i valori tramite il nome del campo, non tramite la posizione assoluta.

4. Validazione delle colonne richieste

Controlliamo che nel file siano presenti le colonne obbligatorie: sku, name, price e stock. Se manca una colonna, interrompiamo l’importazione prima di iniziare. Questo evita import parziali e risultati incoerenti.

5. Transazione per coerenza dei dati

Avviamo una transazione con beginTransaction(). In questo modo, se qualcosa va storto durante l’importazione, possiamo fare un rollBack() e annullare tutte le modifiche. È fondamentale quando i dati devono restare consistenti.

6. Query con ON DUPLICATE KEY UPDATE

La query usa ON DUPLICATE KEY UPDATE, una funzionalità di MySQL/MariaDB utile quando sku è una chiave univoca. In pratica:

  • se il prodotto non esiste, viene inserito;
  • se esiste già, vengono aggiornati nome, prezzo e stock.

Questo comportamento è perfetto per importazioni periodiche o sincronizzazioni con sistemi esterni.

7. Validazione riga per riga

Non basta che il file sia corretto: anche ogni riga deve essere valida. Per questo controlliamo:

  • righe vuote;
  • numero insufficiente di colonne;
  • campi obbligatori mancanti;
  • valori numerici non validi per prezzo e stock.

Le righe non valide vengono saltate e conteggiate, senza interrompere l’intero processo. In un contesto reale, potresti anche salvarle in un file di log o in una tabella di errori.

8. Query preparate

Usiamo prepared statements con parametri nominati. Questo è importante sia per la sicurezza sia per la leggibilità del codice. Anche se nel caso del CSV il rischio di SQL injection è minore rispetto a input web diretti, è comunque una best practice imprescindibile.

Best practice

Importare dati da CSV sembra semplice, ma ci sono alcune regole che fanno davvero la differenza in produzione.

  • Usa sempre UTF-8: imposta il charset del database e assicurati che il CSV sia codificato correttamente.
  • Valida i dati prima dell’inserimento: non fidarti mai del file in ingresso, anche se proviene da un sistema “affidabile”.
  • Preferisci intestazioni esplicite: rendono il CSV più manutenibile e riducono gli errori.
  • Usa transazioni: soprattutto quando importi molte righe o quando i dati devono rimanere coerenti.
  • Gestisci i duplicati in modo esplicito: scegli se ignorarli, aggiornarli o bloccare l’importazione.
  • Registra le righe scartate: in un progetto reale è utile sapere quali record non sono stati importati e perché.
  • Evita di caricare file enormi in memoria: leggi il CSV riga per riga, come nell’esempio, per mantenere basso il consumo di RAM.

Un’altra buona pratica è separare la logica di importazione in una classe o in un servizio dedicato. In questo modo il codice diventa più testabile e più facile da riutilizzare in script CLI, pannelli admin o job pianificati.

Riepilogo

In questo tutorial abbiamo visto come importare un file CSV in MySQL/MariaDB con PHP in modo professionale. Il punto chiave non è solo leggere il file, ma farlo con attenzione a:

  • connessione sicura al database;
  • lettura dell’intestazione e mappatura delle colonne;
  • validazione dei dati riga per riga;
  • uso di prepared statements;
  • gestione dei duplicati con ON DUPLICATE KEY UPDATE;
  • transazioni per garantire coerenza.

Questo approccio è molto utile in scenari reali, perché riduce errori, semplifica la manutenzione e rende l’importazione affidabile anche con file non perfetti.

Approfondisci con risorse ufficiali

SHARE