vendredi 25 septembre 2026

À propos

Contact

Extensions

Traiter 50 000 éléments sans timeout : découpage en lots et reprise

Un import massif ou un recalcul qui dépasse le temps d'exécution PHP échoue en silence. Découpez en lots, mémorisez la progression et reprenez après erreur.

Par Clément Hadrot • 4 octobre 2023 • 5 min de lecture • Aucun commentaire
Traiter 50 000 éléments sans timeout : découpage en lots et reprise

Une agence cliente nous a transmis un fichier CSV de 50 000 références produit à importer dans un catalogue WooCommerce, avec recalcul de prix HT/TTC et rattachement à des catégories. Le premier essai, un bouton « Importer » qui lançait tout le traitement dans une seule requête HTTP, s’arrêtait invariablement autour de la douze-millième ligne : le serveur mutualisé du client limitait le temps d’exécution PHP à 60 secondes, sans message d’erreur explicite côté navigateur, juste une page blanche.

Ce genre de traitement massif — import, recalcul, migration de données — ne doit jamais reposer sur une exécution unique et continue. La bonne approche consiste à découper le travail en lots de taille raisonnable, à mémoriser où l’on en est, et à permettre une reprise propre si le traitement est interrompu, qu’il s’agisse d’un timeout, d’une erreur PHP ou d’une simple fermeture d’onglet.

Étape 1 : définir la taille de lot et le stockage de progression

On commence par choisir une taille de lot raisonnable — 500 éléments dans notre cas, ajustée après quelques tests de charge — et un endroit où stocker l’avancement. Une option WordPress classique suffit pour un traitement mono-site :

L'essentiel à retenir : Un lot de taille fixe traité par requête, jamais tout en une passe ; La progression est stockée pour permettre une reprise après coupure ; Une commande WP-CLI expose le même traitement sans dépendre du navigateur
function mon_extension_get_progression( $id_traitement ) {
    return get_option( 'mon_extension_progression_' . $id_traitement, array(
        'ligne_courante' => 0,
        'total'          => 0,
        'termine'        => false,
        'erreurs'        => array(),
    ) );
}

function mon_extension_set_progression( $id_traitement, $progression ) {
    update_option( 'mon_extension_progression_' . $id_traitement, $progression, false );
}

Le troisième argument false de update_option() désactive l’autoload pour cette option : elle est mise à jour fréquemment et n’a aucune raison d’être chargée sur chaque page du site.

Étape 2 : traiter un lot et rendre la main

La fonction de traitement lit la progression, traite un nombre limité de lignes, met à jour la progression, puis s’arrête — elle ne boucle jamais sur l’intégralité du fichier en une seule invocation.

function mon_extension_traiter_lot( $id_traitement, $lignes, $taille_lot = 500 ) {
    $progression = mon_extension_get_progression( $id_traitement );
    $depart      = $progression['ligne_courante'];
    $fin         = min( $depart + $taille_lot, count( $lignes ) );

    for ( $i = $depart; $i < $fin; $i++ ) {
        try {
            mon_extension_importer_une_ligne( $lignes[ $i ] );
        } catch ( \Throwable $e ) {
            $progression['erreurs'][] = array(
                'ligne'   => $i,
                'message' => $e->getMessage(),
            );
        }
    }

    $progression['ligne_courante'] = $fin;
    $progression['total']          = count( $lignes );
    $progression['termine']        = ( $fin >= count( $lignes ) );

    mon_extension_set_progression( $id_traitement, $progression );

    return $progression;
}

Le bloc try/catch autour de chaque ligne est essentiel : une seule ligne malformée dans un fichier de 50 000 ne doit jamais interrompre tout l’import. Les erreurs sont collectées et présentées à la fin, pas levées en cours de route.

Étape 3 : enchaîner les lots depuis l’admin

Côté navigateur, un script déclenche des appels successifs vers un endpoint REST, chacun traitant un lot, jusqu’à ce que termine devienne vrai. Chaque requête reste courte (quelques secondes pour 500 lignes), donc bien en dessous de n’importe quelle limite de temps d’exécution serveur :

async function lancerImport(idTraitement) {
    let termine = false;
    while (!termine) {
        const reponse = await fetch(`/wp-json/mon-extension/v1/import/${idTraitement}/lot`, {
            method: 'POST',
            headers: { 'X-WP-Nonce': monExtensionData.nonce },
        });
        const donnees = await reponse.json();
        afficherProgression(donnees.ligne_courante, donnees.total);
        termine = donnees.termine;
    }
}

Étape 4 : reprendre après une coupure

Parce que la progression est stockée en base et non en mémoire, une coupure — fermeture d’onglet, erreur réseau, redémarrage du serveur — n’oblige jamais à recommencer depuis zéro. Relancer le même identifiant de traitement reprend exactement où il s’était arrêté, puisque ligne_courante est relu depuis l’option avant chaque lot.

Exposer le même traitement en WP-CLI

Pour les gros volumes, dépendre d’un navigateur ouvert reste fragile. Une commande WP-CLI permet de lancer l’import depuis le serveur, dans une session screen ou tmux, indépendamment de toute connexion HTTP :

class Mon_Extension_CLI {
    /**
     * Importe un fichier CSV de produits par lots.
     *
     * [--fichier=<chemin>]
     * : Chemin du fichier CSV à importer.
     *
     * [--taille-lot=<nombre>]
     * : Nombre de lignes par lot. Par défaut 500.
     */
    public function importer( $args, $assoc_args ) {
        $fichier    = $assoc_args['fichier'];
        $taille_lot = (int) ( $assoc_args['taille-lot'] ?? 500 );
        $lignes     = mon_extension_lire_csv( $fichier );
        $id         = 'wpcli_' . md5( $fichier );

        $barre = \WP_CLI\Utils\make_progress_bar( 'Import produits', count( $lignes ) );

        do {
            $progression = mon_extension_traiter_lot( $id, $lignes, $taille_lot );
            $barre->tick( $taille_lot );
        } while ( ! $progression['termine'] );

        $barre->finish();
        \WP_CLI::success( sprintf( '%d lignes importées, %d erreurs.', count( $lignes ), count( $progression['erreurs'] ) ) );
    }
}
\WP_CLI::add_command( 'mon-extension importer', array( new Mon_Extension_CLI(), 'importer' ) );

La commande s’appelle ensuite simplement avec wp mon-extension importer --fichier=produits.csv --taille-lot=1000, avec une barre de progression native et un code de sortie exploitable dans un script de déploiement.

En résumé

Aucun traitement massif ne devrait tenir dans une seule requête, quelle que soit la puissance du serveur : les limites de temps d’exécution, de mémoire, ou simplement les aléas réseau finissent toujours par se manifester sur un volume suffisant. Découper en lots de taille fixe, stocker la progression en base plutôt qu’en mémoire, et exposer le même traitement en WP-CLI pour les gros volumes : ces trois habitudes transforment un import fragile en un traitement que l’on peut interrompre et reprendre sans perte, quelle que soit la cause de l’interruption.

Partager :

À propos de l'auteur

Clément Hadrot

Développeur WordPress, passionné par Elementor, le FSE et l’automatisation par IA.

Voir tous ses articles

Dans la même veine

À lire aussi