Skip to Content
WordPressPartie 7 — Développer des pluginsTutoriel : le plugin wpr-books

Chapitre 7.9 — Tutoriel : le plugin wpr-books

⏱️ TL;DR — On assemble tout ce que la Partie 7 a enseigné en un plugin complet et sûr : wpr-books. Il enregistre un CPT livre + une taxonomie genre (ch. 7.3), attache des métadonnées ISBN/prix via register_post_meta et une metabox sécurisée (ch. 7.4), ajoute une page de réglages (ch. 7.5), un shortcode de listing (ch. 7.6), applique les 4 piliers de sécurité (ch. 7.7), et i18n partout (ch. 7.8). Ce plugin sera étendu en bloc React (Partie 8) puis consommé en headless (Partie 9).

🎯 Objectifs

  • Structurer un plugin réel de bout en bout.
  • Combiner CPT, taxonomie, méta, admin, shortcode, sécurité, i18n.
  • Poser les bases pour l’extension bloc (Partie 8) et REST/headless (Partie 9).

1. Structure et fichier principal

wpr-books/ ├── wpr-books.php ← en-tête + amorçage ├── uninstall.php ├── includes/ │ ├── class-plugin.php ← hooks + orchestration │ ├── class-cpt.php ← CPT + taxonomie │ ├── class-meta.php ← métadonnées + metabox │ ├── class-settings.php ← page de réglages │ └── class-shortcode.php └── languages/

wpr-books.php :

<?php /** * Plugin Name: WPR Books * Description: Catalogue de livres (CPT, taxonomie, méta, réglages, shortcode). * Version: 1.0.0 * Requires PHP: 8.1 * Text Domain: wpr-books * Domain Path: /languages * License: GPL-2.0-or-later */ if ( ! defined( 'ABSPATH' ) ) { exit; } define( 'WPR_BOOKS_FILE', __FILE__ ); define( 'WPR_BOOKS_PATH', plugin_dir_path( __FILE__ ) ); define( 'WPR_BOOKS_URL', plugin_dir_url( __FILE__ ) ); require_once WPR_BOOKS_PATH . 'includes/class-plugin.php'; require_once WPR_BOOKS_PATH . 'includes/class-cpt.php'; require_once WPR_BOOKS_PATH . 'includes/class-meta.php'; require_once WPR_BOOKS_PATH . 'includes/class-settings.php'; require_once WPR_BOOKS_PATH . 'includes/class-shortcode.php'; // Activation : enregistrer le CPT PUIS flush (une seule fois) register_activation_hook( __FILE__, function () { ( new \WPR\Books\CPT() )->register(); flush_rewrite_rules(); } ); register_deactivation_hook( __FILE__, 'flush_rewrite_rules' ); add_action( 'plugins_loaded', function () { \WPR\Books\Plugin::instance(); } );

2. L’orchestrateur (class-plugin.php)

<?php namespace WPR\Books; if ( ! defined( 'ABSPATH' ) ) { exit; } final class Plugin { private static ?Plugin $instance = null; public static function instance(): Plugin { return self::$instance ??= new self(); } private function __construct() { add_action( 'init', array( $this, 'i18n' ) ); ( new CPT() )->hooks(); ( new Meta() )->hooks(); ( new Settings() )->hooks(); ( new Shortcode() )->hooks(); } public function i18n(): void { load_plugin_textdomain( 'wpr-books', false, dirname( plugin_basename( WPR_BOOKS_FILE ) ) . '/languages' ); } }

3. CPT + taxonomie (class-cpt.php)

<?php namespace WPR\Books; if ( ! defined( 'ABSPATH' ) ) { exit; } class CPT { public function hooks(): void { add_action( 'init', array( $this, 'register' ) ); } public function register(): void { register_post_type( 'livre', array( 'labels' => array( 'name' => __( 'Livres', 'wpr-books' ), 'singular_name' => __( 'Livre', 'wpr-books' ), 'add_new_item' => __( 'Ajouter un livre', 'wpr-books' ), ), 'public' => true, 'has_archive' => true, 'menu_icon' => 'dashicons-book', 'supports' => array( 'title', 'editor', 'thumbnail', 'excerpt' ), 'rewrite' => array( 'slug' => 'livres' ), 'show_in_rest' => true, // Gutenberg + headless ) ); register_taxonomy( 'genre', array( 'livre' ), array( 'labels' => array( 'name' => __( 'Genres', 'wpr-books' ), 'singular_name' => __( 'Genre', 'wpr-books' ) ), 'public' => true, 'hierarchical' => true, 'show_in_rest' => true, 'rewrite' => array( 'slug' => 'genre' ), ) ); } }

4. Métadonnées + metabox sécurisée (class-meta.php)

<?php namespace WPR\Books; if ( ! defined( 'ABSPATH' ) ) { exit; } class Meta { public function hooks(): void { add_action( 'init', array( $this, 'register_meta' ) ); add_action( 'add_meta_boxes', array( $this, 'add_box' ) ); add_action( 'save_post_livre',array( $this, 'save' ) ); } // Exposée en REST (headless + panneau bloc, Partie 8/9) public function register_meta(): void { foreach ( array( 'wpr_isbn' => 'string', 'wpr_prix' => 'number' ) as $key => $type ) { register_post_meta( 'livre', $key, array( 'type' => $type, 'single' => true, 'show_in_rest' => true, 'auth_callback' => fn() => current_user_can( 'edit_posts' ), 'sanitize_callback' => 'wpr_isbn' === $key ? 'sanitize_text_field' : null, ) ); } } public function add_box(): void { add_meta_box( 'wpr_book_details', __( 'Détails', 'wpr-books' ), array( $this, 'render' ), 'livre', 'side' ); } public function render( \WP_Post $post ): void { wp_nonce_field( 'wpr_book_save', 'wpr_book_nonce' ); $isbn = get_post_meta( $post->ID, 'wpr_isbn', true ); $prix = get_post_meta( $post->ID, 'wpr_prix', true ); printf( '<p><label>%s<br><input type="text" name="wpr_isbn" value="%s" class="widefat"></label></p>', esc_html__( 'ISBN', 'wpr-books' ), esc_attr( $isbn ) ); printf( '<p><label>%s<br><input type="number" step="0.01" name="wpr_prix" value="%s" class="widefat"></label></p>', esc_html__( 'Prix (€)', 'wpr-books' ), esc_attr( $prix ) ); } public function save( int $post_id ): void { // 4 gardes : nonce, autosave, capability, sanitize (ch. 7.4 & 7.7) if ( ! isset( $_POST['wpr_book_nonce'] ) || ! wp_verify_nonce( $_POST['wpr_book_nonce'], 'wpr_book_save' ) ) { return; } if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) { 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'] ) ) ); } if ( isset( $_POST['wpr_prix'] ) ) { update_post_meta( $post_id, 'wpr_prix', (float) $_POST['wpr_prix'] ); } } }

⚠️ Piège — la double source de vérité. Ici la meta est éditable à la fois par la metabox (PHP) et exposée en REST (register_post_meta). C’est volontaire (compat + headless). Mais quand vous ajouterez le panneau bloc React (Partie 8), veillez à ne pas dupliquer la logique de validation : la sanitize_callback de register_post_meta reste la garde commune.


5. Réglages + shortcode (extraits)

class-settings.php (Settings API, ch. 7.5) — une option wpr_books_options avec par_page. class-shortcode.php (ch. 7.6) — [wpr_livres genre="roman" nombre="5"] qui liste les livres (requête assainie, sorties échappées, wp_reset_postdata). Le nombre par défaut lit le réglage par_page.

// class-shortcode.php (cœur) public function render( $atts ): string { $opts = get_option( 'wpr_books_options', array( 'par_page' => 12 ) ); $atts = shortcode_atts( array( 'genre' => '', 'nombre' => $opts['par_page'] ), $atts, 'wpr_livres' ); $args = array( 'post_type' => 'livre', 'posts_per_page' => absint( $atts['nombre'] ) ); if ( $atts['genre'] ) { $args['tax_query'] = array( array( 'taxonomy' => 'genre', 'field' => 'slug', 'terms' => sanitize_title( $atts['genre'] ) ) ); } $q = new \WP_Query( $args ); ob_start(); if ( $q->have_posts() ) { echo '<ul class="wpr-livres">'; while ( $q->have_posts() ) { $q->the_post(); $prix = get_post_meta( get_the_ID(), 'wpr_prix', true ); printf( '<li><a href="%s">%s</a>%s</li>', esc_url( get_permalink() ), esc_html( get_the_title() ), $prix ? ' — ' . esc_html( number_format_i18n( (float) $prix, 2 ) ) . ' €' : '' ); } echo '</ul>'; wp_reset_postdata(); } return ob_get_clean(); }

6. Ce qu’on obtient (et la suite)

Le plugin fournit :

  • un catalogue de livres géré en admin (CPT + genres + ISBN/prix) ;
  • des URLs propres (/livres/…, /genre/…) ;
  • un shortcode de listing configurable ;
  • une API REST prête (/wp-json/wp/v2/livre avec les meta exposées) — donc headless-ready ;
  • du code sécurisé et traduisible.
  • Partie 8 : un bloc Gutenberg en React (« Livre à la une », panneau d’ISBN/prix éditable) branché sur ces meta.
  • Partie 9 : un front Next.js qui lit /wp-json/wp/v2/livre (headless) pour afficher le catalogue.
  • Partie 13 : transformer wpr-books en plugin freemium distribuable.

💡 Pour un dev React — Vous venez de construire, en PHP, un back-office typé (un modèle « Livre » avec ses champs, son admin, ses routes, son API) — l’équivalent d’un CMS headless que vous auriez configuré, mais entièrement sous votre contrôle et extensible. À partir d’ici, tout ce que vous ajoutez (bloc React, front Next.js) se branche sur cette fondation via la REST API. C’est le pont Partie 7 → 8 → 9.


✏️ Exercices

  1. Dans save(), retirez mentalement la vérification du nonce : quelle faille ouvrez-vous ?
  2. Pourquoi show_in_rest => true sur le CPT et register_post_meta(..., show_in_rest => true) sont-ils tous deux nécessaires pour un front Next.js qui affiche l’ISBN ?
  3. Le shortcode utilise absint() et sanitize_title() sur ses attributs. Que se passerait-il sans ces assainissements ?

✅ Solution

  1. Sans nonce, l’action d’enregistrement devient vulnérable au CSRF : un site tiers pourrait faire enregistrer des données arbitraires au nom d’un utilisateur connecté ayant le droit. Le nonce prouve que la requête vient de votre formulaire.
  2. show_in_rest sur le CPT expose la ressource /wp/v2/livre ; show_in_rest sur la meta (register_post_meta) expose le champ meta.wpr_isbn dans cette ressource. Sans le second, l’API renverrait le livre sans son ISBN → le front Next.js ne pourrait pas l’afficher.
  3. Les attributs viennent du contenu (potentiellement d’un éditeur) : sans absint/sanitize_title, une valeur malveillante ou malformée pourrait fausser/injecter la requête (tax_query) ou produire un affichage non maîtrisé. On assainit selon le type attendu (entier, slug).

🧠 Quiz de révision

1. Quels composants de la Partie 7 wpr-books combine-t-il ?

CPT + taxonomie, métadonnées (+ metabox sécurisée et REST), page de réglages, shortcode, sécurité (4 piliers), i18n.

2. Où enregistre-t-on le CPT à l’activation, et pourquoi flush ensuite ?

Dans register_activation_hook : on enregistre le CPT puis flush_rewrite_rules() une fois, pour que les URLs /livres/ fonctionnent (éviter le 404).

3. Qu’est-ce qui rend wpr-books « headless-ready » ?

show_in_rest sur le CPT/taxonomie et les meta exposées via register_post_meta : la ressource et ses champs sont dans la REST API.

4. Quelles sont les 4 gardes dans save() ?

Nonce, autosave (DOING_AUTOSAVE), capability (edit_post), sanitize de l’entrée.

5. Comment wpr-books sera-t-il étendu dans les parties suivantes ?

Par un bloc React (Partie 8) et un front Next.js headless (Partie 9), puis en plugin freemium (Partie 13).


Partie suivante : Partie 8 — Gutenberg : blocs custom en React.