Le WordPress d'aujourd'hui, décodé pour les développeurs

Extensions

Structurer ses routes avec WP_REST_Controller plutôt que des callbacks

Un plugin qui expose cinq ressources REST via des fonctions anonymes empilées devient vite illisible. La classe abstraite fournie par le cœur de WordPress structure ce chaos.

Par WordPress Développement • 30 septembre 2026 • 9 min de lecture • Aucun commentaire
Structurer ses routes avec WP_REST_Controller plutôt que des callbacks

Une extension de gestion de flotte de véhicules pour des entreprises de location expose, au fil des versions, de plus en plus de ressources via l’API REST de WordPress : véhicules, réservations, conducteurs, incidents. Chaque nouvelle ressource a été ajoutée par un appel supplémentaire à register_rest_route() avec une fonction anonyme en callback, jusqu’à atteindre un fichier de plus de huit cents lignes où il devient difficile de retrouver la logique propre à chaque ressource, tant tout est mélangé dans un seul espace de noms de fichier.

La refonte adoptée s’appuie sur WP_REST_Controller, la classe abstraite que WordPress fournit lui-même en interne pour structurer ses propres points de terminaison REST, comme ceux des articles ou des utilisateurs. Cette classe n’apporte aucune magie particulière : elle impose surtout une convention de nommage de méthodes qui rend le code de chaque ressource immédiatement reconnaissable d’un développeur à l’autre.

Le squelette d’un contrôleur de ressource

L'essentiel à retenir : WP_REST_Controller impose une convention de méthodes, pas un carcan rigide ; Chaque ressource devient une classe autonome, testable isolément ; Le schéma exposé documente automatiquement la ressource pour l'API découverte

Un contrôleur de ressource commence par un constructeur qui fixe l’espace de noms et la base de la route, puis une méthode register_routes() qui enregistre les deux familles de routes d’une ressource : la collection (/vehicules) et l’élément (/vehicules/12). Voici le squelette, limité à la lecture et à la création :

class Flotte_Vehicules_Controller extends WP_REST_Controller {

	public function __construct() {
		$this->namespace = 'flotte/v1';
		$this->rest_base = 'vehicules';
	}

	public function register_routes() {
		register_rest_route( $this->namespace, '/' . $this->rest_base, array(
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'get_items' ),
				'permission_callback' => array( $this, 'get_items_permissions_check' ),
				'args'                => $this->get_collection_params(),
			),
			array(
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => array( $this, 'create_item' ),
				'permission_callback' => array( $this, 'create_item_permissions_check' ),
				'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
			),
			'schema' => array( $this, 'get_public_item_schema' ),
		) );

		register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
			'args'   => array(
				'id' => array(
					'description' => __( 'Identifiant du véhicule.', 'flotte' ),
					'type'        => 'integer',
				),
			),
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'get_item' ),
				'permission_callback' => array( $this, 'get_item_permissions_check' ),
				'args'                => array(
					'context' => $this->get_context_param( array( 'default' => 'view' ) ),
				),
			),
			'schema' => array( $this, 'get_public_item_schema' ),
		) );
	}
}

add_action( 'rest_api_init', function () {
	$controleur = new Flotte_Vehicules_Controller();
	$controleur->register_routes();
} );

Trois éléments de ce squelette font le travail de structuration. Le tableau d’arguments de la collection provient de get_collection_params(), qui fournit déjà context, page, per_page et search, avec leur schéma. Les arguments de création sont déduits du schéma de la ressource par get_endpoint_args_for_item_schema() : on ne les écrit donc pas deux fois. Enfin la clé schema permet à WordPress de publier le schéma de la ressource, y compris dans la réponse à une requête OPTIONS.

Les méthodes que l’on surcharge

La classe de base définit une convention de noms, et chaque méthode a un rôle précis. On ne surcharge que celles dont la ressource a besoin : une ressource en lecture seule n’implémente ni create_item() ni delete_item(), et la classe de base renvoie dans ce cas une erreur « non implémenté » plutôt que de laisser un trou.

  • get_items_permissions_check(), get_item_permissions_check(), create_item_permissions_check() : une méthode de permission par action, qui retourne true ou un WP_Error.
  • get_items(), get_item(), create_item(), update_item(), delete_item() : la logique de chaque action.
  • prepare_item_for_response() : transforme l’objet interne (ici un article) en tableau conforme au schéma.
  • get_item_schema() : décrit la ressource au format JSON Schema.

Permissions, collection et préparation de la réponse

Les méthodes suivantes se placent à l’intérieur de la classe. Chaque action a sa propre vérification. L’intérêt est de pouvoir les tester séparément et de lire en quelques lignes qui a le droit de faire quoi :

	public function get_items_permissions_check( $request ) {
		if ( ! current_user_can( 'gerer_flotte' ) ) {
			return new WP_Error(
				'rest_forbidden',
				__( 'Vous ne pouvez pas consulter la flotte.', 'flotte' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}
		return true;
	}

	public function get_items( $request ) {
		$requete = new WP_Query( array(
			'post_type'      => 'vehicule',
			'post_status'    => 'publish',
			'posts_per_page' => $request['per_page'],
			'paged'          => $request['page'],
			's'              => $request['search'],
		) );

		$vehicules = array();
		foreach ( $requete->posts as $post ) {
			$donnees     = $this->prepare_item_for_response( $post, $request );
			$vehicules[] = $this->prepare_response_for_collection( $donnees );
		}

		$reponse = rest_ensure_response( $vehicules );
		$reponse->header( 'X-WP-Total', (int) $requete->found_posts );
		$reponse->header( 'X-WP-TotalPages', (int) $requete->max_num_pages );

		return $reponse;
	}

	public function prepare_item_for_response( $post, $request ) {
		$champs  = $this->get_fields_for_response( $request );
		$donnees = array();

		if ( rest_is_field_included( 'id', $champs ) ) {
			$donnees['id'] = (int) $post->ID;
		}
		if ( rest_is_field_included( 'immatriculation', $champs ) ) {
			$donnees['immatriculation'] = (string) get_post_meta( $post->ID, '_immatriculation', true );
		}
		if ( rest_is_field_included( 'kilometrage', $champs ) ) {
			$donnees['kilometrage'] = (int) get_post_meta( $post->ID, '_kilometrage', true );
		}

		$contexte = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$donnees  = $this->add_additional_fields_to_object( $donnees, $request );
		$donnees  = $this->filter_response_by_context( $donnees, $contexte );

		return rest_ensure_response( $donnees );
	}

La capacité gerer_flotte est ici une capacité métier, à ajouter aux rôles concernés. Le code HTTP renvoyé par rest_authorization_required_code() est 401 pour un visiteur non connecté et 403 pour un utilisateur connecté mais non autorisé, ce qui permet au front de distinguer « connectez-vous » de « vous n’avez pas le droit ». Les fonctions get_fields_for_response() et rest_is_field_included(), disponibles depuis WordPress 5.3, font respecter le paramètre _fields : le client peut ne demander que l’immatriculation, et la réponse ne calcule que ce champ.

Le schéma, source de vérité de la ressource

Cette méthode se place elle aussi dans la classe. Le schéma est la pièce la plus rentable du contrôleur : il sert à la fois à documenter la ressource, à valider les données de création et à filtrer les champs selon le contexte.

	public function get_item_schema() {
		if ( $this->schema ) {
			return $this->add_additional_fields_schema( $this->schema );
		}

		$this->schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'vehicule',
			'type'       => 'object',
			'properties' => array(
				'id'              => array(
					'description' => __( 'Identifiant unique du véhicule.', 'flotte' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'immatriculation' => array(
					'description' => __( "Plaque d'immatriculation.", 'flotte' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'required'    => true,
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_text_field',
					),
				),
				'kilometrage'     => array(
					'description' => __( 'Kilométrage relevé.', 'flotte' ),
					'type'        => 'integer',
					'minimum'     => 0,
					'context'     => array( 'view', 'edit' ),
				),
			),
		);

		return $this->add_additional_fields_schema( $this->schema );
	}

Les clés context déterminent quels champs apparaissent en contexte view (public) ou edit (éditeur). La clé readonly exclut le champ des arguments de création. La clé arg_options permet de surcharger les callbacks de validation et de nettoyage déduits du schéma pour un champ précis.

Tester un contrôleur isolément

Comme chaque ressource est une classe, on peut la tester sans charger le reste de l’extension. Avec la suite de tests de WordPress, la fonction rest_do_request() simule une requête complète, permissions comprises :

class Flotte_Vehicules_Controller_Test extends WP_UnitTestCase {

	public function test_liste_refusee_aux_anonymes() {
		wp_set_current_user( 0 );

		$requete  = new WP_REST_Request( 'GET', '/flotte/v1/vehicules' );
		$reponse  = rest_do_request( $requete );

		$this->assertSame( 401, $reponse->get_status() );
	}
}

Cas concret et pièges

Dans l’extension de location, la migration s’est faite ressource par ressource : d’abord les véhicules, puis les réservations, en conservant les anciennes routes jusqu’à ce que le front soit mis à jour. Chaque contrôleur est devenu un fichier d’une centaine de lignes, et le fichier de huit cents lignes a disparu sans qu’aucune adresse publique ne change. Quelques pièges reviennent :

  • Oublier la clé schema dans register_rest_route() : la route fonctionne, mais l’API de découverte ne décrit plus la ressource.
  • Muter $this->schema sans précaution. Le schéma est mis en cache dans la propriété ; le retourner via add_additional_fields_schema() permet de prendre en compte les champs ajoutés par register_rest_field() sans le modifier.
  • Faire entrer de la logique métier dans le contrôleur. Sa tâche est de traduire HTTP en appels à votre code (calcul, dépôt de données) et inversement ; les règles de gestion vivent ailleurs.
  • Retourner un objet interne au lieu d’un tableau préparé expose des champs que le schéma n’a jamais déclarés.

Quand ne pas l’utiliser ? Pour une route unique et simple (un point de santé, un webhook), une fonction avec register_rest_route() reste plus lisible : la classe de base n’apporte rien si l’on n’a ni collection, ni schéma, ni plusieurs actions.

Une convention n’est pas un carcan : elle évite simplement de redécouvrir, dans chaque fichier, où l’on range la permission, le schéma et la préparation de la réponse.

Conclusion

WP_REST_Controller n’ajoute pas de fonctionnalité, il ajoute de la lisibilité : une ressource, une classe, des méthodes aux noms prévisibles, un schéma qui documente et valide. C’est la même organisation que celle des contrôleurs du cœur pour les articles ou les utilisateurs, ce qui permet à tout développeur WordPress de s’y retrouver sans guide. Commencez par la ressource la plus encombrante de votre fichier de routes, migrez-la, testez-la, puis passez à la suivante.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Partager :

À propos de l'auteur

WordPress Développement

Développeur WordPress, passionné par Elementor, le FSE et l’automatisation par IA.

Voir tous ses articles

Dans la même veine

À lire aussi