Gérer les modifications de base de données à la main sur un projet WordPress, c’est le genre de chose qui fonctionne… jusqu’au jour où ça ne fonctionne plus. Un champ oublié en production, une modification appliquée deux fois, un rollback impossible : on a tous vécu ce moment un peu stressant. Bonne nouvelle : il existe une approche propre et professionnelle pour versionner vos migrations de base de données, les automatiser avec WP-CLI et les intégrer directement dans votre workflow de déploiement.
Pourquoi versionner vos migrations de base de données WordPress ?
On a tous fait cette erreur. Une petite modification en base « juste pour tester », une option ajoutée à la volée via phpMyAdmin, une table custom créée directement sur le serveur de production… Et six mois plus tard, impossible de savoir pourquoi l’environnement local ne se comporte pas comme la prod. Bienvenue dans l’enfer des bases de données non versionnées.
Le problème classique : la base de données hors de contrôle
Le code source, on sait le gérer : Git est là pour ça. Mais la base de données ? C’est souvent le parent pauvre du workflow de développement WordPress. On modifie une option directement dans wp_options via phpMyAdmin, on crée une table custom à la main pour un projet client, on ajoute un index sur une colonne… Et tout ça sans laisser la moindre trace écrite.
Le résultat, c’est prévisible : votre environnement local diverge de la staging, qui diverge elle-même de la production. Votre collègue qui rejoint le projet ne peut pas reproduire l’état exact de la base. Et si vous devez tout remettre à zéro après un incident ? Bonne chance pour retrouver toutes les modifications faites « à la main » au fil des mois.
C’est un problème de traçabilité, mais aussi de confiance. On ne sait plus vraiment ce que contient la base, ni comment elle a évolué.
Ce qu’apporte un système de migrations versionné
L’idée derrière les migrations versionnées, c’est simple : chaque modification de la structure de la base de données (et parfois même des données elles-mêmes) est décrite dans un fichier PHP versionné avec le reste du code. Un peu comme des commits Git, mais pour votre schéma de base de données.
Concrètement, ça change beaucoup de choses :
- Reproductibilité : n’importe quel développeur peut cloner le dépôt et rejouer toutes les migrations pour obtenir une base identique à celle de la prod.
- Traçabilité : on sait exactement quand telle table a été créée, pourquoi, et par qui.
- Travail en équipe : fini les conflits silencieux entre environnements. Chacun applique les migrations dans l’ordre, point.
- Déploiement fiabilisé : les montées en production deviennent prévisibles. Plus de « ah oui, j’avais oublié de créer cette table en prod… »
Bon, ça demande un peu de discipline au départ. Mais une fois la mécanique en place, vous ne voudrez plus revenir en arrière.
WP-CLI comme socle technique : pourquoi c’est le bon choix
WP-CLI est l’outil en ligne de commande officiel de WordPress. Si vous ne l’utilisez pas encore au quotidien, c’est le moment de vous y mettre. Il permet d’interagir avec WordPress sans passer par l’interface graphique : gérer les plugins, les utilisateurs, les options… et surtout, exécuter des scripts PHP dans le contexte complet de WordPress.
C’est précisément ça qui en fait le socle idéal pour un système de migrations. Avec WP-CLI, on peut :
- Créer des commandes personnalisées (
wp migration run,wp migration status, etc.) - Exécuter des scripts dans le contexte WordPress, avec accès à
$wpdb, aux fonctions natives, aux hooks - Automatiser les migrations dans un pipeline CI/CD sans intervention manuelle
- Logger les résultats directement dans la console ou dans un fichier
Par rapport à des solutions externes comme Phinx ou Liquibase, WP-CLI a l’avantage de rester dans l’écosystème WordPress. Pas de dépendance supplémentaire complexe à gérer, pas de friction avec les conventions du CMS. Et franchement, pour un projet WordPress, c’est exactement ce qu’il faut.
Concevoir l’architecture du système de migrations
Bon, on rentre dans le vif du sujet. Avant d’écrire la moindre ligne de code, il faut poser une architecture claire et cohérente. C’est exactement ce qui différencie un système de migrations solide d’un bricolage qui tiendra six mois… si on a de la chance.
La structure des fichiers et le nommage versionné
L’idée de base : centraliser toutes les migrations dans un dossier dédié. On peut le placer à la racine du projet ou, mieux encore, dans un plugin dédié (par exemple wp-content/plugins/mon-plugin/migrations/). Cette deuxième option est préférable si vos migrations sont liées à un plugin spécifique — ça garde les choses cohérentes et portables.
Chaque fichier de migration suit un schéma de nommage horodaté strict :
2026_04_01_000001_add_custom_table.php
2026_04_01_000002_add_index_to_orders.php
2026_04_02_000001_rename_column_user_meta.php
Ce format YYYY_MM_DD_XXXXXX_description.php garantit un ordre d’exécution chronologique et lisible. La partie description (en snake_case) permet de comprendre d’un coup d’œil ce que fait la migration. Pas besoin d’ouvrir le fichier pour savoir qu’il crée une table ou ajoute un index — c’est de la documentation gratuite.
La structure du dossier ressemble donc à ça :
mon-plugin/
├── migrations/
│ ├── 2026_04_01_000001_add_custom_table.php
│ └── 2026_04_01_000002_add_index_to_orders.php
├── includes/
│ └── class-migration-command.php
└── mon-plugin.php
Simple, prévisible, facile à versionner avec Git. Exactement ce qu’on veut.
La table de suivi des migrations en base de données
Pour savoir quelles migrations ont déjà été exécutées, on a besoin d’une table dédiée en base de données. On va l’appeler {prefix}migrations (avec le préfixe WordPress habituel, donc souvent wp_migrations).
Voici le SQL pour la créer :
global $wpdb;
$table_name = $wpdb->prefix . 'migrations';
$charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE IF NOT EXISTS {$table_name} (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
migration VARCHAR(255) NOT NULL,
batch INT(11) NOT NULL,
executed_at DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY migration (migration)
) {$charset_collate};";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta( $sql );
Les colonnes sont simples mais suffisantes :
migration: le nom du fichier (sans extension, ou avec — à vous de choisir, mais soyez cohérents)batch: le numéro de lot d’exécution, essentiel pour le rollback groupéexecuted_at: horodatage automatique pour l’audit
Le champ batch est particulièrement important. Il permet de regrouper toutes les migrations exécutées lors d’un même appel à wp migrate run et de les annuler ensemble avec wp migrate rollback. Exactement comme Laravel le fait — et c’est une idée brillante qu’on a tout intérêt à copier.
Le script PHP de gestion : enregistrement et exécution
Chaque fichier de migration contient une classe avec deux méthodes : up() pour appliquer la migration et down() pour l’annuler. Voici un exemple concret :
<?php
// 2026_04_01_000001_add_custom_table.php
class Migration_2026_04_01_000001_Add_Custom_Table {
public function up() {
global $wpdb;
$table = $wpdb->prefix . 'custom_orders';
$charset = $wpdb->get_charset_collate();
$wpdb->query( "START TRANSACTION" );
try {
$sql = "CREATE TABLE IF NOT EXISTS {$table} (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
user_id BIGINT(20) UNSIGNED NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'pending',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) {$charset};";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta( $sql );
$wpdb->query( "COMMIT" );
error_log( '[Migration] Table custom_orders créée avec succès.' );
} catch ( Exception $e ) {
$wpdb->query( "ROLLBACK" );
error_log( '[Migration] Erreur : ' . $e->getMessage() );
throw $e;
}
}
public function down() {
global $wpdb;
$table = $wpdb->prefix . 'custom_orders';
$wpdb->query( "DROP TABLE IF EXISTS {$table}" );
error_log( '[Migration] Table custom_orders supprimée.' );
}
}
Quelques points importants ici. On utilise $wpdb pour toutes les requêtes — jamais de PDO ou de mysqli direct, on reste dans l’écosystème WordPress. On encapsule les opérations dans une transaction SQL quand c’est possible (attention : dbDelta() ne supporte pas toujours les transactions sur CREATE TABLE, mais pour les INSERT ou UPDATE c’est indispensable). Et on log systématiquement avec error_log() pour garder une trace.
Le gestionnaire de migrations charge dynamiquement ces classes, vérifie dans wp_migrations ce qui a déjà tourné, puis exécute uniquement ce qui reste en attente. C’est du PHP classique mais efficace.
Intégration avec WP-CLI : créer ses propres commandes
C’est là que le système devient vraiment puissant. On va enregistrer des commandes WP-CLI personnalisées via WP_CLI::add_command() dans un fichier class-migration-command.php.
<?php
// includes/class-migration-command.php
if ( ! defined( 'WP_CLI' ) || ! WP_CLI ) {
return;
}
class Migration_Command {
/**
* Exécute les migrations en attente.
*
* ## EXAMPLES
* wp migrate run
*/
public function run( $args, $assoc_args ) {
global $wpdb;
$migrations_path = plugin_dir_path( __FILE__ ) . '../migrations/';
$pending = $this->get_pending_migrations( $migrations_path );
if ( empty( $pending ) ) {
WP_CLI::success( 'Aucune migration en attente.' );
return;
}
$batch = $this->get_next_batch();
foreach ( $pending as $file ) {
require_once $migrations_path . $file;
$class_name = $this->get_class_name( $file );
$migration = new $class_name();
try {
$migration->up();
$wpdb->insert( $wpdb->prefix . 'migrations', [
'migration' => $file,
'batch' => $batch,
] );
WP_CLI::log( "✔ Migration exécutée : {$file}" );
} catch ( Exception $e ) {
WP_CLI::error( "Échec sur {$file} : " . $e->getMessage() );
}
}
WP_CLI::success( count( $pending ) . ' migration(s) exécutée(s).' );
}
/**
* Affiche le statut des migrations.
*
* ## EXAMPLES
* wp migrate status
*/
public function status( $args, $assoc_args ) {
global $wpdb;
$executed = $wpdb->get_col(
"SELECT migration FROM {$wpdb->prefix}migrations ORDER BY id ASC"
);
$migrations_path = plugin_dir_path( __FILE__ ) . '../migrations/';
$all_files = glob( $migrations_path . '*.php' );
$rows = [];
foreach ( $all_files as $filepath ) {
$file = basename( $filepath );
$rows[] = [
'Migration' => $file,
'Statut' => in_array( $file, $executed ) ? '✔ Exécutée' : '⏳ En attente',
];
}
WP_CLI\Utils\format_items( 'table', $rows, [ 'Migration', 'Statut' ] );
}
/**
* Annule le dernier lot de migrations.
*
* ## EXAMPLES
* wp migrate rollback
*/
public function rollback( $args, $assoc_args ) {
global $wpdb;
$last_batch = $wpdb->get_var(
"SELECT MAX(batch) FROM {$wpdb->prefix}migrations"
);
if ( ! $last_batch ) {
WP_CLI::warning( 'Aucune migration à annuler.' );
return;
}
$to_rollback = $wpdb->get_col( $wpdb->prepare(
"SELECT migration FROM {$wpdb->prefix}migrations WHERE batch = %d ORDER BY id DESC",
$last_batch
) );
$migrations_path = plugin_dir_path( __FILE__ ) . '../migrations/';
foreach ( $to_rollback as $file ) {
require_once $migrations_path . $file;
$class_name = $this->get_class_name( $file );
$migration = new $class_name();
try {
$migration->down();
$wpdb->delete( $wpdb->prefix . 'migrations', [ 'migration' => $file ] );
WP_CLI::log( "↩ Rollback effectué : {$file}" );
} catch ( Exception $e ) {
WP_CLI::error( "Échec du rollback sur {$file} : " . $e->getMessage() );
}
}
WP_CLI::success( 'Rollback du lot ' . $last_batch . ' effectué.' );
}
// Méthodes utilitaires privées...
private function get_pending_migrations( $path ) { /* ... */ }
private function get_next_batch() { /* ... */ }
private function get_class_name( $file ) { /* ... */ }
}
WP_CLI::add_command( 'migrate', 'Migration_Command' );
On enregistre ensuite cette commande dans le fichier principal du plugin :
if ( defined( 'WP_CLI' ) && WP_CLI ) {
require_once plugin_dir_path( __FILE__ ) . 'includes/class-migration-command.php';
}
Et voilà : on dispose de wp migrate run, wp migrate status et wp migrate rollback — trois commandes qui couvrent 90% des besoins au quotidien. Le tout avec une gestion des erreurs propre via try/catch et des logs pour tracer chaque opération. C’est pas parfait, mais c’est robuste et maintenant vous avez une base solide pour aller plus loin.
Automatiser l’exécution avec des scripts bash et des hooks de déploiement
On a maintenant un système de migrations fonctionnel. Mais si on doit lancer wp migrate run à la main après chaque déploiement, on va inévitablement oublier. Et un oubli sur un environnement de production, ça peut faire mal. L’idée, c’est donc de brancher l’exécution des migrations directement dans le pipeline de déploiement.
Un script bash minimal pour démarrer
Voici un exemple simple mais robuste qu’on peut placer dans un fichier post-deploy.sh à la racine du projet :
#!/bin/bash
set -e
# Se placer dans le bon répertoire
cd /var/www/html
# Vérifier que WP-CLI est disponible
if ! command -v wp &> /dev/null; then
echo "Erreur : WP-CLI introuvable. Abandon."
exit 1
fi
# Exécuter les migrations
echo "Lancement des migrations..."
wp migrate run --allow-root
# Vérifier le code de retour
if [ $? -eq 0 ]; then
echo "Migrations terminées avec succès."
else
echo "Échec des migrations. Vérifiez les logs."
exit 1
fi
Le set -e est important : il stoppe le script dès qu’une commande échoue, ce qui évite de continuer un déploiement en état incohérent. L’option --allow-root est souvent nécessaire quand le script tourne en tant que root (ce qui est fréquent dans les pipelines CI/CD).
Déclencher via un hook Git post-receive
Sur un serveur avec accès SSH et un dépôt Git bare, on peut brancher ce script directement dans le hook post-receive. Il suffit de le placer dans .git/hooks/post-receive et de le rendre exécutable :
chmod +x .git/hooks/post-receive
À chaque git push vers le serveur, les migrations s’exécutent automatiquement. C’est élégant, mais ça suppose un accès SSH et un contrôle total sur le serveur — ce qui n’est pas toujours le cas.
Intégration dans un pipeline CI/CD
Avec GitHub Actions ou GitLab CI, on peut intégrer l’exécution des migrations dans le workflow de déploiement. Exemple minimal avec GitHub Actions :
- name: Run database migrations
run: |
ssh user@mon-serveur "cd /var/www/html && wp migrate run --allow-root"
Pour GitLab CI, même principe dans le bloc deploy :
deploy:
stage: deploy
script:
- ssh user@mon-serveur "cd /var/www/html && wp migrate run --allow-root"
only:
- main
C’est propre, traçable, et on dispose du log complet dans l’interface CI/CD. On sait exactement quand une migration a été jouée et si elle a réussi.
Le cas des hébergements mutualisés
Sur un hébergement mutualisé, c’est plus compliqué — mais pas impossible. WP-CLI peut être absent, ou disponible en version limitée sans accès en ligne de commande directe. Dans ce cas, deux alternatives existent :
- Via un cron WordPress : on enregistre une action dans
wp_schedule_eventqui appelle la méthoderun()du runner. C’est moins immédiat, mais ça fonctionne. On peut déclencher ce cron manuellement via une requête HTTP après le déploiement. - Via une page admin protégée : on crée une route dans
wp-adminaccessible uniquement avec un token secret (passé en paramètre GET). Un simple appelcurlen fin de déploiement suffit à déclencher les migrations.
Ces méthodes sont moins élégantes, je l’accorde. Mais sur un mutualisé OVH ou Ionos, c’est souvent tout ce qu’on a. Et ça reste infiniment mieux que de se connecter en FTP pour exécuter un script à la main.
