Un client qui gère un catalogue de pièces détachées m’a demandé un tableau de bord d’administration capable de filtrer une liste de plusieurs milliers de références sans recharger la page à chaque clic. Pas question d’installer un framework JavaScript pour ça : admin-ajax.php suffit largement, à condition de comprendre les deux ou trois pièges qui font perdre une soirée entière quand on ne les a jamais rencontrés.
Ce fichier existe depuis les origines de WordPress et reste, en 2020, le point d’entrée le plus simple pour exécuter du PHP depuis du JavaScript dans l’admin. Il n’a rien d’obsolète : REST API est plus adaptée aux échanges front-end publics ou aux applications complexes, mais pour une action ponctuelle dans un écran d’administration, admin-ajax.php reste souvent la solution la plus rapide à mettre en œuvre.
Déclarer les deux hooks wp_ajax_
Le principe tient en une ligne : WordPress route toute requête POST ou GET envoyée à admin-ajax.php avec un paramètre action vers un hook nommé wp_ajax_{action} pour un utilisateur connecté, et wp_ajax_nopriv_{action} pour un visiteur anonyme.
add_action( 'wp_ajax_filtrer_pieces', 'kaolin_filtrer_pieces_callback' );
function kaolin_filtrer_pieces_callback() {
check_ajax_referer( 'kaolin_pieces_nonce', 'nonce' );
if ( ! current_user_can( 'edit_posts' ) ) {
wp_send_json_error( array( 'message' => 'Permission refusée.' ), 403 );
}
$reference = isset( $_POST['reference'] ) ? sanitize_text_field( wp_unslash( $_POST['reference'] ) ) : '';
$resultats = kaolin_rechercher_pieces( $reference );
wp_send_json_success( array( 'items' => $resultats ) );
}
Dans un écran strictement réservé aux administrateurs, il est tentant d’omettre wp_ajax_nopriv_. C’est justement la bonne pratique : si l’action ne concerne que des utilisateurs connectés, n’enregistrez que le premier hook. Ajouter le second par réflexe ouvre une porte inutile.
Transmettre l’URL et le nonce au bon moment
Le script JavaScript ne connaît ni l’URL d’admin-ajax.php ni le nonce attendu par check_ajax_referer. C’est le rôle de wp_localize_script, appelé juste après wp_enqueue_script, qui injecte un objet global exploitable côté client.

wp_enqueue_script( 'kaolin-admin-pieces', plugins_url( 'js/pieces.js', __FILE__ ), array( 'jquery' ), '1.0', true );
wp_localize_script( 'kaolin-admin-pieces', 'kaolinPieces', array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'kaolin_pieces_nonce' ),
) );
Côté JavaScript, l’appel ressemble à ceci :
jQuery.post( kaolinPieces.ajaxUrl, {
action: 'filtrer_pieces',
nonce: kaolinPieces.nonce,
reference: valeurSaisie
} ).done( function( reponse ) {
if ( reponse.success ) {
afficherResultats( reponse.data.items );
}
} );
Une erreur fréquente : oublier que action doit correspondre exactement au suffixe du hook PHP, sans le préfixe wp_ajax_. Une faute de frappe ici renvoie une erreur 400 silencieuse, sans message explicite dans la console.
Répondre avec wp_send_json_success et wp_send_json_error
Avant l’introduction de ces deux fonctions, il fallait construire soi-même un tableau avec une clé success et appeler wp_send_json. Elles font exactement cela, mais avec une convention stable que jQuery et fetch savent exploiter côté client sans code supplémentaire.
wp_send_json_success( $data )envoie un objet{ success: true, data: ... }et termine l’exécution avecwp_die()wp_send_json_error( $data, $status_code )fait de même avecsuccess: falseet permet de préciser un code HTTP- Les deux appellent
wp_die()en interne : tout code placé après ne s’exécute jamais
C’est justement cette dernière ligne qui piège le plus de développeurs débutants : ils ajoutent un return après wp_send_json_success() en pensant arrêter la fonction, alors que l’exécution s’est déjà arrêtée à l’appel précédent. Le code n’est pas faux, simplement inutile.
Vérifier les permissions avant de traiter la requête
check_ajax_referer vérifie que la requête vient bien du navigateur qui a chargé la page admin, pas qu’elle vient d’un utilisateur autorisé. Ce sont deux vérifications distinctes, et j’ai vu plusieurs extensions confondre les deux.
La règle que je retiens de ce projet : toujours enchaîner check_ajax_referer puis current_user_can, dans cet ordre. Le nonce protège contre le rejeu et la falsification de requête intersite, la capacité protège contre un utilisateur connecté mais non autorisé à effectuer l’action demandée.
Un nonce valide ne prouve jamais qu’un utilisateur a le droit de faire une action : il prouve seulement que la requête vient du bon formulaire. Les deux contrôles sont complémentaires, jamais interchangeables.
admin-ajax.php ou REST API : lequel choisir
Pour ce projet, la question s’est posée : fallait-il migrer directement vers un endpoint REST personnalisé ? La réponse a été non, pour une raison simple : l’action ne concernait qu’un unique écran d’administration, sans besoin d’authentification par clé d’API ni de réutilisation externe.
- Restez sur
admin-ajax.phppour une action ponctuelle, interne à un écran d’admin, sans consommateur externe - Passez à REST API dès qu’une application mobile, un site tiers ou un bloc Gutenberg autonome doit consommer les mêmes données
- REST API expose automatiquement une documentation de schéma et gère nativement les méthodes HTTP, ce qu’
admin-ajax.phpne fait pas
Dans les faits, une bonne partie des extensions professionnelles continuent d’utiliser admin-ajax.php pour leurs écrans internes, tout en exposant une API REST pour les fonctionnalités destinées à être consommées ailleurs. Les deux mécanismes cohabitent très bien dans un même projet.
Pour aller plus loin
Le couple wp_ajax_ / wp_ajax_nopriv_ reste, dix-sept ans après sa création, l’un des mécanismes les plus utilisés du cœur de WordPress. Sa simplicité est sa force : pas de schéma à déclarer, pas de route à enregistrer, juste une action et un callback.
Le prix de cette simplicité, c’est la rigueur qu’il faut s’imposer soi-même : nonce systématique, vérification de capacité, et sortie propre via wp_send_json_*. Sur ce projet, ces trois réflexes ont suffi à livrer un tableau de bord fluide, sans jamais toucher à la couche REST.