WP-CLI : migrer des milliers de produits WooCommerce sans timeout
Le problème : 504 Gateway Timeout
Vous avez 50 000 produits WooCommerce et vous devez mettre à jour un champ meta sur chacun d’eux. Vous écrivez un script PHP, vous le lancez depuis le navigateur, et au bout de 30 secondes : 504 Gateway Timeout.
C’est un classique. Le serveur web (Nginx, Apache) coupe la connexion avant que le script ait fini. Augmenter le max_execution_time PHP ne fait que retarder le problème — le vrai souci, c’est que le navigateur n’est pas fait pour ça.
Mike Davey l’explique bien dans son article sur Delicious Brains (source : deliciousbrains.com) : la combinaison des timeouts serveur, des limites PHP-FPM et de l’explosion mémoire rend le navigateur impraticable pour les opérations massives.
La solution : WP-CLI. Pas de serveur web, pas de timeout, pas de limite de temps. Juste PHP en ligne de commande, avec un accès direct à toute l’API WordPress.
Le piège mémoire : get_posts() avec -1
Le premier réflexe quand on veut traiter tous les produits, c’est d’écrire :
$products = get_posts( [
'post_type' => 'product',
'posts_per_page' => -1,
] );
Ça charge tous les objets en mémoire d’un coup. Avec 50 000 produits, vous dépassez facilement les 256 Mo de RAM alloués à PHP. Et même en traitant par lots de 500, le cache objet interne de WordPress grossit à chaque itération — la mémoire ne redescend jamais.
La solution : générateurs PHP + WP-CLI
L’approche qui tient à l’échelle combine trois choses : un générateur PHP pour ne charger qu’un ID à la fois en mémoire, un nettoyage de cache entre chaque enregistrement, et les transactions SQL pour pouvoir tout annuler en cas de problème.
Enregistrer la commande
if ( defined( 'WP_CLI' ) && WP_CLI ) {
WP_CLI::add_command( 'db-migrate', 'DB_Migration_Command' );
}
Le defined( 'WP_CLI' ) empêche le code de se charger hors de la ligne de commande — il ne ralentit pas votre front-end.
Le générateur d’IDs
Au lieu de charger tous les produits en mémoire, on récupère les IDs et on les distribue un par un avec yield :
private function get_all_product_ids() {
global $wpdb;
$ids = $wpdb->get_col(
"SELECT ID FROM {$wpdb->posts}
WHERE post_type = 'product' AND post_status = 'publish'"
);
foreach ( $ids as $id ) {
yield (int) $id;
}
}
La différence avec un return $ids classique : le générateur met en pause l’exécution après chaque yield. Il consomme la même quantité de mémoire pour 10 produits que pour 1 000 000.
La commande complète
class DB_Migration_Command {
/**
* Met à jour les métadonnées produits en masse.
*
* ## OPTIONS
*
* [--batch-size=<number>]
* : Nombre de produits avant un flush global du cache.
* ---
* default: 500
* ---
*
* [--dry-run]
* : Exécuter sans sauvegarder les modifications.
*/
public function products( $args, $assoc_args ) {
global $wpdb;
$dry_run = isset( $assoc_args['dry-run'] );
$batch_size = max( 1, (int) $assoc_args['batch-size'] );
$total = $wpdb->get_var(
"SELECT COUNT(ID) FROM {$wpdb->posts}
WHERE post_type = 'product' AND post_status = 'publish'"
);
$progress = \WP_CLI\Utils\make_progress_bar( 'Mise à jour produits', $total );
$wpdb->query( 'START TRANSACTION' );
try {
foreach ( $this->get_all_product_ids() as $index => $post_id ) {
if ( ! $dry_run ) {
update_post_meta( $post_id, '_new_meta_key', 'updated_value' );
}
$progress->tick();
// Nettoyage chirurgical : libérer la mémoire du post traité
clean_post_cache( $post_id );
// Nettoyage global : vider le cache objet tous les X posts
if ( 0 === ( $index + 1 ) % $batch_size ) {
wp_cache_flush();
}
}
$progress->finish();
if ( ! $dry_run ) {
$wpdb->query( 'COMMIT' );
WP_CLI::success( 'Migration terminée !' );
} else {
$wpdb->query( 'ROLLBACK' );
WP_CLI::log( 'Dry run terminé. Aucune modification sauvegardée.' );
}
} catch ( Exception $e ) {
$wpdb->query( 'ROLLBACK' );
WP_CLI::error( 'Erreur critique : ' . $e->getMessage() );
}
}
}
Pourquoi chaque détail compte
Transactions SQL
START TRANSACTION + COMMIT / ROLLBACK : soit tout passe, soit rien ne change. Sans ça, un crash en plein milieu vous laisse avec une base à moitié migrée — le pire scénario.
Double stratégie de cache
Deux niveaux de nettoyage qui travaillent ensemble :
clean_post_cache( $post_id ) libère le cache du post qu’on vient de traiter — c’est le scalpel. wp_cache_flush() vide tout le cache objet global tous les 500 posts (configurable via --batch-size) — c’est le marteau.
Si votre serveur utilise Redis ou Memcached, le flush global est d’autant plus important : sans lui, le cache persistant gonfle indéfiniment et finit par saturer la mémoire du serveur de cache. C’est aussi pour cette raison qu’un plugin de cache classique ne suffit pas face aux opérations massives WooCommerce.
Dry-run
Le flag --dry-run exécute toute la migration dans une transaction, puis fait un ROLLBACK à la fin. Vous voyez la barre de progression avancer, les warnings apparaître s’il y en a, mais rien n’est écrit en base. C’est la répétition générale avant la production.
Barre de progression
make_progress_bar() transforme un script silencieux en processus transparent. Vous voyez exactement où en est la migration, combien il reste, et si quelque chose bloque.
Le workflow complet en production
- Exporter la base avant tout :
wp db export pre-migration-backup.sql
- Tester en dry-run :
wp db-migrate products --dry-run
- Lancer la migration :
wp db-migrate products --batch-size=500
-
Vérifier les données manuellement sur un échantillon de produits.
-
En cas de problème, restaurer :
wp db import pre-migration-backup.sql
Si vous avez un environnement de staging, faites les étapes 1 à 4 là-bas d’abord. Ne lancez la migration en production que quand le dry-run + staging sont validés.
Cas d’usage WooCommerce courants
Cette approche s’applique à beaucoup de situations au-delà d’un simple update_post_meta, et fait partie intégrante d’une optimisation WooCommerce en profondeur :
Migration HPOS — Si vous migrez vos commandes de wp_posts vers les tables HPOS (High-Performance Order Storage) et que le processus natif échoue sur les gros volumes, une commande WP-CLI custom avec la même structure (générateur + transactions + cache flush) vous donne le contrôle total.
Nettoyage des options autoloadées — Parcourir la table wp_options pour identifier et désactiver l’autoload sur les options volumineuses. Le même pattern fonctionne : récupérer les IDs, traiter un par un, vider le cache régulièrement.
Mise à jour de prix en masse — Modifier le prix de milliers de produits en appliquant un pourcentage, une règle par catégorie, ou un taux de change. Avec les transactions SQL, si le calcul est faux sur un produit, vous annulez tout d’un bloc.
Régénération des thumbnails — wp media regenerate existe déjà, mais pour des traitements custom (watermark, format spécifique), la structure générateur + barre de progression est exactement ce qu’il faut.
En résumé
Dès que vous dépassez quelques milliers d’enregistrements, le navigateur n’est plus une option. WP-CLI vous donne un environnement sans timeout, et les générateurs PHP vous garantissent une consommation mémoire constante. Ajoutez les transactions SQL et le dry-run, et vous avez un filet de sécurité complet.
Le code de cet article est adaptable à n’importe quelle migration WooCommerce. Changez la requête SQL, changez le update_post_meta, et le reste de la structure — générateur, cache, transactions, progression — reste identique.
Vous avez une migration WooCommerce complexe à réaliser ? Demandez un audit gratuit — je vous aide à planifier et exécuter la migration sans risque pour votre production.
Besoin d'un audit de votre boutique WooCommerce ?
Je vous envoie un diagnostic personnalisé avec les points à améliorer en priorité. Gratuit, sans engagement.