# Post meta, user meta, term meta : les bonnes pratiques pour vos extensions

> register_post_meta, sérialisation, requêtes meta_query lentes : comment gérer les métadonnées WordPress sans piéger les performances ni la sécurité.

- Auteur : Clément Hadrot
- Publié le : 2021-03-09
- Mis à jour le : 2021-03-09
- Catégorie : Extensions
- URL : https://wpmoderne.dev.wordpress-developpement.fr/extensions/metadata-post-user-term-bonnes-pratiques/

## L’essentiel

- register_post_meta expose la donnée à l'API REST proprement
- Éviter meta_query sur de gros volumes
- Une table dédiée bat toujours la table wp_postmeta au-delà d'un certain volume

Les métadonnées sont le mécanisme le plus utilisé et le plus mal maîtrisé de WordPress. La fonction `update_post_meta()` est tellement simple d'usage qu'elle invite à l'excès : des extensions stockent des tableaux de plusieurs centaines d'éléments dans une seule clé de métadonnée, puis s'étonnent que les listes d'articles de l'administration deviennent lentes après quelques mois d'utilisation.

Cet article revient sur le fonctionnement réel des tables de métadonnées, sur l'API moderne `register_post_meta()` introduite en 2017 mais encore trop peu utilisée, et sur les limites à connaître avant qu'une extension ne devienne un problème de performance pour le site qui l'héberge.

## Comment WordPress stocke réellement une métadonnée

Les tables `wp_postmeta`, `wp_usermeta`, `wp_termmeta` et `wp_commentmeta` partagent la même structure : un identifiant, l'identifiant de l'objet parent, une clé et une valeur. Toute valeur qui n'est pas un scalaire (tableau, objet) est automatiquement sérialisée avec `maybe_serialize()` avant l'enregistrement, et désérialisée à la lecture.

```
update_post_meta( $post_id, 'acme_options', [
    'couleur'  => 'bleu',
    'taille'   => 'M',
    'variants' => [ 'rouge', 'vert', 'jaune' ],
] );

$options = get_post_meta( $post_id, 'acme_options', true );
// $options['couleur'] === 'bleu'
```

Ce confort a un coût : une valeur sérialisée est stockée comme une seule chaîne de caractères opaque pour MySQL. Impossible d'écrire une requête SQL qui filtre sur `couleur` à l'intérieur de ce tableau sans désérialiser côté PHP l'ensemble des lignes candidates. Pour toute donnée qui doit être filtrable ou triable, mieux vaut une clé de métadonnée dédiée par valeur simple plutôt qu'un tableau imbriqué.

```
// Préférable si "couleur" doit être filtrable :
update_post_meta( $post_id, 'acme_couleur', 'bleu' );
update_post_meta( $post_id, 'acme_taille', 'M' );
```

## register_post_meta : la déclaration explicite

Depuis WordPress 4.9.8, `register_post_meta()` permet de déclarer une métadonnée avec son type, sa visibilité dans l'API REST et sa fonction de sanitisation, plutôt que de la manipuler de façon implicite.

```
add_action( 'init', function () {
    register_post_meta( 'produit', 'acme_reference', [
        'type'              => 'string',
        'single'            => true,
        'show_in_rest'      => true,
        'sanitize_callback' => 'sanitize_text_field',
        'auth_callback'     => function () {
            return current_user_can( 'edit_posts' );
        },
    ] );
} );
```

`show_in_rest` expose automatiquement la métadonnée dans l'objet retourné par l'API REST, sous `meta.acme_reference`, sans écrire de contrôleur personnalisé. L'`auth_callback` contrôle qui a le droit de la modifier via cette même API : un oubli ici rend la métadonnée modifiable par n'importe quel utilisateur authentifié disposant d'un accès en écriture minimal, ce qui est rarement l'intention recherchée.

> L'essentiel à retenir : register_post_meta expose la donnée à l'API REST proprement ; Éviter meta_query sur de gros volumes ; Une table dédiée bat toujours la table wp_postmeta au-delà d'un certain volume

## Le piège classique de meta_query

Filtrer une liste d'articles par métadonnée via `WP_Query` et l'argument `meta_query` fonctionne bien à petite échelle, mais se dégrade fortement au-delà de quelques dizaines de milliers d'entrées, car la table `wp_postmeta` n'est indexée efficacement que sur la clé, pas sur la valeur.

```
$query = new WP_Query( [
    'post_type'  => 'produit',
    'meta_query' => [
        [
            'key'     => 'acme_stock',
            'value'   => 0,
            'compare' => '>',
            'type'    => 'NUMERIC',
        ],
    ],
] );
```

Cette requête génère une jointure SQL sur `wp_postmeta` qui doit convertir chaque valeur en nombre à la volée, sans pouvoir s'appuyer sur un index numérique. Sur un catalogue de dix mille produits, le temps de réponse peut passer de quelques millisecondes à plusieurs centaines. Deux solutions existent : mettre en cache le résultat via un objet cache (`wp_cache_set`) quand la donnée change peu, ou migrer vers une table SQL personnalisée dès que le volume et la fréquence de filtrage le justifient.

### Créer une table dédiée quand c'est justifié

```
global $wpdb;

$wpdb->query( "
    CREATE TABLE {$wpdb->prefix}acme_stock (
        post_id BIGINT UNSIGNED NOT NULL,
        quantite INT NOT NULL DEFAULT 0,
        PRIMARY KEY (post_id),
        KEY quantite (quantite)
    ) {$wpdb->get_charset_collate()}
" );
```

Une table dédiée avec un index sur la colonne filtrée transforme une requête de plusieurs centaines de millisecondes en une requête de quelques millisecondes. Ce chantier ne se justifie pas systématiquement : il devient pertinent au-delà de quelques dizaines de milliers d'objets filtrés régulièrement, pas avant.

## User meta et term meta : les mêmes règles

Les fonctions `update_user_meta()`, `get_user_meta()`, `update_term_meta()` et `get_term_meta()` suivent une logique strictement identique. `register_meta()` couvre d'ailleurs les quatre types d'objets avec un seul appel générique :

```
register_meta( 'term', 'acme_couleur_theme', [
    'type'         => 'string',
    'single'       => true,
    'show_in_rest' => true,
] );
```

Un piège spécifique au term meta : la table `wp_termmeta` n'existe que depuis WordPress 4.4 (2015). Toute extension encore compatible avec des versions antérieures (ce qui devient rarissime en 2021) doit prévoir une table de repli, mais ce cas est aujourd'hui purement académique.

> La règle qu'on applique systématiquement : une métadonnée qui sert uniquement à l'affichage reste dans postmeta sans complication ; une métadonnée qui sert à filtrer, trier ou agréger des milliers d'enregistrements mérite sa propre table dès la conception, pas en urgence six mois plus tard.

## En résumé

`register_post_meta()` et ses équivalents apportent structure, sécurité et intégration REST à un mécanisme trop souvent utilisé de façon implicite. Le vrai arbitrage à faire dès la conception d'une extension porte sur le volume : au-delà de quelques dizaines de milliers d'objets à filtrer régulièrement, la table de métadonnées générique montre ses limites et une table SQL dédiée devient le choix le plus sain.
