Créer un plugin WordPress personnalisé : Architecture et bonnes pratiques
Introduction Le développement de plugins WordPress a considérablement évolué en 2025. Avec plus de 92,81%…
L’API REST de WordPress est devenue un composant essentiel pour créer des applications modernes, des interfaces découplées et des intégrations tierces. En 2025, la sécurité des endpoints personnalisés est plus critique que jamais, avec une attention particulière portée à l’authentification, la validation des données et la limitation du débit. Ce guide complet vous accompagnera dans la création d’endpoints REST API sécurisés et performants.
L’API REST WordPress suit les conventions RESTful standards :
GET : Récupérer des donnéesPOST : Créer de nouvelles donnéesPUT/PATCH : Mettre à jour des données existantesDELETE : Supprimer des donnéesLes endpoints WordPress suivent cette structure :
https://example.com/wp-json/namespace/v1/resource
Voici les composants essentiels d’un endpoint REST :
Enregistrement d'un endpoint REST personnalisé
/
namespace MonPluginAPI;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class EndpointExample {
/
Namespace de l'API
/
private $namespace = 'mon-plugin/v1';
/
Constructeur
/
public function construct() {
addaction( 'restapiinit', array( $this, 'registerroutes' ) );
}
/
Enregistrer les routes
/
public function registerroutes() {
// Route GET simple
registerrestroute( $this->namespace, '/items', array(
'methods' => WPRESTServer::READABLE, // GET
'callback' => array( $this, 'getitems' ),
'permissioncallback' => array( $this, 'getitemspermissionscheck' ),
'args' => $this->getcollectionparams(),
) );
// Route POST
registerrestroute( $this->namespace, '/items', array(
'methods' => WPRESTServer::CREATABLE, // POST
'callback' => array( $this, 'createitem' ),
'permissioncallback' => array( $this, 'createitempermissionscheck' ),
'args' => $this->getendpointargsforitemschema(),
) );
// Route pour un item spécifique (GET, PUT, DELETE)
registerrestroute( $this->namespace, '/items/(?Pd+)', array(
array(
'methods' => WPRESTServer::READABLE, // GET
'callback' => array( $this, 'getitem' ),
'permissioncallback' => array( $this, 'getitempermissionscheck' ),
'args' => array(
'id' => array(
'description' => ( 'Identifiant unique de l'item.', 'mon-plugin' ),
'type' => 'integer',
'required' => true,
),
),
),
array(
'methods' => WPRESTServer::EDITABLE, // PUT/PATCH
'callback' => array( $this, 'updateitem' ),
'permissioncallback' => array( $this, 'updateitempermissionscheck' ),
'args' => $this->getendpointargsforitemschema( false ),
),
array(
'methods' => WPRESTServer::DELETABLE, // DELETE
'callback' => array( $this, 'deleteitem' ),
'permissioncallback' => array( $this, 'deleteitempermissionscheck' ),
),
'schema' => array( $this, 'getpublicitemschema' ),
) );
}
/
Récupérer une collection d'items
/
public function getitems( $request ) {
$items = array();
$data = array();
// Récupérer les paramètres de pagination
$perpage = $request->getparam( 'perpage' );
$page = $request->getparam( 'page' );
$search = $request->getparam( 'search' );
// Requête à la base de données
global $wpdb;
$tablename = $wpdb->prefix . 'monpluginitems';
$offset = ( $page - 1 ) $perpage;
$where = '';
if ( ! empty( $search ) ) {
$where = $wpdb->prepare( " WHERE title LIKE %s", '%' . $wpdb->esclike( $search ) . '%' );
}
$items = $wpdb->getresults(
$wpdb->prepare(
"SELECT FROM $tablename $where ORDER BY createdat DESC LIMIT %d OFFSET %d",
$perpage,
$offset
)
);
// Préparer les données pour la réponse
foreach ( $items as $item ) {
$itemdata = $this->prepareitemforresponse( $item, $request );
$data[] = $this->prepareresponseforcollection( $itemdata );
}
// Compter le total pour les en-têtes de pagination
$totalitems = $wpdb->getvar( "SELECT COUNT() FROM $tablename $where" );
$maxpages = ceil( $totalitems / $perpage );
// Créer la réponse avec en-têtes
$response = restensureresponse( $data );
$response->header( 'X-WP-Total', (int) $totalitems );
$response->header( 'X-WP-TotalPages', (int) $maxpages );
return $response;
}
/
Récupérer un seul item
/
public function getitem( $request ) {
$id = $request->getparam( 'id' );
global $wpdb;
$tablename = $wpdb->prefix . 'monpluginitems';
$item = $wpdb->getrow(
$wpdb->prepare( "SELECT FROM $tablename WHERE id = %d", $id )
);
if ( empty( $item ) ) {
return new WPError(
'restitemnotfound',
( 'Item non trouvé.', 'mon-plugin' ),
array( 'status' => 404 )
);
}
$data = $this->prepareitemforresponse( $item, $request );
return restensureresponse( $data );
}
/
Créer un nouvel item
/
public function createitem( $request ) {
global $wpdb;
$tablename = $wpdb->prefix . 'monpluginitems';
// Récupérer et valider les données
$title = sanitizetextfield( $request->getparam( 'title' ) );
$description = wpksespost( $request->getparam( 'description' ) );
$status = sanitizetextfield( $request->getparam( 'status' ) );
$userid = getcurrentuserid();
// Insérer dans la base de données
$inserted = $wpdb->insert(
$tablename,
array(
'title' => $title,
'description' => $description,
'status' => $status,
'userid' => $userid,
'createdat' => currenttime( 'mysql' ),
),
array( '%s', '%s', '%s', '%d', '%s' )
);
if ( ! $inserted ) {
return new WPError(
'restitemcreatefailed',
( 'Impossible de créer l'item.', 'mon-plugin' ),
array( 'status' => 500 )
);
}
$itemid = $wpdb->insertid;
// Récupérer l'item créé
$item = $wpdb->getrow(
$wpdb->prepare( "SELECT FROM $tablename WHERE id = %d", $itemid )
);
$data = $this->prepareitemforresponse( $item, $request );
$response = restensureresponse( $data );
$response->setstatus( 201 );
$response->header( 'Location', resturl( sprintf( '%s/items/%d', $this->namespace, $itemid ) ) );
return $response;
}
/
Mettre à jour un item
/
public function updateitem( $request ) {
$id = $request->getparam( 'id' );
global $wpdb;
$tablename = $wpdb->prefix . 'monpluginitems';
// Vérifier que l'item existe
$item = $wpdb->getrow(
$wpdb->prepare( "SELECT FROM $tablename WHERE id = %d", $id )
);
if ( empty( $item ) ) {
return new WPError(
'restitemnotfound',
( 'Item non trouvé.', 'mon-plugin' ),
array( 'status' => 404 )
);
}
// Préparer les données à mettre à jour
$updatedata = array();
$updateformat = array();
if ( $request->hasparam( 'title' ) ) {
$updatedata['title'] = sanitizetextfield( $request->getparam( 'title' ) );
$updateformat[] = '%s';
}
if ( $request->hasparam( 'description' ) ) {
$updatedata['description'] = wpksespost( $request->getparam( 'description' ) );
$updateformat[] = '%s';
}
if ( $request->hasparam( 'status' ) ) {
$updatedata['status'] = sanitizetextfield( $request->getparam( 'status' ) );
$updateformat[] = '%s';
}
$updatedata['updatedat'] = currenttime( 'mysql' );
$updateformat[] = '%s';
// Mettre à jour dans la base de données
$updated = $wpdb->update(
$tablename,
$updatedata,
array( 'id' => $id ),
$updateformat,
array( '%d' )
);
if ( false === $updated ) {
return new WPError(
'restitemupdatefailed',
( 'Impossible de mettre à jour l'item.', 'mon-plugin' ),
array( 'status' => 500 )
);
}
// Récupérer l'item mis à jour
$item = $wpdb->getrow(
$wpdb->prepare( "SELECT FROM $tablename WHERE id = %d", $id )
);
$data = $this->prepareitemforresponse( $item, $request );
return restensureresponse( $data );
}
/
Supprimer un item
/
public function deleteitem( $request ) {
$id = $request->getparam( 'id' );
global $wpdb;
$tablename = $wpdb->prefix . 'monpluginitems';
// Vérifier que l'item existe
$item = $wpdb->getrow(
$wpdb->prepare( "SELECT FROM $tablename WHERE id = %d", $id )
);
if ( empty( $item ) ) {
return new WPError(
'restitemnotfound',
( 'Item non trouvé.', 'mon-plugin' ),
array( 'status' => 404 )
);
}
// Préparer la réponse avant suppression
$previous = $this->prepareitemforresponse( $item, $request );
// Supprimer de la base de données
$deleted = $wpdb->delete(
$tablename,
array( 'id' => $id ),
array( '%d' )
);
if ( ! $deleted ) {
return new WPError(
'restitemdeletefailed',
( 'Impossible de supprimer l'item.', 'mon-plugin' ),
array( 'status' => 500 )
);
}
$response = new WPRESTResponse();
$response->setdata( array(
'deleted' => true,
'previous' => $previous->getdata(),
) );
return $response;
}
/
Préparer un item pour la réponse
/
public function prepareitemforresponse( $item, $request ) {
$data = array(
'id' => (int) $item->id,
'title' => $item->title,
'description' => $item->description,
'status' => $item->status,
'userid' => (int) $item->userid,
'createdat' => mysql2date( 'c', $item->createdat ),
'updatedat' => $item->updatedat ? mysql2date( 'c', $item->updatedat ) : null,
);
// Ajouter des liens
$data['links'] = array(
'self' => array(
'href' => resturl( sprintf( '%s/items/%d', $this->namespace, $item->id ) ),
),
'collection' => array(
'href' => resturl( sprintf( '%s/items', $this->namespace ) ),
),
);
if ( ! empty( $item->userid ) ) {
$data['links']['author'] = array(
'href' => resturl( sprintf( 'wp/v2/users/%d', $item->userid ) ),
'embeddable' => true,
);
}
return restensureresponse( $data );
}
/
Préparer la réponse pour une collection
/
public function prepareresponseforcollection( $response ) {
if ( ! ( $response instanceof WPRESTResponse ) ) {
return $response;
}
$data = (array) $response->getdata();
$server = restgetserver();
$links = $server::getcompactresponselinks( $response );
if ( ! empty( $links ) ) {
$data['links'] = $links;
}
return $data;
}
/
Paramètres de collection (pagination, recherche)
/
public function getcollectionparams() {
return array(
'page' => array(
'description' => ( 'Page actuelle de la collection.', 'mon-plugin' ),
'type' => 'integer',
'default' => 1,
'sanitizecallback' => 'absint',
'validatecallback' => 'restvalidaterequestarg',
'minimum' => 1,
),
'perpage' => array(
'description' => ( 'Nombre maximum d'items retournés.', 'mon-plugin' ),
'type' => 'integer',
'default' => 10,
'minimum' => 1,
'maximum' => 100,
'sanitizecallback' => 'absint',
'validatecallback' => 'restvalidaterequestarg',
),
'search' => array(
'description' => ( 'Rechercher dans les items.', 'mon-plugin' ),
'type' => 'string',
'sanitizecallback' => 'sanitizetextfield',
'validatecallback' => 'restvalidaterequestarg',
),
);
}
/
Arguments pour créer/modifier un item
/
public function getendpointargsforitemschema( $methodcreate = true ) {
return array(
'title' => array(
'description' => ( 'Titre de l'item.', 'mon-plugin' ),
'type' => 'string',
'required' => $methodcreate,
'sanitizecallback' => 'sanitizetextfield',
'validatecallback' => 'restvalidaterequestarg',
),
'description' => array(
'description' => ( 'Description de l'item.', 'mon-plugin' ),
'type' => 'string',
'sanitizecallback' => 'wpksespost',
'validatecallback' => 'restvalidaterequestarg',
),
'status' => array(
'description' => ( 'Statut de l'item.', 'mon-plugin' ),
'type' => 'string',
'enum' => array( 'draft', 'published', 'archived' ),
'default' => 'draft',
'sanitizecallback' => 'sanitizetextfield',
'validatecallback' => 'restvalidaterequestarg',
),
);
}
/
Schéma de l'item
/
public function getpublicitemschema() {
if ( $this->schema ) {
return $this->addadditionalfieldsschema( $this->schema );
}
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'item',
'type' => 'object',
'properties' => array(
'id' => array(
'description' => ( 'Identifiant unique de l'item.', 'mon-plugin' ),
'type' => 'integer',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'title' => array(
'description' => ( 'Titre de l'item.', 'mon-plugin' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
),
'description' => array(
'description' => ( 'Description de l'item.', 'mon-plugin' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'status' => array(
'description' => ( 'Statut de l'item.', 'mon-plugin' ),
'type' => 'string',
'enum' => array( 'draft', 'published', 'archived' ),
'context' => array( 'view', 'edit' ),
),
'userid' => array(
'description' => ( 'ID de l'auteur.', 'mon-plugin' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'createdat' => array(
'description' => ( 'Date de création (ISO8601).', 'mon-plugin' ),
'type' => 'string',
'format' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'updatedat' => array(
'description' => ( 'Date de dernière modification (ISO8601).', 'mon-plugin' ),
'type' => array( 'string', 'null' ),
'format' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
);
$this->schema = $schema;
return $this->addadditionalfieldsschema( $this->schema );
}
/
Vérifications de permissions - Récupérer items
/
public function getitemspermissionscheck( $request ) {
// Accessible publiquement
return true;
}
/
Vérifications de permissions - Récupérer un item
/
public function getitempermissionscheck( $request ) {
// Accessible publiquement
return true;
}
/
Vérifications de permissions - Créer un item
/
public function createitempermissionscheck( $request ) {
// Seuls les utilisateurs connectés peuvent créer
if ( ! isuserloggedin() ) {
return new WPError(
'restforbidden',
( 'Vous devez être connecté pour créer un item.', 'mon-plugin' ),
array( 'status' => restauthorizationrequiredcode() )
);
}
return true;
}
/
Vérifications de permissions - Mettre à jour un item
/
public function updateitempermissionscheck( $request ) {
if ( ! isuserloggedin() ) {
return new WPError(
'restforbidden',
( 'Vous devez être connecté pour modifier un item.', 'mon-plugin' ),
array( 'status' => restauthorizationrequiredcode() )
);
}
$id = $request->getparam( 'id' );
global $wpdb;
$tablename = $wpdb->prefix . 'monpluginitems';
$item = $wpdb->getrow(
$wpdb->prepare( "SELECT FROM $tablename WHERE id = %d", $id )
);
if ( empty( $item ) ) {
return new WPError(
'restitemnotfound',
( 'Item non trouvé.', 'mon-plugin' ),
array( 'status' => 404 )
);
}
// Vérifier que l'utilisateur est propriétaire ou admin
$currentuserid = getcurrentuserid();
if ( (int) $item->userid !== $currentuserid && ! currentusercan( 'manageoptions' ) ) {
return new WPError(
'restforbidden',
( 'Vous n'avez pas la permission de modifier cet item.', 'mon-plugin' ),
array( 'status' => restauthorizationrequiredcode() )
);
}
return true;
}
/
Vérifications de permissions - Supprimer un item
/
public function deleteitempermissionscheck( $request ) {
return $this->updateitempermissionscheck( $request );
}
}
// Initialiser l'endpoint
new EndpointExample();
WordPress offre plusieurs méthodes d’authentification :
Utilisée automatiquement pour les requêtes depuis le site WordPress :
noncepermissionscheck( $request ) {
$nonce = $request->getheader( 'X-WP-Nonce' );
if ( ! wpverifynonce( $nonce, 'wprest' ) ) {
return new WPError(
'restcookieinvalidnonce',
( 'Nonce invalide.', 'mon-plugin' ),
array( 'status' => 403 )
);
}
return true;
}
Constructeur
/
public function construct() {
addfilter( 'determinecurrentuser', array( $this, 'determinecurrentuser' ), 20 );
addfilter( 'restauthenticationerrors', array( $this, 'restauthenticationerrors' ) );
}
/
Déterminer l'utilisateur actuel via clé API
/
public function determinecurrentuser( $userid ) {
// Si déjà authentifié, ne rien faire
if ( $userid ) {
return $userid;
}
// Récupérer la clé API depuis l'en-tête
$apikey = $this->getapikeyfromrequest();
if ( empty( $apikey ) ) {
return $userid;
}
// Valider la clé API
$userid = $this->validateapikey( $apikey );
return $userid;
}
/
Récupérer la clé API depuis la requête
/
private function getapikeyfromrequest() {
// Vérifier dans les en-têtes
$apikey = null;
if ( isset( $SERVER['HTTPXAPIKEY'] ) ) {
$apikey = sanitizetextfield( wpunslash( $SERVER['HTTPXAPIKEY'] ) );
} elseif ( isset( $GET['apikey'] ) ) {
// Fallback pour query parameter (moins sécurisé)
$apikey = sanitizetextfield( wpunslash( $GET['apikey'] ) );
}
return $apikey;
}
/
Valider la clé API et retourner l'ID utilisateur
/
private function validateapikey( $apikey ) {
global $wpdb;
// Hasher la clé pour comparaison
$apikeyhash = hash( 'sha256', $apikey );
$userid = $wpdb->getvar(
$wpdb->prepare(
"SELECT userid FROM {$wpdb->usermeta}
WHERE metakey = 'monpluginapikeyhash'
AND metavalue = %s
LIMIT 1",
$apikeyhash
)
);
if ( $userid ) {
// Logger l'utilisation de la clé
$this->logapikeyusage( $userid, $apikeyhash );
return (int) $userid;
}
return 0;
}
/
Logger l'utilisation de la clé API
/
private function logapikeyusage( $userid, $apikeyhash ) {
global $wpdb;
$wpdb->insert(
$wpdb->prefix . 'monpluginapilogs',
array(
'userid' => $userid,
'apikeyhash' => $apikeyhash,
'endpoint' => sanitizetextfield( $SERVER['REQUESTURI'] ),
'ipaddress' => $this->getclientip(),
'useragent' => sanitizetextfield( $SERVER['HTTPUSERAGENT'] ),
'timestamp' => currenttime( 'mysql' ),
),
array( '%d', '%s', '%s', '%s', '%s', '%s' )
);
}
/
Récupérer l'IP du client
/
private function getclientip() {
$ip = '';
if ( isset( $SERVER['HTTPCFCONNECTINGIP'] ) ) {
// Cloudflare
$ip = sanitizetextfield( wpunslash( $SERVER['HTTPCFCONNECTINGIP'] ) );
} elseif ( isset( $SERVER['HTTPXFORWARDEDFOR'] ) ) {
$ip = sanitizetextfield( wpunslash( $SERVER['HTTPXFORWARDEDFOR'] ) );
} elseif ( isset( $SERVER['REMOTEADDR'] ) ) {
$ip = sanitizetextfield( wpunslash( $SERVER['REMOTEADDR'] ) );
}
return $ip;
}
/
Gérer les erreurs d'authentification
/
public function restauthenticationerrors( $error ) {
// Ne pas vérifier sur les routes publiques
if ( ! empty( $error ) ) {
return $error;
}
// Vérifier si l'authentification par API key est requise
if ( ! isuserloggedin() ) {
$apikey = $this->getapikeyfromrequest();
if ( $apikey && ! $this->validateapikey( $apikey ) ) {
return new WPError(
'restapikeyinvalid',
( 'Clé API invalide.', 'mon-plugin' ),
array( 'status' => 401 )
);
}
}
return $error;
}
/
Générer une nouvelle clé API pour un utilisateur
/
public static function generateapikey( $userid ) {
// Générer une clé aléatoire sécurisée
$apikey = bin2hex( randombytes( 32 ) );
// Hasher et stocker
$apikeyhash = hash( 'sha256', $apikey );
updateusermeta( $userid, 'monpluginapikeyhash', $apikeyhash );
updateusermeta( $userid, 'monpluginapikeycreated', currenttime( 'mysql' ) );
// Retourner la clé non hashée (à afficher une seule fois)
return $apikey;
}
/
Révoquer la clé API d'un utilisateur
/
public static function revokeapikey( $userid ) {
deleteusermeta( $userid, 'monpluginapikeyhash' );
deleteusermeta( $userid, 'monpluginapikeycreated' );
}
}
new APIKeyAuth();
Implémentez un rate limiting pour prévenir les abus :
requests = 100; // Requêtes par heure
private $timewindow = 3600; // 1 heure en secondes
/
Constructeur
/
public function construct() {
addfilter( 'restpredispatch', array( $this, 'checkratelimit' ), 10, 3 );
}
/
Vérifier la limite de débit
/
public function checkratelimit( $result, $server, $request ) {
// Ne limiter que nos endpoints personnalisés
$route = $request->getroute();
if ( strpos( $route, '/mon-plugin/' ) !== 0 ) {
return $result;
}
$identifier = $this->getidentifier();
$currentcount = $this->getrequestcount( $identifier );
if ( $currentcount >= $this->maxrequests ) {
return new WPError(
'restratelimitexceeded',
sprintf(
( 'Limite de débit dépassée. Maximum %d requêtes par heure.', 'mon-plugin' ),
$this->maxrequests
),
array(
'status' => 429,
'headers' => array(
'X-RateLimit-Limit' => $this->maxrequests,
'X-RateLimit-Remaining' => 0,
'X-RateLimit-Reset' => $this->getresettime( $identifier ),
),
)
);
}
// Incrémenter le compteur
$this->incrementrequestcount( $identifier );
// Ajouter les en-têtes de rate limit
addfilter( 'restpostdispatch', function( $response ) use ( $currentcount ) {
$response->header( 'X-RateLimit-Limit', $this->maxrequests );
$response->header( 'X-RateLimit-Remaining', max( 0, $this->maxrequests - $currentcount - 1 ) );
return $response;
} );
return $result;
}
/
Obtenir un identifiant unique (IP ou User ID)
/
private function getidentifier() {
$userid = getcurrentuserid();
if ( $userid ) {
return 'user' . $userid;
}
// Utiliser l'IP pour les utilisateurs non connectés
$ip = $this->getclientip();
return 'ip' . md5( $ip );
}
/
Obtenir le nombre de requêtes
/
private function getrequestcount( $identifier ) {
$transientkey = 'monpluginratelimit' . $identifier;
$count = gettransient( $transientkey );
return $count ? (int) $count : 0;
}
/
Incrémenter le compteur de requêtes
/
private function incrementrequestcount( $identifier ) {
$transientkey = 'monpluginratelimit' . $identifier;
$count = $this->getrequestcount( $identifier );
settransient( $transientkey, $count + 1, $this->timewindow );
}
/
Obtenir le timestamp de réinitialisation
/
private function getresettime( $identifier ) {
$transientkey = 'monpluginratelimit' . $identifier;
$timeout = getoption( 'transienttimeout' . $transientkey );
return $timeout ? (int) $timeout : time() + $this->timewindow;
}
/
Récupérer l'IP du client
/
private function getclientip() {
$ip = '';
if ( isset( $SERVER['HTTPCFCONNECTINGIP'] ) ) {
$ip = sanitizetextfield( wpunslash( $SERVER['HTTPCFCONNECTINGIP'] ) );
} elseif ( isset( $SERVER['HTTPXFORWARDEDFOR'] ) ) {
$ip = sanitizetextfield( wpunslash( $SERVER['HTTPXFORWARDEDFOR'] ) );
} elseif ( isset( $SERVER['REMOTEADDR'] ) ) {
$ip = sanitizetextfield( wpunslash( $SERVER['REMOTEADDR'] ) );
}
return $ip;
}
}
new RateLimiter();
Valider un email avec domaines autorisés
/
public static function validateemaildomain( $value, $request, $param ) {
if ( ! isemail( $value ) ) {
return new WPError(
'restinvalidemail',
( 'L'adresse email n'est pas valide.', 'mon-plugin' ),
array( 'status' => 400 )
);
}
// Vérifier le domaine
$alloweddomains = array( 'example.com', 'test.com' );
$emailparts = explode( '@', $value );
$domain = end( $emailparts );
if ( ! inarray( $domain, $alloweddomains, true ) ) {
return new WPError(
'restinvalidemaildomain',
sprintf(
( 'Le domaine email doit être l'un des suivants : %s', 'mon-plugin' ),
implode( ', ', $alloweddomains )
),
array( 'status' => 400 )
);
}
return true;
}
/
Valider un numéro de téléphone
/
public static function validatephone( $value, $request, $param ) {
// Format : +33612345678 ou 0612345678
$pattern = '/^(+33|0)1-9$/';
if ( ! pregmatch( $pattern, $value ) ) {
return new WPError(
'restinvalidphone',
( 'Le numéro de téléphone n'est pas valide.', 'mon-plugin' ),
array( 'status' => 400 )
);
}
return true;
}
/
Valider une URL avec domaines autorisés
/
public static function validateurldomain( $value, $request, $param ) {
if ( ! filtervar( $value, FILTERVALIDATEURL ) ) {
return new WPError(
'restinvalidurl',
( 'L'URL n'est pas valide.', 'mon-plugin' ),
array( 'status' => 400 )
);
}
$alloweddomains = array( 'example.com', 'cdn.example.com' );
$parsedurl = wpparseurl( $value );
$host = $parsedurl['host'] ?? '';
if ( ! inarray( $host, $alloweddomains, true ) ) {
return new WPError(
'restinvalidurldomain',
( 'Le domaine de l'URL n'est pas autorisé.', 'mon-plugin' ),
array( 'status' => 400 )
);
}
return true;
}
/
Valider une date au format ISO 8601
/
public static function validateiso8601date( $value, $request, $param ) {
$date = DateTime::createFromFormat( DateTime::ISO8601, $value );
if ( ! $date || $date->format( DateTime::ISO8601 ) !== $value ) {
return new WPError(
'restinvaliddate',
( 'La date doit être au format ISO 8601.', 'mon-plugin' ),
array( 'status' => 400 )
);
}
return true;
}
/
Valider un JSON
/
public static function validatejson( $value, $request, $param ) {
jsondecode( $value );
if ( jsonlasterror() !== JSONERRORNONE ) {
return new WPError(
'restinvalidjson',
sprintf(
( 'JSON invalide : %s', 'mon-plugin' ),
jsonlasterrormsg()
),
array( 'status' => 400 )
);
}
return true;
}
/
Sanitizer pour JSON
/
public static function sanitizejson( $value ) {
$decoded = jsondecode( $value, true );
if ( jsonlasterror() !== JSONERRORNONE ) {
return '';
}
// Re-encoder pour garantir un JSON propre
return wpjsonencode( $decoded );
}
}
/
Client JavaScript pour l'API REST personnalisée
/
class MonPluginAPIClient {
constructor(baseUrl = '/wp-json/mon-plugin/v1') {
this.baseUrl = baseUrl;
this.nonce = wpApiSettings?.nonce || '';
}
/
Effectuer une requête HTTP
/
async request(endpoint, options = {}) {
const url = ${this.baseUrl}${endpoint};
const defaultOptions = {
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': this.nonce,
},
};
const mergedOptions = {
...defaultOptions,
...options,
headers: {
...defaultOptions.headers,
...options.headers,
},
};
try {
const response = await fetch(url, mergedOptions);
// Vérifier les en-têtes de rate limiting
const rateLimit = {
limit: response.headers.get('X-RateLimit-Limit'),
remaining: response.headers.get('X-RateLimit-Remaining'),
reset: response.headers.get('X-RateLimit-Reset'),
};
if (rateLimit.limit) {
console.log('Rate Limit:', rateLimit);
}
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || 'Erreur API');
}
return {
success: true,
data: data,
status: response.status,
headers: response.headers,
};
} catch (error) {
console.error('Erreur API:', error);
return {
success: false,
error: error.message,
};
}
}
/
GET - Récupérer tous les items
/
async getItems(params = {}) {
const queryString = new URLSearchParams(params).toString();
const endpoint = queryString ? /items?${queryString} : '/items';
return this.request(endpoint, {
method: 'GET',
});
}
/
GET - Récupérer un item
/
async getItem(id) {
return this.request(/items/${id}, {
method: 'GET',
});
}
/
POST - Créer un item
/
async createItem(data) {
return this.request('/items', {
method: 'POST',
body: JSON.stringify(data),
});
}
/
PUT - Mettre à jour un item
/
async updateItem(id, data) {
return this.request(/items/${id}, {
method: 'PUT',
body: JSON.stringify(data),
});
}
/
DELETE - Supprimer un item
/
async deleteItem(id) {
return this.request(/items/${id}, {
method: 'DELETE',
});
}
}
// Utilisation
(async () => {
const api = new MonPluginAPIClient();
// Récupérer tous les items avec pagination
const items = await api.getItems({ page: 1, perpage: 10, search: 'test' });
console.log('Items:', items);
// Créer un nouvel item
const newItem = await api.createItem({
title: 'Mon nouvel item',
description: 'Description de l'item',
status: 'published',
});
console.log('Item créé:', newItem);
// Mettre à jour un item
if (newItem.success) {
const updated = await api.updateItem(newItem.data.id, {
title: 'Titre modifié',
});
console.log('Item mis à jour:', updated);
}
// Supprimer un item
if (newItem.success) {
const deleted = await api.deleteItem(newItem.data.id);
console.log('Item supprimé:', deleted);
}
})();
# Lister toutes les routes
wp rest list
# Tester un endpoint GET
wp rest get /mon-plugin/v1/items
# Tester un endpoint POST
wp rest post /mon-plugin/v1/items --title="Test Item" --status="published"
# Tester avec un utilisateur spécifique
wp rest get /mon-plugin/v1/items --user=admin
Logger toutes les requêtes API
/
public function construct() {
addfilter( 'restpredispatch', array( $this, 'logrequest' ), 10, 3 );
addfilter( 'restpostdispatch', array( $this, 'logresponse' ), 10, 3 );
}
/
Logger la requête
/
public function logrequest( $result, $server, $request ) {
if ( ! WPDEBUG || ! WPDEBUGLOG ) {
return $result;
}
$route = $request->getroute();
if ( strpos( $route, '/mon-plugin/' ) !== 0 ) {
return $result;
}
errorlog( sprintf(
'[API Request] %s %s | Params: %s',
$request->getmethod(),
$route,
wpjsonencode( $request->getparams() )
) );
return $result;
}
/
Logger la réponse
/
public function logresponse( $response, $server, $request ) {
if ( ! WPDEBUG || ! WPDEBUGLOG ) {
return $response;
}
$route = $request->getroute();
if ( strpos( $route, '/mon-plugin/' ) !== 0 ) {
return $response;
}
$status = $response->getstatus();
$data = $response->getdata();
errorlog( sprintf(
'[API Response] %s %s | Status: %d | Data: %s',
$request->getmethod(),
$route,
$status,
wpjsonencode( $data )
) );
return $response;
}
}
if ( WPDEBUG ) {
new APILogger();
}
MAUVAIS :
'permissioncallback' => 'returntrue'
BON :
'permissioncallback' => array( $this, 'checkpermissions' )
MAUVAIS :
$title = $POST['title'];
BON :
$title = sanitizetextfield( $request->getparam( 'title' ) );
MAUVAIS :
return $data;
BON :
if ( ! $data ) {
return new WPError( 'restnotfound', ( 'Données non trouvées.' ), array( 'status' => 404 ) );
}
return restensureresponse( $data );
La création d’endpoints REST API personnalisés en 2025 nécessite une attention particulière à la sécurité, avec l’implémentation systématique de l’authentification, de la validation des données, du rate limiting et du logging. En suivant ces pratiques, vous créerez des APIs robustes et sécurisées.
Points clés :
Mots-clés:* WordPress REST API, endpoints personnalisés WordPress, API REST sécurité, authentification WordPress API, rate limiting WordPress, validation REST API, WordPress API 2025, WP REST API custom, WordPress API développement
Cet article est vivant — corrections, contre-arguments et retours de production sont les bienvenus. Trois canaux, choisissez celui qui vous convient.