logo oujood
🔍

set_local_infile_handler() en PHP : contrôler l'import de données MySQL ligne par ligne

Branchez un callback sur LOAD DATA LOCAL INFILE pour filtrer, valider ou transformer chaque ligne avant qu'elle atteigne votre base MySQL. Exemples concrets, approches procédurale et OO.

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 :

  📋 Copier le code

// 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 :

  📋 Copier le code

<?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 :

  📋 Copier le code

<?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 :

  📋 Copier le code

<?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 :

  📋 Copier le code

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

  📋 Copier le code

<?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 :

  📋 Copier le code

<?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

  📋 Copier le code

<?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

  📋 Copier le code

<?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