OUJOOD.COM
Pourquoi utiliser set_local_infile_handler()
Importer un fichier CSV dans MySQL avec LOAD DATA LOCAL INFILE, c'est rapide. Le problème, c'est quand le fichier contient des lignes incomplètes, des valeurs à transformer, ou des entrées qui n'ont rien à faire dans la base. Sans interception, tout rentre — et nettoyer après coup, sur de gros volumes, peut prendre autant de temps que l'import lui-même.
set_local_infile_handler() permet d'intervenir avant. Elle branche une fonction de rappel (un callback) dans le mécanisme d'import : PHP appelle cette fonction pour chaque ligne lue. Vous décidez ligne par ligne : on insère, ou on ignore. La base ne reçoit que ce que vous avez laissé passer.
La fonction fait partie de l'extension mysqli, disponible sur toutes les versions PHP 8.x actuellement maintenues (PHP 8.1, 8.2, 8.3, 8.4).
Ce que fait set_local_infile_handler()
En usage normal, LOAD DATA LOCAL INFILE lit un fichier et envoie chaque ligne directement à MySQL. Avec set_local_infile_handler(), votre logique de validation s'intercale entre la lecture et l'envoi.
La fonction s'écrit de deux façons :
// Approche procédurale set_local_infile_handler(callable $handler, int $handle = null); // Approche orientée objet $db->set_local_infile_handler(callable $handler, int $handle = null);
Le premier paramètre $handler est la fonction appelée à chaque ligne. Le second, $handle, est facultatif : il sert d'identifiant pour désactiver le callback plus tard via unset_local_infile_handler(). Sans lui, le callback est retiré automatiquement après l'import.
Sur un projet PHP 8 avec un framework comme Laravel ou Symfony, l'approche orientée objet colle mieux à l'organisation du code — mais les deux syntaxes fonctionnent.
La fonction de rappel : comment ça marche
Votre callback reçoit un paramètre : un flux de données ($stream) depuis lequel vous lisez la ligne courante avec $stream->readLine(). Il doit retourner un booléen :
- true → la ligne est transmise à MySQL pour insertion.
- false → la ligne est ignorée, aucune insertion n'a lieu.
Toute la logique de validation — format, champs obligatoires, plages de valeurs — s'écrit dans ce callback, sans modifier le reste du code d'import.
Premiers exemples : voir la mécanique en action
Deux exemples courts avant d'aller plus loin.
Le premier callback affiche chaque ligne lue sans bloquer l'insertion :
<?php // Callback : affiche chaque ligne avant import function afficherLigne($stream) { $ligne = $stream->readLine(); echo $ligne . PHP_EOL; // true = on autorise l'insertion de cette ligne return true; } // Branchement du callback set_local_infile_handler('afficherLigne'); // Déclenchement de l'import $query = "LOAD DATA LOCAL INFILE 'data.txt' INTO TABLE perso"; mysqli_query($link, $query); ?>
Le second vérifie que la ligne est un JSON valide avec les champs attendus. Si ce n'est pas le cas, elle est rejetée sans bruit :
<?php // Callback : valide que la ligne est un JSON avec les champs "name" et "age" function validerJson($stream) { $donnees = json_decode($stream->readLine(), true); // JSON invalide ou champs manquants : on rejette la ligne if (!is_array($donnees) || !isset($donnees['name'], $donnees['age'])) { return false; } return true; } ?>
Cas d'utilisation concrets
Pour les exemples suivants, on travaille sur la base teste avec une table perso (champs : id, nom, prenom, email, age). La connexion est identique pour les deux approches :
<?php
$serveur = 'localhost';
$utilisateur = 'root';
$motDePasse = '';
$ma_base_de_donnees = 'teste';
// Connexion à la base (approche procédurale)
$link = mysqli_connect($serveur, $utilisateur, $motDePasse, $ma_base_de_donnees);
?>
Cas 1 : importer un fichier CSV dans une table MySQL
Situation classique : un fichier data.txt avec des colonnes séparées par des points-virgules, dont la première ligne est l'en-tête. On veut insérer les données dans perso en sautant cette ligne.
Voici à quoi ressemble le fichier :
id;nom;prenom;email;age 1;John;Doe;john.doe@example.com;25 2;Jane;Doe;jane.doe@example.com;23
Le callback lit la ligne, la découpe, vérifie qu'elle contient bien 5 colonnes, puis construit la requête. Un compteur static identifie la première ligne reçue — l'en-tête — et la passe systématiquement.
Approche procédurale
<?php
$link = mysqli_connect($serveur, $utilisateur, $motDePasse, $ma_base_de_donnees);
function importerDonnees($stream) {
global $link;
static $numLigne = 0;
$numLigne++;
// Première ligne = en-tête, on passe
if ($numLigne === 1) return false;
$ligne = $stream->readLine();
$champs = explode(";", trim($ligne));
// On vérifie qu'on a exactement 5 colonnes avant d'insérer
if (count($champs) !== 5) return false;
$query = "INSERT INTO perso (id, nom, prenom, email, age)
VALUES ($champs[0], '$champs[1]', '$champs[2]', '$champs[3]', $champs[4])";
mysqli_query($link, $query);
return true;
}
set_local_infile_handler('importerDonnees');
$query = "LOAD DATA LOCAL INFILE 'data.txt' INTO TABLE perso";
mysqli_query($link, $query);
?>
Approche orientée objet
Même logique, avec l'objet $db pour la connexion et l'appel du callback :
<?php
$db = new mysqli($serveur, $utilisateur, $motDePasse, $ma_base_de_donnees);
function importerDonnees($stream) {
global $db;
static $numLigne = 0;
$numLigne++;
// Ignorer la ligne d'en-tête
if ($numLigne === 1) return false;
$ligne = $stream->readLine();
$champs = explode(";", trim($ligne));
if (count($champs) !== 5) return false;
$query = "INSERT INTO perso (id, nom, prenom, email, age)
VALUES ($champs[0], '$champs[1]', '$champs[2]', '$champs[3]', $champs[4])";
$db->query($query);
return true;
}
// Branchement du callback via la méthode OO
$db->set_local_infile_handler('importerDonnees');
$db->query("LOAD DATA LOCAL INFILE 'data.txt' INTO TABLE perso");
?>
Cas 2 : valider les données avant insertion
Un champ nom vide ou un age absent rend l'entrée inutilisable. Autant la rejeter à l'import plutôt qu'après. Le callback retourne false dès qu'un champ obligatoire manque — MySQL ne voit rien, aucune erreur n'est levée côté base.
Approche procédurale
<?php
$link = mysqli_connect($serveur, $utilisateur, $motDePasse, $ma_base_de_donnees);
function validerDonnees($stream) {
static $numLigne = 0;
$numLigne++;
if ($numLigne === 1) return false; // Ignorer l'en-tête
$ligne = $stream->readLine();
$champs = explode(";", trim($ligne));
// nom (index 1) et age (index 4) sont obligatoires
if (empty($champs[1]) || empty($champs[4])) {
return false;
}
return true;
}
set_local_infile_handler('validerDonnees');
mysqli_query($link, "LOAD DATA LOCAL INFILE 'data.txt' INTO TABLE perso");
?>
Approche orientée objet
<?php
$db = new mysqli($serveur, $utilisateur, $motDePasse, $ma_base_de_donnees);
function validerDonnees($stream) {
static $numLigne = 0;
$numLigne++;
if ($numLigne === 1) return false;
$ligne = $stream->readLine();
$champs = explode(";", trim($ligne));
// Rejet si le nom ou l'âge est vide
if (empty($champs[1]) || empty($champs[4])) {
return false;
}
return true;
}
$db->set_local_infile_handler('validerDonnees');
$db->query("LOAD DATA LOCAL INFILE 'data.txt' INTO TABLE perso");
?>
Bonnes pratiques
- En production, remplacez les insertions directes par des requêtes préparées (
prepare/bind_param) dans le callback. Insérer des valeurs non échappées directement dans la requête ouvre la porte aux injections SQL. - Préférez un compteur static dans le callback pour gérer l'en-tête. Un compteur global peut être modifié par une autre partie du code sans que vous le voyiez.
- Testez toujours sur un petit fichier avant l'import complet. Un callback bogué qui retourne false sur toutes les lignes ne lève aucune erreur MySQL — sans log applicatif, l'import silencieux passe inaperçu.
- Pour des flux continus (APIs, webhooks), cette approche batch n'est pas adaptée. Des outils ETL ou des files de messages (RabbitMQ, Redis Streams) gèrent mieux ces cas.
Pour plus de cas d'utilisation : autres exemples set_local_infile_handler()
Par carabde | Mis à jour le 7 mai 2026