WordPress est un CMS qui est infiniment extensible et personnalisable. Il existe des milliers d’extensions qui permettent d’ajouter des fonctionnalités à votre site ou de modifier le comportement normal de WordPress.

Tout cela est rendu possible par l’existence d’une API relativement simple contenue dans le cœur de WordPress depuis très longtemps : la plugin API.

Dans cet article, on va redécouvrir et étudier la plugin API (les fameux hooks de WordPress) pour en comprendre le fonctionnement plus en détail.

La plugin API : définition

La plugin API ou (API des extensions) est simplement un ensemble de fonctions PHP qui permettent aux extensions et thèmes de se greffer sur WordPress et de venir modifier son comportement.

WordPress est une application PHP. C’est simplement un gros script qui s’exécute de façon plus ou moins linéaire à chaque requête envoyée vers votre site web.

Grâce à la plugin API, on a la possibilité d’intervenir à plusieurs moments pendant l’exécution de ce script, que ce soit pour modifier une donnée utilisée par WordPress ou pour exécuter du code personnalisé arbitraire.

Ces moments spécifiques dans l’exécution sont les fameux hooks de WordPress (ou crochets). Ce n’est ni plus ni moins qu’un système d’événements simple. Les hooks sont tous identifiés par une chaîne de caractères unique, et lors de l’exécution de WordPress, ces hooks déclenchent toutes les fonctions qui leur sont associées.

Ainsi, si on a besoin d’ajouter du contenu HTML dans la balise <head> sur le devant du site, c’est sur le hook wp_head qu’il faudra se greffer. Si on doit charger un bout de JavaScript, c’est sur le hook wp_enqueue_scripts. On va simplement déclarer des fonctions personnalisées à exécuter sur tel ou tel hook, et quand WordPress rencontrera ce hook lors de son exécution, il déclenchera les fonctions associées.

C’est comme ça que les extensions ajoutent du contenu à vos pages, ajoutent des menus personnalisés ou des types de contenu personnalisés, modifient le contenu de vos balises de titre et méta-description, etc…

Comprendre les hooks de WordPress

Il y a deux types de hooks : les actions et les filtres.

  • Les filtres vont permettre aux extensions ou thèmes de modifier une donnée fournie par WordPress. WordPress va nous fournir une donnée que l’on peut manipuler avant de la retourner pour que le programme continue de s’exécuter normalement avec la donnée modifiée.
  • Les actions vont permettre d’exécuter du code arbitraire à un certain moment, mais ne nécessitent pas qu’on retourne une valeur.

Les fonctions centrales de la plugin API sont apply_filters() et add_filter() pour les filtres, et do_action() et add_action() pour les actions.

Ajouter un filtre avec add_filter()

Les filtres sont des fonctions qui permettent de modifier une donnée. On ajoute un filtre sur un hook spécifique en utilisant add_filter().

add_filter( string $hook_name, callable $callback, int $priority = 10, int $accepted_args = 1 ) va enregistrer une certaine fonction $callback pour qu’elle soit exécutée sur le hook $hook_name. Elle sert donc à “hooker” une fonction personnalisée sur un moment spécifique de l’exécution de WordPress identifié par le nom du hook. La fonction $callback est en fait simplement ajoutée à une liste d’attente de fonction à déclencher sur le hook et sera exécutée à chaque fois que le hook est déclenché.

On peut passer une priorité $priority pour contrôler l’ordre de déclenchement des fonctions hookées à cet endroit. Plus ce nombre est grand, plus la fonction de rappel $callback sera exécutée tard. A l’inverse, si on veut exécuter la fonction le plus tôt possible, on va utiliser un petit nombre.

On peut spécifier le nombre d’arguments $accepted_args attendus par la fonction $callback. Certains hooks fournissent aux fonctions de rappel plusieurs arguments supplémentaires en plus de la valeur à filtrer. Dans ce cas, si on a besoin de ces arguments supplémentaires, il faut passer leur nombre ici. Plus de détails sur ce point plus tard.

Déclencher les filtres avec apply_filters()

La fonction apply_filters() permet de placer des hooks. Partout dans le code source de WordPress où on voit un appel à apply_filters(), cela indique que l’on peut se greffer à cet endroit pour modifier une certaine donnée.

apply_filters( string $hook_name, mixed $value, mixed ...$args ) va appliquer (apply) tous les filtres (filters) censés s’exécuter sur le hook $hook_name, en leur passant la valeur à filtrer $value et un nombre arbitraire d’arguments supplémentaires. Autrement dit, cette fonction va simplement exécuter toutes les fonctions hookées sur $hook_name.

Les filtres hookés avec add_filter() sur ce même $hook_name vont donc être exécutées dans l’ordre de priorité sur lequel ils ont été ajoutés, et chaque fonction de rappel hookée va recevoir $value ainsi que tous les arguments supplémentaires s’il y en a.

Un exemple simple

Pour résumer :

  • add_filter() ajoute un filtre : c’est-à-dire une fonction qui va modifier une certaine valeur,
  • apply_filters() applique tous les filtres enregistrés en leur passant la valeur à filtrer et d’autres informations.

Voici un exemple simple.

La fonction WordPress get_the_title() permet de récupérer le titre d’une publication. C’est la fonction qui est utilisé dans les thèmes classiques ainsi que dans le bloc Titre de la publication pour afficher le titre. Elle est aussi utilisée pour les éléments de menus et à de nombreux autres endroits.

Si on consulte le code source de la fonction (https://developer.wordpress.org/reference/functions/get_the_title/), on peut voir quelque chose d’intéressant dans ses dernières lignes :

...
/**
 * Filters the post title.
 *
 * @since 0.71
 *
 * @param string $post_title The post title.
 * @param int    $post_id    The post ID.
 */
return apply_filters( 'the_title', $post_title, $post_id );

La fonction get_the_title() renvoie le titre de la publication mais en le passant dans tous les filtres enregistrés sur le hook the_title. La présence d’un appel à apply_filters() nous indique qu’on peut se greffer à cet endroit et modifier la donnée $post_title.

On peut donc intervenir à cet endroit en ajoutant notre propre filtre sur the_title (par exemple dans le fichier functions.php du thème).

add_filter( 'the_title', 'custom_title', 10, 2 );
/**
 * Filters the post title
 * 
 * @param  string  $post_title  The post title.
 * @param  int     $post_id     The post ID.
 * @return string  $post_title
 */
function custom_title( $post_title, $post_id ) {
    $post_title = 'Hello World!';
    return $post_title;
}

Cette fonction force le titre de la publication sur ‘Hello World!’. Elle est exécutée à chaque fois que la fonction get_the_title() est appelée.

Capture des titres sur le devant du site
Tous les titres sont forcés.

C’est aussi vrai dans l’administration.

Capture de la liste des articles dans l'administration
Même dans l’administration, les titres sont forcés.

Vous remarquerez que le filtre reçoit une valeur, la modifie et doit la retourner. En effet, get_the_title() retourne le titre de la publication, mais le passe d’abord à un certain nombre de filtres. Plus précisément, elle retourne la valeur de retour de ces filtres. Si l’un de ces filtres ne “rend” pas la valeur du titre, alors la valeur de retour de la fonction get_the_title() sera null.

La priorité

La priorité permet de contrôler l’ordre d’appel des filtres hookés. On a vu que apply_filters() déclenchent toutes les fonctions hookées, et il le fait par ordre de priorité.

Ainsi, les fonctions avec une petite priorité sont exécutées plus tôt que les fonctions avec une grande priorité.

Voici un exemple :

add_filter( 'the_title', 'custom_title', 10, 2 );
function custom_title( $post_title, $post_id ) {
    $post_title = 'Hello World!';
    return $post_title;
}

add_filter( 'the_title', 'custom_title_2', 100, 2 );
function custom_title_2( $post_title, $post_id ) {
    $post_title = 'Hello Universe!';
    return $post_title;
}

Ici, deux filtres très similaires sont hookés sur the_title. L’un change le titre en “Hello World!”, l’autre en “Hello Universe!”. Le premier est hooké avec une priorité de 10 (valeur par défaut), le deuxième avec une priorité de 100.

Le filtre custom_title() est exécuté avant (priorité plus petite) custom_title_2. Il change le titre en “Hello World!” puis custom_title_2 est déclenchée et change le titre en “Hello Universe”.

On a donc “Hello Universe!” sur le devant du site.

Capture des titres sur le devant du site montrant "Hello Universe"
Le filtre à la priorité la plus élevée est exécuté en premier.

Si on met une priorité de 1 sur custom_title_2, alors ce filtre sera exécuté avant custom_title, puis custom_title sera exécuté et changera le titre en “Hello World!”.

Si les deux filtres sont ajoutés sur la même priorité, alors ils seront mis dans la file d’attente des filtres dans l’ordre dans lequel ils seront déclarés. Premier arrivé, premier déclenché.

Important : la priorité est souvent cause de confusion. Si les extensions WordPress ou le code personnalisé déclenchent plusieurs filtres sur les mêmes données, on peut avoir des résultats inattendus : un filtre peut venir “défaire” ce qu’un autre a fait précédemment. Dans ces cas, ajuster la priorité ces filtres peut résoudre les soucis.

Le nombre d’arguments acceptés

Le dernier paramètre de add_filter() est très important. Il indique à WordPress le nombre d’arguments que le filtre est censé recevoir.

Si l’appel à apply_filters() fournit des arguments supplémentaires utiles aux filtres en plus de la valeur à filtrer, il convient de signaler à WordPress combien d’arguments les filtres hookées avec add_filter() s’attendent à recevoir.

Reprenons l’exemple de get_the_title(). En voici l’appel à apply_filters() que l’on trouve dans son code source :

...
/**
 * Filters the post title.
 *
 * @since 0.71
 *
 * @param string $post_title The post title.
 * @param int    $post_id    The post ID.
 */
return apply_filters( 'the_title', $post_title, $post_id );

apply_filters() est appelé avec trois arguments: le nom du hook the_title, la valeur à filtrer $post_title et un argument supplémentaire $post_id.

Si on veut hooker une fonction sur the_title et utiliser le deuxième argument $post_id pour mieux cibler le titre à modifier, alors il faut spécifier à WordPress que le filtre a besoin des deux arguments : la valeur à filtrer et le $post_id.

add_filter( 'the_title', 'custom_title', 10, 2 );
function custom_title( $post_title, $post_id ) {
    if( 123 === $post_id ){
        $post_title = 'Hello World!';
    }
    return $post_title;
}

Ici, avec add_filter( 'the_title', 'custom_title', 10, 2 ); le dernier paramètre indique bien que custom_title() attend 2 arguments.

Attention, omettre le dernier argument provoquera une erreur fatale !

PHP Fatal error:  Uncaught ArgumentCountError: Too few arguments to function custom_title(), 1 passed in .../app/public/wp-includes/class-wp-hook.php on line 326 and exactly 2 expected in .../app/public/wp-content/themes/kawi-child/functions.php:181

L’erreur indique que la fonction custom_title() n’a reçu qu’un seul argument alors qu’elle en attendait 2. Changer la signature de la fonction et rendre le deuxième optionnel ne corrige pas le souci : on n’a plus d’erreur fatale, mais le code ne fonctionne plus car la fonction ne reçoit jamais le second paramètre, donc le if sera toujours faux.

add_filter( 'the_title', 'custom_title', 10 );
/**
 *  Mauvais exemple !
 *  La fonction ne recevra JAMAIS le second paramètre. 
 */
function custom_title( $post_title, $post_id = null ) {
    if( 123 === $post_id ){
        $post_title = 'Hello World!';
    }
    return $post_title;
}

La solution est simple: le filtre utilise les deux arguments fournis par apply_filters(), il faut donc le hooker en spécifiant qu’il attend ces deux arguments.

Pour trouver le nombre d’arguments fournis par un hook, il n’y a pas de secret. Soit c’est documenté (dans la documentation de WordPress ou de l’extension sur lequel vous voulez vous greffer), soit cela ne l’est pas et il faudra trouver l’appel à apply_filters() dans le code source.

Je suis partisan de la lecture du code source. Cela permet aussi de comprendre le contexte dans lequel le hook est utilisé.

La valeur par défaut du dernier paramètre de add_filter() est 1, ce qui signifie que dans tous les cas, la fonction de rappel reçoit un argument : la valeur à filtrer. C’est quand le filtre a besoin des arguments supplémentaires qu’il ne faut surtout pas oublier de le spécifier.

add_action() et do_action()

Ces deux fonctions se comportent exactement comme add_filter() et apply_filters().

  • add_action( string $hook_name, callable $callback, int $priority = 10, int $accepted_args = 1 ) ajoute une action: c’est-à-dire une fonction qui va se déclencher sur un hook précis et exécuter du code arbitraire.
  • do_action( string $hook_name, mixed ...$args ) va déclencher toutes les actions associées au hook $hook_name, en leur passant les arguments $args.

C’est donc exactement le même principe. La seule différence est qu’aucune valeur de retour n’est attendue pour les actions déclenchées.

Voici un autre exemple courant :

wp_insert_post() est une fonction de WordPress qui insère ou met à jour une publication dans la base de données. Elle est déclenchée quand vous utilisez le bouton Enregistrer dans l’éditeur de contenu. Dans le code source de la fonction (https://github.com/WordPress/wordpress-develop/blob/97382397b2bd7c85aef6d4cd1c10bafd397957fc/src/wp-includes/post.php#L4432C10-L4432C24), on peut voir qu’à la toute fin de la procédure, une série de hooks sont déclenchés, et parmi ces derniers se trouve le hook save_post. On trouve cette ligne :

/**
 * Fires once a post has been saved.
 *
 * @since 1.5.0
 *
 * @param int     $post_id Post ID.
 * @param WP_Post $post    Post object.
 * @param bool    $update  Whether this is an existing post being updated.
 */
do_action( 'save_post', $post_id, $post, $update );

Ce qui signifie que quand WordPress arrive sur cette ligne, il va déclencher toutes les actions associées au hook save_post, en leur passant le $post_id, le $post en entier et un booléen $update qui indique si la publication qui vient d’être sauvegardée est nouvelle ou non.

On peut par exemple sauvegarder des métadonnées supplémentaires lors de l’enregistrement :

add_action( 'save_post', 'custom_save_post', 10, 3 );
/**
 * Adds example meta data
 * 
 * @param int     $post_id Post ID.
 * @param WP_Post $post    Post object.
 * @param bool    $update  Whether this is an existing post being updated.
 */
function custom_save_post( $post_id, $post, $update ) {
    if( $update ){
        update_post_meta( $post_id, 'custom_meta', 'Example meta' );
    }
}

Ainsi, à chaque fois qu’une publication est sauvegardée, l’action custom_save_post() sera déclenchée et la métadonnée sera mise à jour si la publication est mise à jour (pas quand elle est créée). L’action reçoit bien $post_id, $post, et $update car la ligne add_action( 'save_post', 'custom_save_post', 10, 3 ); spécifie le nombre d’arguments attendus (3). On peut donc utiliser ces arguments si besoin.

Hooks statiques et hooks dynamiques

Certains noms des hooks de WordPress sont statiques et ne bougeront pas, et d’autres sont dynamiques et peuvent “changer de nom”. Par exemple, toujours dans la fonction wp_insert_post(), on trouve ceci :

/**
 * Fires once a post has been saved.
 *
 * The dynamic portion of the hook name, `$post->post_type`, refers to
 * the post type slug.
 *
 * Possible hook names include:
 *
 *  - `save_post_post`
 *  - `save_post_page`
 *
 * @since 3.7.0
 *
 * @param int     $post_id Post ID.
 * @param WP_Post $post    Post object.
 * @param bool    $update  Whether this is an existing post being updated.
 */
do_action( "save_post_{$post->post_type}", $post_id, $post, $update );

Ici, le nom du hook est "save_post_{$post->post_type}", entre guillements, ce qui signifie que la chaine de caractère sera évaluée et la valeur de $post->post_type remplacée automatiquement.

Lors de la sauvegarde d’un article, save_post_post sera donc déclenché. Lors de la sauvegarde d’une page, c’est save_post_page qui sera déclenché, et ainsi de suite.

Ces hooks de WordPress au nom dynamique permettent de mieux cibler les actions et de simplifier leur logique. Ainsi, les deux exemples suivants sont équivalents, mais on économise un if et on évite de déclencher l’action sur chaque publication inutilement dans le deuxième.

add_action( 'save_post', 'custom_save_post', 10, 3 );
function custom_save_post( $post_id, $post, $update ) {
    if( 'post' === $post->post_type ){
        update_post_meta( $post_id, 'custom_meta', 'Example meta' );
    }
}

add_action( 'save_post_post', 'custom_dynamic_save_post', 10, 3 );
function custom_dynamic_save_post( $post_id, $post, $update ) {
    update_post_meta( $post_id, 'custom_meta', 'Example meta' );
}

Les autres fonctions

Dans cet article, on a vu que les fonctions essentielles de la plugin API, mais il existe d’autres fonctions utiles :

  • has_action() et has_filter() vont vérifier si une fonction donnée est hookée sur un hook donné.
  • do_action_ref_array() et apply_filters_ref_array() fonctionnent comme do_action() et apply_filters() mais permettent de passer les arguments dans un seul tableau.
  • did_action() retourne le nombre de fois qu’un certain hook a été déclenché.
  • current_filter() et current_action() renvoient le nom du filtre sur lequel la fonction qui l’appelle est hookée.
  • remove_action() et remove_filter() permettent d’enlever un filtre ou une action hookée, sur une priorité donnée.
  • remove_all_actions() et remove_all_filters() permet de supprimer toutes les actions ou filtres sur un hook donné.

Les hooks de WordPress et l’OOP

Dans un contexte de programmation orientée objet (OOP pour Object Oriented Programming), il faut spécifier le contexte dans lequel se trouve la méthode à hooker. Dans 99% des cas, ce contexte est l’objet courant $this.

Voici un exemple :

class Example {
    public function __construct(){}

    public function register_hooks(){
        add_action( 'save_post', array( $this, 'save_post' ), 10, 3 );
    }

    public function save_post( $post_id, $post, $update ) {
        update_post_meta( $post_id, 'custom_meta', 'Example Class meta' );
    }
}

$example = new Example();
$example->register_hooks();

Dans la methode register_hooks() de la classe, on utilise add_action() en spécifiant l’action à ajouter sous forme de tableau plutôt que de chaîne. Ce tableau contient le contexte ($this) et la méthode à utiliser (save_post). Ainsi, on hooke bien la méthode save_post() de l’objet courant sur le hook save_post.
Les autres arguments sont inchangés.

Trouver les hooks dont vous avez besoin

Pour trouver les hooks dont on a besoin, il faut soit consulter la documentation, soit explorer le code source, tout simplement. Toute documentation, aussi fournie soit-elle peut être incomplète. Alors que le code source, lui, ne vous mentira jamais.

C’est uniquement en repérant le hook dont on a besoin dans le code source qu’on va pouvoir être sûr du nombre d’arguments passés aux actions et filtres. Aussi, on a accès au contexte dans lequel le hook est déclenché, ce qui peut nous aider à trouver le bon hook et mieux comprendre le fonctionnement de l’extension ou du thème.

Il ne faut surtout pas hésiter à ouvrir la documentation ou le code source de WordPress et à lire les fonctions que vous rencontrez dans votre thème ou vos extensions. C’est un peu laborieux, mais cela augmentera grandement votre connaissance du fonctionnement interne de WordPress.

Je vous invite à ouvrir une grosse extension comme WooCommerce dans votre éditeur de code, faire une recherche sur apply_filters pour voir les dizaines de résultats qui révèlent donc des dizaines d’endroits où on peut se greffer pour modifier le comportement de l’extension.

Conclusion

Au final, les quatres fonctions centrales de la plugin API sont relativement simples :

  • un filtre est une fonction qui modifie une donnée,
  • une action est une fonction qui exécute du code arbitraire
  • apply_filters() déclenche des filtres associés à un hook, en leur passant des données
  • add_filter() ajoute des filtres à déclencher sur un hook donné
  • do_action() déclenche des actions associés à un hook, en leur passant des données
  • add_action() ajoute une action à déclencher sur un hook donné
  • Lors de l’utilisation d’un filtre, n’oubliez pas de retourner la donnée qui est passée.
  • la priorité est importante pour manipuler l’ordre de déclenchement des fonctions hookées
  • il est très important de spécifier le bon nombre d’arguments à passer à vos actions et filtres dans les appels à add_action() et add_filter().

Le système de hooks de WordPress est fondamental, et il est très important de bien comprendre comment il fonctionne. J’espère que cet article vous a aidé à y voir plus clair, et je vous invite à explorer le code source à la recherche des hooks de WordPress !

Apprenez à développer un Block Theme

Inscrivez-vous pour comprendre les Block Themes, prendre en main l'éditeur de site de WordPress, et développer votre premier Block Theme.