Skip to Content

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é.