Zéro : c’est le nombre de lignes produites dans les journaux alors que les options, les tables personnalisées et les métadonnées d’une extension restent intégralement en base après sa désinstallation depuis l’écran « Extensions ». Ce symptôme, sur une extension qui utilisait pourtant register_uninstall_hook() avec un callback apparemment correct, a mis plusieurs heures à être compris.
La cause tenait à une particularité peu documentée du fonctionnement de ce hook : contrairement à register_activation_hook(), dont le callback s’exécute dans le contexte normal du plugin déjà chargé, register_uninstall_hook() exige que le callback soit disponible dès l’inclusion du fichier principal de l’extension, sans dépendre du reste de l’exécution du plugin.
Symptôme : la désinstallation ne nettoie rien
Le code en cause suivait une structure orientée objet classique, avec une méthode statique de nettoyage appelée depuis le constructeur de la classe principale de l’extension :
class MonExtension {
public function __construct() {
register_uninstall_hook( __FILE__, [ __CLASS__, 'desinstaller' ] );
}
public static function desinstaller() {
delete_option( 'mon_extension_reglages' );
global $wpdb;
$wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}mon_extension_donnees" );
}
}
new MonExtension();
Ce code semblait correct et fonctionnait parfaitement pour register_activation_hook() placé selon la même logique. Pourtant, à la désinstallation réelle, aucune des deux opérations de nettoyage ne s’exécutait.
Diagnostic : le mécanisme réel de désinstallation

WordPress ne conserve pas le plugin chargé en mémoire pour exécuter la désinstallation : au moment où l’utilisateur clique sur « Supprimer définitivement », WordPress recharge uniquement le fichier principal du plugin dans un contexte minimal, cherche l’entrée correspondante dans l’option uninstall_plugins (alimentée par un précédent appel à register_uninstall_hook()), puis tente d’appeler le callback enregistré.
Le problème : au moment de ce rechargement, la classe MonExtension n’a jamais été instanciée dans ce contexte particulier, car le hook est enregistré dans le constructeur, exécuté seulement lors d’un chargement normal du plugin, pas lors de ce rechargement minimal dédié à la désinstallation. Le tableau [ 'MonExtension', 'desinstaller' ] pointe donc vers une classe jamais chargée dans ce contexte, et WordPress échoue silencieusement à appeler le callback, sans lever d’erreur visible.
Correctif : deux approches fiables
La première approche consiste à s’assurer que la classe est chargée indépendamment de toute instanciation, en enregistrant le hook directement au niveau du fichier principal, hors de tout constructeur :
require_once __DIR__ . '/includes/class-mon-extension.php';
register_uninstall_hook( __FILE__, [ 'MonExtension', 'desinstaller' ] );
Ce placement garantit que la classe existe déjà au moment où WordPress tente d’appeler le callback, puisque l’inclusion se fait dès le chargement du fichier principal, indépendamment de toute logique conditionnelle interne à l’extension.
La seconde approche, plus robuste encore et généralement recommandée par la documentation officielle, consiste à ne pas utiliser register_uninstall_hook() du tout, mais à créer un fichier uninstall.php à la racine de l’extension, que WordPress exécute automatiquement à la désinstallation s’il existe, sans configuration supplémentaire :
// uninstall.php
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
exit;
}
delete_option( 'mon_extension_reglages' );
global $wpdb;
$wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}mon_extension_donnees" );
La constante WP_UNINSTALL_PLUGIN, définie automatiquement par WordPress avant d’inclure ce fichier, sert de garde-fou contre une exécution directe accidentelle du fichier en dehors du contexte de désinstallation.
Prévenir la récidive
- Préférer systématiquement
uninstall.phpàregister_uninstall_hook()pour toute nouvelle extension, en raison de sa fiabilité supérieure et de sa simplicité de test. - Si
register_uninstall_hook()reste nécessaire pour une raison historique, vérifier explicitement que le callback ne dépend d’aucune instanciation préalable de classe. - Tester la désinstallation sur un environnement de développement dédié, en vérifiant directement en base que les tables et options ont bien disparu, plutôt que de faire confiance à l’absence d’erreur affichée.
Le principe que je retiens de ce cas : l’absence d’erreur ne signifie jamais qu’une opération a réussi. Sur ce type de hook rarement testé en conditions réelles, seule une vérification directe en base confirme le résultat attendu.
En résumé
Un callback de désinstallation qui dépend d’une classe instanciée ailleurs dans le code échoue silencieusement, faute d’un rechargement minimal du plugin lors de ce contexte particulier. Ce diagnostic porte sur ce mécanisme précis de WordPress ; il ne traite pas de la normalisation Unicode, sujet totalement distinct du cycle de vie d’une extension.