Terrain 15 — Développer un plugin
⏱️ TL;DR — 35 mini-jeux
⭐⭐⭐→⭐⭐⭐⭐pour bâtir un vrai plugin, brique par brique :wpr-books-lite(CPT « Livre » + taxonomie + métadonnées + metabox + shortcode + réglages + REST). C’est le socle du fil rouge du cours (Parties 7–9) et du projet Niveau 2, en version mini-jeux.
⚠️ Chaque enregistrement/donnée doit être préfixé (
wpr_), sécurisé (nonce, capability, sanitize/escape) et propre à la désinstallation. On applique tout ce qu’on a vu aux terrains 11–12.
🕹️ Mini-jeu 1 — L’en-tête du plugin ⭐⭐⭐ 💻
But : faire exister le plugin.
Mission : crée plugins/wpr-books-lite/wpr-books-lite.php.
Étapes :
<?php
/**
* Plugin Name: WPR Books Lite
* Description: Catalogue de livres (mini-jeux terrain 15).
* Version: 0.1.0
* Text Domain: wpr-books-lite
*/
defined( 'ABSPATH' ) || exit;✅ Solution
L’en-tête fait apparaître le plugin dans Extensions. defined( 'ABSPATH' ) || exit; empêche l’accès direct au fichier (sécurité de base, à mettre en tête de chaque fichier PHP).
🕹️ Mini-jeu 2 — Hooks d’activation/désactivation ⭐⭐⭐ 💻
But : réagir au cycle de vie. Mission : logue un message à l’activation. Étapes :
register_activation_hook( __FILE__, 'wpr_activate' );
function wpr_activate() {
error_log( 'wpr-books-lite activé' );
}✅ Solution
register_activation_hook/register_deactivation_hook tirent à l’(dés)activation. C’est ici qu’on prépare (tables, options, flush rewrite) et qu’on nettoie temporairement. À ne pas confondre avec la désinstallation (jeu 24).
🕹️ Mini-jeu 3 — Enregistrer le CPT « Livre » ⭐⭐⭐⭐ 💻
But : un type de contenu.
Mission : enregistre wpr_book sur init.
Étapes :
add_action( 'init', function () {
register_post_type( 'wpr_book', array(
'labels' => array( 'name' => 'Livres', 'singular_name' => 'Livre' ),
'public' => true,
'has_archive' => true,
'rewrite' => array( 'slug' => 'livres' ),
'supports' => array( 'title', 'editor', 'thumbnail' ),
'show_in_rest' => true,
) );
} );✅ Solution
register_post_type sur init crée le type « Livre » (menu admin, URLs /livres/…, éditeur de blocs). show_in_rest: true l’expose à l’API et active Gutenberg pour ce type. Cœur du plugin.
🕹️ Mini-jeu 4 — Flush rewrite à l’activation ⭐⭐⭐⭐ 💻
But : éviter le 404 des URLs de livres. Mission : enregistre le CPT puis vide les règles à l’activation. Étapes :
function wpr_activate() {
wpr_register_book_cpt(); // même code que le jeu 3, extrait en fonction
flush_rewrite_rules();
}✅ Solution
Les URLs /livres/… n’existent qu’après un flush des règles de réécriture. On l’appelle une fois à l’activation (après avoir enregistré le CPT), pas à chaque chargement (coûteux). Sinon : 404 fantômes (terrain 09, jeu 13).
🕹️ Mini-jeu 5 — La taxonomie « Genre » ⭐⭐⭐⭐ 💻
But : classer les livres.
Mission : enregistre wpr_genre sur init.
Étapes :
register_taxonomy( 'wpr_genre', 'wpr_book', array(
'labels' => array( 'name' => 'Genres' ),
'hierarchical' => true,
'show_in_rest' => true,
) );✅ Solution
register_taxonomy lie « Genre » au CPT « Livre » (comme les catégories aux articles). hierarchical: true = comportement « catégories » (cases à cocher). show_in_rest pour l’API et l’éditeur.
🕹️ Mini-jeu 6 — Enregistrer des métadonnées ⭐⭐⭐⭐ 💻
But : ISBN et prix.
Mission : déclare wpr_isbn et wpr_prix.
Étapes :
register_post_meta( 'wpr_book', 'wpr_isbn', array(
'type' => 'string', 'single' => true, 'show_in_rest' => true,
'sanitize_callback' => 'sanitize_text_field',
) );✅ Solution
register_post_meta déclare une métadonnée typée, exposée en REST (show_in_rest) et assainie à l’écriture (sanitize_callback). Déclarer la meta (plutôt que l’écrire à la volée) la rend visible à l’API et à l’éditeur.
🕹️ Mini-jeu 7 — Ajouter une metabox ⭐⭐⭐⭐ 💻
But : éditer ISBN/prix dans l’admin. Mission : ajoute une metabox à l’écran « Livre ». Étapes :
add_action( 'add_meta_boxes', function () {
add_meta_box( 'wpr_details', 'Détails du livre', 'wpr_metabox_html', 'wpr_book' );
} );✅ Solution
add_meta_box insère un encart custom dans l’écran d’édition du CPT. La fonction wpr_metabox_html (jeu 8) en rend le contenu. Alternative moderne : un panneau de bloc (Partie 8), mais la metabox reste universelle.
🕹️ Mini-jeu 8 — Rendre la metabox (avec nonce) ⭐⭐⭐⭐ 💻
But : le formulaire + protection CSRF. Mission : affiche les champs et un nonce. Étapes :
function wpr_metabox_html( $post ) {
wp_nonce_field( 'wpr_save', 'wpr_nonce' );
$isbn = get_post_meta( $post->ID, 'wpr_isbn', true );
echo '<label>ISBN <input name="wpr_isbn" value="' . esc_attr( $isbn ) . '"></label>';
}✅ Solution
wp_nonce_field génère un jeton anti-CSRF (vérifié au save, jeu 9). On échappe la valeur affichée (esc_attr). Champ nommé wpr_isbn → récupéré dans $_POST au save. Jamais de sortie non échappée.
🕹️ Mini-jeu 9 — Sauvegarder la metabox (sécurisé) ⭐⭐⭐⭐ 💻
But : le save blindé.
Mission : sauve wpr_isbn avec vérif nonce + capability + sanitize.
Étapes :
add_action( 'save_post_wpr_book', function ( $post_id ) {
if ( ! isset( $_POST['wpr_nonce'] ) || ! wp_verify_nonce( $_POST['wpr_nonce'], 'wpr_save' ) ) return;
if ( ! current_user_can( 'edit_post', $post_id ) ) return;
if ( isset( $_POST['wpr_isbn'] ) ) {
update_post_meta( $post_id, 'wpr_isbn', sanitize_text_field( wp_unslash( $_POST['wpr_isbn'] ) ) );
}
} );✅ Solution
Les 3 gardes obligatoires : nonce (wp_verify_nonce), capability (current_user_can), sanitize (sanitize_text_field + wp_unslash). save_post_wpr_book cible ce CPT. C’est le pattern de sécurité d’écriture (terrain 16).
🕹️ Mini-jeu 10 — Une colonne d’admin ⭐⭐⭐⭐ 💻
But : afficher le prix dans la liste. Mission : ajoute une colonne « Prix ». Étapes :
add_filter( 'manage_wpr_book_posts_columns', function ( $cols ) {
$cols['wpr_prix'] = 'Prix';
return $cols;
} );
add_action( 'manage_wpr_book_posts_custom_column', function ( $col, $id ) {
if ( 'wpr_prix' === $col ) echo esc_html( get_post_meta( $id, 'wpr_prix', true ) );
}, 10, 2 );✅ Solution
Deux hooks : un filtre déclare la colonne, une action la remplit. Améliore la vue d’ensemble de l’admin. Motif réutilisable pour tout CPT.
🕹️ Mini-jeu 11 — Rendre la colonne triable ⭐⭐⭐⭐ 💻
But : trier par prix. Mission : rends la colonne « Prix » triable. Étapes :
add_filter( 'manage_edit-wpr_book_sortable_columns', function ( $cols ) {
$cols['wpr_prix'] = 'wpr_prix';
return $cols;
} );✅ Solution
Déclarer la colonne sortable ajoute le tri au clic ; pour un tri numérique correct, on complète avec pre_get_posts (meta_key + orderby=meta_value_num). Confort d’admin réel.
🕹️ Mini-jeu 12 — Un shortcode de catalogue ⭐⭐⭐⭐ 💻
But : lister les livres en front.
Mission : crée [wpr_books].
Étapes :
add_shortcode( 'wpr_books', function ( $atts ) {
$a = shortcode_atts( array( 'genre' => '', 'limit' => 10 ), $atts );
// requête au jeu 13
return wpr_render_books( $a );
} );✅ Solution
Le shortcode retourne le HTML (jamais echo). shortcode_atts gère les défauts (genre, limit). Insérable via le bloc Shortcode (terrain 05, jeu 27). Interface front simple du plugin.
🕹️ Mini-jeu 13 — La requête WP_Query ⭐⭐⭐⭐ 💻
But : interroger les livres. Mission : récupère les livres (filtrés par genre). Étapes :
$q = new WP_Query( array(
'post_type' => 'wpr_book',
'posts_per_page' => (int) $a['limit'],
'tax_query' => $a['genre'] ? array( array(
'taxonomy' => 'wpr_genre', 'field' => 'slug', 'terms' => $a['genre'],
) ) : array(),
) );✅ Solution
WP_Query est la requête typée de WordPress (Partie 5). tax_query filtre par genre. Toujours wp_reset_postdata() après une boucle secondaire. On caste limit en int (défense en profondeur).
🕹️ Mini-jeu 14 — Échapper la sortie du catalogue ⭐⭐⭐⭐ 💻
But : rendu sûr. Mission : boucle et affiche titre + prix échappés. Étapes :
while ( $q->have_posts() ) { $q->the_post();
printf( '<li>%s — %s €</li>',
esc_html( get_the_title() ),
esc_html( get_post_meta( get_the_ID(), 'wpr_prix', true ) )
);
}
wp_reset_postdata();✅ Solution
Toute donnée sortie est échappée (esc_html). wp_reset_postdata() restaure la requête principale après la boucle secondaire (sinon effets de bord sur le reste de la page). Deux réflexes non négociables.
🕹️ Mini-jeu 15 — Enregistrer un réglage ⭐⭐⭐⭐ 💻
But : Settings API. Mission : enregistre une option « devise ». Étapes :
add_action( 'admin_init', function () {
register_setting( 'wpr_options', 'wpr_devise', array(
'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'default' => '€',
) );
} );✅ Solution
register_setting déclare une option assainie, rattachée à un groupe (wpr_options). La Settings API gère la sauvegarde et le nonce du formulaire pour toi — plus sûr que de traiter $_POST à la main.
🕹️ Mini-jeu 16 — Section et champ de réglages ⭐⭐⭐⭐ 💻
But : l’UI du réglage. Mission : ajoute une section et un champ pour « devise ». Étapes :
add_settings_section( 'wpr_main', 'Général', '__return_false', 'wpr' );
add_settings_field( 'wpr_devise', 'Devise', 'wpr_devise_field', 'wpr', 'wpr_main' );✅ Solution
add_settings_section + add_settings_field construisent l’UI ; la fonction de rendu du champ affiche l’input (avec esc_attr sur la valeur). Le trio register_setting/section/field = le pattern officiel des pages d’options.
🕹️ Mini-jeu 17 — La page de réglages ⭐⭐⭐⭐ 💻
But : afficher le formulaire. Mission : ajoute une page d’admin qui rend le formulaire de réglages. Étapes :
add_action( 'admin_menu', function () {
add_options_page( 'WPR Books', 'WPR Books', 'manage_options', 'wpr', 'wpr_settings_page' );
} );
function wpr_settings_page() {
echo '<div class="wrap"><h1>WPR Books</h1><form method="post" action="options.php">';
settings_fields( 'wpr_options' ); do_settings_sections( 'wpr' ); submit_button();
echo '</form></div>';
}✅ Solution
settings_fields + do_settings_sections + submit_button rendent le formulaire complet ; action="options.php" fait que WordPress sauve tout seul (avec nonce). Capability manage_options = admin only.
🕹️ Mini-jeu 18 — Lire l’option dans le front ⭐⭐⭐ 💻
But : utiliser le réglage. Mission : affiche le prix avec la devise réglée. Étapes :
$devise = get_option( 'wpr_devise', '€' );✅ Solution
get_option lit la valeur (avec défaut). Le catalogue (jeu 14) affiche alors 12 $ ou 12 € selon le réglage. La boucle est paramétrée par la config — le comportement attendu d’un vrai plugin.
🕹️ Mini-jeu 19 — Internationaliser ⭐⭐⭐⭐ 💻
But : plugin traduisible. Mission : charge le textdomain et enveloppe une chaîne. Étapes :
add_action( 'init', function () {
load_plugin_textdomain( 'wpr-books-lite', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' );
} );
// usage : __( 'Livres', 'wpr-books-lite' )✅ Solution
Toutes les chaînes visibles passent par __()/_e() avec le même textdomain. On génère le .pot avec wp i18n make-pot. Obligatoire pour wp.org et pour un plugin pro (Partie 11).
🕹️ Mini-jeu 20 — Enfiler un asset ciblé ⭐⭐⭐⭐ 💻
But : charger le CSS seulement où il faut. Mission : enfile un CSS uniquement sur l’archive des livres. Étapes :
add_action( 'wp_enqueue_scripts', function () {
if ( is_post_type_archive( 'wpr_book' ) ) {
wp_enqueue_style( 'wpr', plugins_url( 'assets/wpr.css', __FILE__ ), array(), '0.1.0' );
}
} );✅ Solution
On conditionne le chargement (is_post_type_archive) pour ne pas alourdir tout le site. Bon citoyen perf : ne charge que le nécessaire, là où c’est nécessaire.
🕹️ Mini-jeu 21 — Un gabarit front depuis le plugin ⭐⭐⭐⭐⭐ 💻
But : contrôler l’affichage d’un livre.
Mission : fournis un single-wpr_book.php de repli via template_include.
Étapes :
add_filter( 'template_include', function ( $tpl ) {
if ( is_singular( 'wpr_book' ) && ! locate_template( 'single-wpr_book.php' ) ) {
return plugin_dir_path( __FILE__ ) . 'templates/single-wpr_book.php';
}
return $tpl;
} );✅ Solution
Le plugin propose un gabarit si le thème n’en fournit pas (locate_template). Ainsi il fonctionne partout, tout en laissant le thème prioritaire. Bon design : le plugin complète, il n’impose pas.
🕹️ Mini-jeu 22 — Exposer un champ en REST ⭐⭐⭐⭐⭐ 💻
But : enrichir l’API (prépare le headless).
Mission : ajoute wpr_prix à la réponse REST du CPT.
Étapes :
add_action( 'rest_api_init', function () {
register_rest_field( 'wpr_book', 'prix', array(
'get_callback' => fn( $obj ) => get_post_meta( $obj['id'], 'wpr_prix', true ),
) );
} );✅ Solution
register_rest_field ajoute un champ calculé à /wp-json/wp/v2/wpr_book. Ton front Next.js (terrain 17) le consommera directement. Le pont plugin → headless. (La meta déclarée au jeu 6 apparaît déjà ; ceci ajoute un champ formaté.)
🕹️ Mini-jeu 23 — Mettre en cache avec un transient ⭐⭐⭐⭐⭐ 💻
But : éviter de recalculer. Mission : mets en cache la liste des livres 1 h. Étapes :
$html = get_transient( 'wpr_catalogue' );
if ( false === $html ) {
$html = wpr_render_books( $a );
set_transient( 'wpr_catalogue', $html, HOUR_IN_SECONDS );
}✅ Solution
Un transient stocke un résultat coûteux avec expiration (HOUR_IN_SECONDS). Sur un catalogue lu souvent et modifié rarement, c’est un gros gain. À invalider au save (jeu 24). API de cache native (Partie 5).
🕹️ Mini-jeu 24 — Invalider le cache au save ⭐⭐⭐⭐ 💻
But : cohérence. Mission : vide le transient quand un livre change. Étapes :
add_action( 'save_post_wpr_book', function () {
delete_transient( 'wpr_catalogue' );
} );✅ Solution
Un cache sans invalidation sert des données périmées. On supprime le transient à chaque modification de livre → le prochain affichage le régénère. La règle d’or du cache : savoir quand l’invalider.
🕹️ Mini-jeu 25 — Nettoyer à la désinstallation ⭐⭐⭐⭐ 💻
But : ne rien laisser traîner.
Mission : crée uninstall.php qui supprime l’option.
Étapes :
<?php
defined( 'WP_UNINSTALL_PLUGIN' ) || exit;
delete_option( 'wpr_devise' );✅ Solution
uninstall.php s’exécute à la suppression (pas à la désactivation). La garde WP_UNINSTALL_PLUGIN empêche l’accès direct. On y supprime options/tables/meta créées. Plugin propre = plugin qui sait partir.
🕹️ Mini-jeu 26 — Une capability custom ⭐⭐⭐⭐ 💻
But : droits dédiés.
Mission : protège la page de réglages par manage_wpr_books.
✅ Solution
Plutôt que manage_options (large), on peut définir et exiger une capability dédiée (manage_wpr_books) accordée à certains rôles (ajoutée via add_cap à l’activation). Granularité et moindre privilège (terrain 10). Nettoyer les caps à la désinstallation.
🕹️ Mini-jeu 27 — Un handler AJAX ⭐⭐⭐⭐⭐ 💻
But : interaction sans rechargement. Mission : ajoute un endpoint AJAX qui renvoie un livre au hasard. Étapes :
add_action( 'wp_ajax_wpr_random', 'wpr_random' );
add_action( 'wp_ajax_nopriv_wpr_random', 'wpr_random' );
function wpr_random() {
check_ajax_referer( 'wpr_ajax' );
wp_send_json_success( array( 'titre' => '…' ) );
}✅ Solution
wp_ajax_{action} (connectés) et wp_ajax_nopriv_{action} (anonymes) gèrent l’AJAX admin-ajax. check_ajax_referer vérifie le nonce ; wp_send_json_* répond. Pour du neuf, préfère un endpoint REST (terrain 17), mais l’AJAX reste très présent.
🕹️ Mini-jeu 28 — Une tâche planifiée ⭐⭐⭐⭐⭐ 💻
But : du récurrent (WP-Cron). Mission : planifie un nettoyage quotidien. Étapes :
register_activation_hook( __FILE__, function () {
if ( ! wp_next_scheduled( 'wpr_daily' ) ) {
wp_schedule_event( time(), 'daily', 'wpr_daily' );
}
} );
add_action( 'wpr_daily', 'wpr_do_cleanup' );✅ Solution
wp_schedule_event planifie un hook récurrent (daily), exécuté par WP-Cron. On déplanifie à la désactivation (wp_clear_scheduled_hook). Base des traitements périodiques (imports, e-mails, nettoyage). Partie 5.
🕹️ Mini-jeu 29 — Structurer en includes/ ⭐⭐⭐ 💻
But : un plugin lisible. Mission : éclate le code en fichiers chargés depuis le principal. Étapes :
require_once __DIR__ . '/includes/cpt.php';
require_once __DIR__ . '/includes/metabox.php';
require_once __DIR__ . '/includes/settings.php';
require_once __DIR__ . '/includes/rest.php';✅ Solution
Un plugin qui grossit se découpe par responsabilité (includes/), chargé depuis le fichier principal. Étape vers l’organisation en classes + autoload (jeu 30). Lisibilité et maintenabilité.
🕹️ Mini-jeu 30 — Passer aux classes ⭐⭐⭐⭐⭐ 💻
But : POO et autoload. Mission : encapsule le CPT dans une classe. Étapes :
class WPR_Book_CPT {
public function register() {
add_action( 'init', array( $this, 'cpt' ) );
}
public function cpt() { /* register_post_type */ }
}
( new WPR_Book_CPT() )->register();✅ Solution
Les classes organisent le code (une responsabilité par classe), avec un autoloader (Composer ou custom) pour charger à la demande. C’est la structure d’un plugin pro et distribuable (Partie 7). Ton réflexe de dev moderne s’applique.
🕹️ Mini-jeu 31 — Débuguer avec Query Monitor ⭐⭐⭐⭐ 💻
But : vérifier ton plugin. Mission : confirme, via Query Monitor, que tes hooks tirent et mesure les requêtes du shortcode.
✅ Solution
Query Monitor (terrain 11) montre tes hooks, la WP_Query du shortcode, son temps. Tu vérifies au lieu de supposer. Repère les requêtes redondantes (candidates au transient, jeu 23).
🕹️ Mini-jeu 32 — Le readme.txt ⭐⭐⭐⭐ 💻
But : préparer la distribution.
Mission : crée un readme.txt conforme wp.org (en-tête + sections).
✅ Solution
Le readme.txt (format wp.org : Stable tag, Tested up to, Requires PHP, sections Description/Installation/Changelog) est exigé pour publier. Validable avec le readme validator (Partie 11). Ta vitrine sur le répertoire.
🕹️ Mini-jeu 33 — Passer Plugin Check ⭐⭐⭐⭐⭐ 🛠️
But : conformité aux standards.
Mission : installe « Plugin Check » et fais passer wpr-books-lite.
✅ Solution
Plugin Check (officiel) audite sécurité, i18n, fonctions interdites, readme… Vise zéro erreur bloquante avant distribution. C’est le juge automatique de wp.org (Partie 11, projet Niveau 5).
🕹️ Mini-jeu 34 — Zipper et installer ailleurs ⭐⭐⭐ 💻
But : livrer. Mission : zippe le plugin et installe-le sur une autre install (upload de zip).
✅ Solution
Le plugin se distribue en zip (terrain 08, jeu 9). Teste-le sur une install vierge : activation sans erreur, CPT présent, shortcode fonctionnel, désinstallation propre. Le vrai test d’un plugin distribuable.
🕹️ Mini-jeu 35 — Récap wpr-books-lite ⭐⭐⭐⭐ 💻
But : capstone du terrain. Mission : assemble tout : CPT + taxo + meta + metabox sécurisée + colonnes + shortcode caché + réglages + i18n + REST + transient + uninstall — préfixé et conforme Plugin Check.
✅ Solution
Tu as construit un vrai plugin de bout en bout — exactement wpr-books du cours (Parties 7–9) et du projet Niveau 2. Tu tiens le cœur du métier de dev WordPress. Reste à le sécuriser à fond (terrain 16) et le connecter en headless (terrain 17).
Terrain suivant : 16 — Sécurité.