# Un contrôleur REST qui pagine avec X-WP-Total sur une ressource maison

> Les clients REST habitués au cœur de WordPress attendent des en-têtes précis pour paginer. Une route entièrement personnalisée doit les fournir elle-même.

- Auteur : Clément Hadrot
- Publié le : 2025-12-14
- Mis à jour le : 2025-12-14
- Catégorie : Extensions
- URL : https://wpmoderne.dev.wordpress-developpement.fr/extensions/controleur-rest-pagine-x-wp-total-ressource-maison/

## L’essentiel

- X-WP-Total et X-WP-TotalPages ne sont jamais ajoutés automatiquement hors des routes natives
- Le total doit être calculé indépendamment de la page demandée
- Les liens de navigation via l'en-tête Link facilitent l'intégration côté client

Une extension d'inventaire exposait une route REST personnalisée listant des références de pièces détachées, consommée par une application mobile développée par une autre équipe. Cette équipe a rapidement signalé que leur bibliothèque cliente, habituée aux routes natives de WordPress comme `/wp/v2/posts`, ne parvenait pas à savoir combien de pages restaient à charger, faute d'en-têtes de pagination dans la réponse.

Ce comportement n'a rien d'un bug : les routes personnalisées enregistrées via `register_rest_route()` ne bénéficient d'aucune pagination automatique. Le cœur de WordPress ajoute lui-même les en-têtes `X-WP-Total` et `X-WP-TotalPages` sur ses propres contrôleurs, mais une route entièrement maison doit reproduire ce comportement explicitement.

## Ce que le client REST attend

La convention établie par le cœur de WordPress, et reprise par la plupart des bibliothèques clientes construites autour de l'API REST, repose sur deux en-têtes de réponse HTTP : `X-WP-Total`, qui indique le nombre total d'éléments correspondant à la requête, indépendamment de la pagination appliquée, et `X-WP-TotalPages`, qui indique le nombre total de pages compte tenu du paramètre `per_page` utilisé.

## Construire le contrôleur avec pagination

La route enregistrée pour cette ressource d'inventaire ressemblait à ceci avant correction, un simple retour de tableau sans aucune information de pagination :

```
register_rest_route( 'inventaire/v1', '/pieces', array(
    'methods'             => 'GET',
    'callback'            => 'inventaire_lister_pieces',
    'permission_callback' => 'inventaire_verifier_permission',
) );
```

La version corrigée calcule le total indépendamment de la page demandée, puis construit la réponse avec les en-têtes attendus, en s'appuyant sur la classe `WP_REST_Response` plutôt qu'un simple tableau brut :

> L'essentiel à retenir : X-WP-Total et X-WP-TotalPages ne sont jamais ajoutés automatiquement hors des routes natives ; Le total doit être calculé indépendamment de la page demandée ; Les liens de navigation via l'en-tête Link facilitent l'intégration côté client

```
function inventaire_lister_pieces( WP_REST_Request $request ) {
    global $wpdb;

    $page     = max( 1, (int) $request->get_param( 'page' ) ?: 1 );
    $per_page = min( 100, max( 1, (int) $request->get_param( 'per_page' ) ?: 20 ) );
    $offset   = ( $page - 1 ) * $per_page;

    $total = (int) $wpdb->get_var(
        "SELECT COUNT(*) FROM {$wpdb->prefix}inventaire_pieces"
    );

    $lignes = $wpdb->get_results(
        $wpdb->prepare(
            "SELECT * FROM {$wpdb->prefix}inventaire_pieces
             ORDER BY reference ASC
             LIMIT %d OFFSET %d",
            $per_page,
            $offset
        )
    );

    $total_pages = (int) ceil( $total / $per_page );

    $response = new WP_REST_Response( $lignes );
    $response->header( 'X-WP-Total', (string) $total );
    $response->header( 'X-WP-TotalPages', (string) $total_pages );

    return $response;
}
```

Le calcul du total via un `COUNT(*)` distinct, exécuté sans les clauses `LIMIT` et `OFFSET`, est indispensable : il ne doit jamais dépendre de la page demandée, sous peine de renvoyer un total qui varie selon la position de navigation, ce qui casserait toute logique de pagination côté client.

## Ajouter les liens de navigation

Au-delà des deux en-têtes de comptage, le cœur de WordPress ajoute également un en-tête `Link` standard, au format défini par la RFC 8288, qui fournit directement les URL de la page suivante et de la page précédente, évitant au client de les reconstruire lui-même :

```
$base_url = rest_url( 'inventaire/v1/pieces' );

if ( $page > 1 ) {
    $lien_precedent = add_query_arg( array( 'page' => $page - 1, 'per_page' => $per_page ), $base_url );
    $response->link_header( 'prev', $lien_precedent );
}

if ( $page < $total_pages ) {
    $lien_suivant = add_query_arg( array( 'page' => $page + 1, 'per_page' => $per_page ), $base_url );
    $response->link_header( 'next', $lien_suivant );
}
```

La méthode `link_header()`, disponible directement sur `WP_REST_Response`, gère automatiquement le formatage correct de l'en-tête, sans avoir à concaténer soi-même la syntaxe attendue par la RFC.

### Limiter per_page à une valeur raisonnable

Un point de sécurité et de performance souvent oublié : sans limite haute explicite sur `per_page`, un client mal intentionné ou simplement mal configuré pourrait demander plusieurs milliers d'éléments en une seule requête, générant une charge disproportionnée côté serveur. Le plafond de 100 appliqué dans l'exemple ci-dessus reprend la convention utilisée par les routes natives de WordPress elles-mêmes.

- Toujours calculer `X-WP-Total` via un comptage indépendant de la pagination appliquée, jamais via la taille du tableau de résultats retournés.
- Ajouter systématiquement `X-WP-TotalPages`, calculé à partir du total et de `per_page`, jamais codé en dur.
- Plafonner `per_page` à une valeur raisonnable, en cohérence avec les conventions déjà appliquées par le cœur de WordPress sur ses propres routes.

> Un test que nous ajoutons désormais systématiquement à toute nouvelle route REST personnalisée : vérifier avec un simple appel `curl -I` que les en-têtes de pagination sont bien présents, avant même de tester le contenu de la réponse elle-même.

## En résumé

Une route REST personnalisée n'hérite d'aucune pagination automatique, contrairement aux routes natives de WordPress. Reproduire les en-têtes `X-WP-Total`, `X-WP-TotalPages` et, dans l'idéal, `Link`, ne demande que quelques lignes de code supplémentaires, mais rend la ressource immédiatement compatible avec les attentes des bibliothèques clientes déjà habituées aux conventions du cœur de WordPress.
