Un site WordPress qui accumule, au fil des années, des dizaines de tables et d’options orphelines appartenant à des extensions désinstallées depuis longtemps : ce scénario est d’une banalité affligeante. La cause est presque toujours la même — l’auteur de l’extension a soigné son activation et négligé sa désinstallation, considérée à tort comme un détail secondaire.
WordPress propose deux mécanismes pour gérer proprement la désinstallation d’une extension : le fichier uninstall.php et la fonction register_uninstall_hook(). Ils ne sont pas équivalents, et le choix entre les deux a des conséquences concrètes sur la fiabilité du nettoyage.
Désactivation, suppression : une distinction essentielle
La confusion la plus fréquente concerne le moment où le nettoyage doit intervenir. La désactivation d’une extension (via register_deactivation_hook()) est un état réversible : l’utilisateur peut réactiver l’extension l’instant d’après, et ses réglages doivent être préservés. La suppression (l’action « Supprimer » dans la liste des extensions, ou wp plugin delete) est en revanche définitive et c’est à ce moment précis, et uniquement à ce moment, que les données doivent être effacées.
| Hook | Se déclenche lors de | Doit nettoyer les données ? |
|---|---|---|
| register_deactivation_hook | Désactivation depuis l’admin | Non, jamais |
| uninstall.php / register_uninstall_hook | Suppression définitive | Oui, systématiquement |
uninstall.php : la méthode recommandée
Un fichier nommé exactement uninstall.php, placé à la racine du dossier de l’extension, est automatiquement exécuté par WordPress au moment de la suppression, sans qu’aucun appel de fonction ne soit nécessaire dans le fichier principal. C’est l’approche recommandée par le Plugin Handbook, pour une raison de performance : ce fichier n’est chargé que lors de la suppression, jamais à chaque requête normale du site.
<?php
// uninstall.php
// Sécurité impérative : ce fichier ne doit jamais s'exécuter directement
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
exit;
}
global $wpdb;
// Options simples
delete_option( 'acme_api_key' );
delete_option( 'acme_settings' );
// Métadonnées liées aux articles
$wpdb->query(
"DELETE FROM {$wpdb->postmeta} WHERE meta_key LIKE 'acme\_%'"
);
// Table personnalisée créée par l'extension
$wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}acme_journal" );
// Tâches planifiées restantes
wp_clear_scheduled_hook( 'acme_verification_stock' );
// Sites d'un réseau multisite : à traiter séparément (voir plus bas)
La vérification de la constante WP_UNINSTALL_PLUGIN en tout début de fichier n’est pas cosmétique : sans elle, un accès direct à ce fichier via une URL publique déclencherait la suppression des données du site, une faille de sécurité qui a réellement été exploitée sur des extensions négligentes par le passé.

register_uninstall_hook : quand l’éviter
L’alternative, register_uninstall_hook(), appelée depuis le fichier principal de l’extension, fonctionne aussi mais présente un inconvénient concret : elle oblige WordPress à charger l’intégralité du fichier principal de l’extension (et donc potentiellement toutes ses dépendances) uniquement pour exécuter cette fonction de nettoyage, y compris lors de requêtes normales où WordPress vérifie simplement si un hook de désinstallation existe.
// Dans le fichier principal, approche moins recommandée
register_uninstall_hook( __FILE__, 'acme_desinstaller' );
function acme_desinstaller() {
delete_option( 'acme_api_key' );
}
Un autre piège spécifique à cette fonction : le callback ne peut pas être une méthode d’instance d’objet ni une closure, uniquement une fonction nommée ou une méthode statique, car WordPress sérialise la référence au callback en base de données pour la retrouver plus tard.
// Fonctionne
register_uninstall_hook( __FILE__, [ 'Acme\MonExtension\Uninstaller', 'run' ] );
// Ne fonctionne PAS : une closure ne peut pas être sérialisée
register_uninstall_hook( __FILE__, function () { /* ... */ } );
Ne pas oublier le cas multisite
Sur un réseau multisite, une extension activée sur l’ensemble du réseau doit nettoyer les données de chaque site individuellement, pas seulement du site courant au moment de la désinstallation.
if ( is_multisite() ) {
$sites = get_sites( [ 'fields' => 'ids' ] );
foreach ( $sites as $site_id ) {
switch_to_blog( $site_id );
delete_option( 'acme_api_key' );
restore_current_blog();
}
} else {
delete_option( 'acme_api_key' );
}
restore_current_blog() après chaque switch_to_blog() est impératif : l’oublier laisse le contexte du site courant corrompu pour le reste du script de désinstallation, un bug difficile à repérer car il ne provoque pas d’erreur visible immédiate.
Laisser le choix à l’utilisateur
Pour certaines extensions, effacer systématiquement toutes les données à la suppression n’est pas souhaitable : un utilisateur qui désinstalle temporairement pour tester une alternative ne veut pas perdre sa configuration. Bonne pratique : un réglage explicite, décoché par défaut, du type « Supprimer toutes les données à la désinstallation ».
// uninstall.php
if ( get_option( 'acme_supprimer_donnees_a_la_desinstallation' ) ) {
delete_option( 'acme_api_key' );
// ... nettoyage complet
}
Un conseil qu’on donne systématiquement en revue de code : tester la désinstallation avant chaque publication, pas seulement l’activation. Un simple
wp plugin uninstallsuivi d’une inspection de la base suffit à repérer les oublis.
Vérifier le résultat avec WP-CLI
wp plugin deactivate mon-extension
wp plugin uninstall mon-extension
wp db query "SELECT option_name FROM wp_options WHERE option_name LIKE 'acme%'"
Si cette dernière requête retourne encore des lignes après la désinstallation, le nettoyage est incomplet.
En résumé
uninstall.php reste la méthode la plus performante et la plus lisible pour gérer la désinstallation d’une extension. Le nettoyage doit couvrir les options, les métadonnées, les tables personnalisées et les tâches planifiées, en tenant compte du multisite, et idéalement laisser à l’utilisateur le choix de conserver ses données. Ce travail, souvent traité en dernière minute, mérite le même soin que l’activation.