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
- PHP Manual - PDO: https://www.php.net/manual/it/book.pdo.php
- PHP Manual - fgetcsv(): https://www.php.net/manual/it/function.fgetcsv.php
- PHP Manual - fopen(): https://www.php.net/manual/it/function.fopen.php
- MySQL Documentation - INSERT ... ON DUPLICATE KEY UPDATE: https://dev.mysql.com/doc/refman/8.0/en/insert-on-duplicate.html
- MariaDB Knowledge Base - INSERT ON DUPLICATE KEY UPDATE: https://mariadb.com/kb/en/insert-on-duplicate-key-update/
