# 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.

- Auteur : Clément Hadrot
- Publié le : 2023-10-04
- Mis à jour le : 2023-10-04
- Catégorie : Extensions
- URL : https://wpmoderne.dev.wordpress-developpement.fr/extensions/traiter-50000-elements-sans-timeout-lots-reprise/

## L’essentiel

- 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

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.
