# wp_cache_add contre wp_cache_set : la nuance qui provoque des bugs de cache

> Confondre wp_cache_add et wp_cache_set introduit un bug de cache silencieux qui sert de vieilles données après une mise à jour. Fonctionnement interne et pièges.

- Auteur : Clément Hadrot
- Publié le : 2022-02-02
- Mis à jour le : 2022-02-02
- Catégorie : Performance
- URL : https://wpmoderne.dev.wordpress-developpement.fr/performance/wp-cache-add-contre-wp-cache-set-nuance/

## L’essentiel

- wp_cache_add() n'écrit que si la clé n'existe pas déjà en cache
- wp_cache_set() écrase systématiquement la valeur existante
- Utiliser add au lieu de set peut figer une donnée obsolète en cache

Est-ce qu'un développeur qui écrit `wp_cache_add()` à la place de `wp_cache_set()` introduit vraiment un bug ? La réponse est oui, et ce genre d'erreur est particulièrement vicieux parce qu'elle ne se manifeste pas immédiatement : le code fonctionne parfaitement au premier appel, puis semble ignorer silencieusement toute mise à jour ultérieure de la donnée.

C'est exactement ce qui s'est produit sur une extension maison développée pour un site de recettes de cuisine, qui mettait en cache le nombre d'ingrédients d'une recette pour éviter de le recalculer à chaque affichage. Après une modification de recette, l'ancien nombre continuait à s'afficher pendant plusieurs heures, jusqu'à expiration naturelle du cache.

## Le fonctionnement interne des deux fonctions

`wp_cache_set( $key, $data, $group, $expire )` écrit systématiquement la valeur fournie dans le cache d'objet, qu'une valeur existe déjà pour cette clé ou non. C'est le comportement attendu pour la grande majorité des usages : on veut que la valeur la plus récente remplace la précédente.

`wp_cache_add( $key, $data, $group, $expire )` se comporte différemment : la fonction vérifie d'abord si une valeur existe déjà pour cette clé et ce groupe. Si c'est le cas, elle ne fait rien et retourne `false`, sans jamais écraser la valeur existante. Ce comportement est voulu et utile dans certains contextes précis, notamment pour éviter qu'une opération concurrente ne réécrive une valeur déjà posée par un autre processus, un usage proche d'un verrou léger.

## Le piège rencontré dans le code réel

Le développeur de l'extension avait choisi `wp_cache_add()` en pensant, par analogie avec `update_option()` et `add_option()`, qu'il s'agissait simplement d'une variante plus prudente d'écriture en cache, sans en comprendre la conséquence exacte sur une mise à jour :

```
function recette_get_nombre_ingredients( $recette_id ) {
    $cache_key = 'nb_ingredients_' . $recette_id;
    $nombre    = wp_cache_get( $cache_key, 'recettes' );

    if ( false === $nombre ) {
        $nombre = recette_compter_ingredients( $recette_id );
        wp_cache_add( $cache_key, $nombre, 'recettes', HOUR_IN_SECONDS );
    }

    return $nombre;
}
```

Ce code semble correct à première lecture : `wp_cache_get()` précède bien l'écriture, et `wp_cache_add()` n'est appelé qu'en cas d'absence en cache. Le problème apparaît ailleurs, dans la fonction déclenchée à la sauvegarde d'une recette modifiée, qui tentait de forcer la mise à jour immédiate du cache après un changement d'ingrédients :

> L'essentiel à retenir : wp_cache_add() n'écrit que si la clé n'existe pas déjà en cache ; wp_cache_set() écrase systématiquement la valeur existante ; Utiliser add au lieu de set peut figer une donnée obsolète en cache

```
add_action( 'save_post_recette', function ( $post_id ) {
    $nombre = recette_compter_ingredients( $post_id );
    wp_cache_add( 'nb_ingredients_' . $post_id, $nombre, 'recettes', HOUR_IN_SECONDS );
} );
```

Ici, l'usage de `wp_cache_add()` était une erreur pure : si une valeur existait déjà en cache pour cette clé, ce qui est presque toujours le cas pour une recette déjà consultée, la fonction ne faisait rien du tout. La nouvelle valeur, pourtant calculée correctement, n'était jamais écrite. L'ancienne valeur restait en cache jusqu'à expiration naturelle de l'heure définie.

## Le correctif

Le correctif consistait simplement à remplacer `wp_cache_add()` par `wp_cache_set()` dans le hook de sauvegarde, où l'intention réelle était bien d'écraser la valeur existante :

```
add_action( 'save_post_recette', function ( $post_id ) {
    $nombre = recette_compter_ingredients( $post_id );
    wp_cache_set( 'nb_ingredients_' . $post_id, $nombre, 'recettes', HOUR_IN_SECONDS );
} );
```

La fonction de lecture, elle, conservait `wp_cache_add()` à juste titre : dans ce contexte précis, on ne veut écrire que si aucune valeur n'existe déjà, exactement le comportement recherché lors d'une première lecture.

## Comment repérer ce type de bug plus tôt

- Se poser systématiquement la question « est-ce que je veux écraser une valeur existante, ou seulement écrire si rien n'existe ? » avant de choisir entre les deux fonctions.
- Ajouter un test unitaire simple qui modifie une donnée puis vérifie que le cache reflète bien la nouvelle valeur immédiatement après.
- Se méfier de toute fonction de mise en cache appelée depuis un hook de sauvegarde ou de mise à jour : c'est précisément le contexte où l'écrasement est presque toujours l'intention recherchée.

## En résumé

La différence entre `wp_cache_add()` et `wp_cache_set()` tient à un seul comportement, mais ses conséquences en production peuvent rester invisibles pendant des heures, le temps que le cache expire naturellement. Ce cas ne traite pas le choix du backend Redis ou Memcached, qui n'a aucune influence sur ce comportement : la logique d'ajout contre écrasement est définie au niveau de l'API de cache de WordPress elle-même, avant même d'atteindre le backend configuré.
