Développement Web

Anatomie d’une extension WordPress : le squelette pas à pas

14 min de lecture

📍 Guide principal : Développement WordPress : plugins, thèmes et blocs. Ce tutoriel ouvre la série ; pour la vue d’ensemble, lisez d’abord le guide.

Introduction

Un client vous demande un annuaire des artisans de son quartier, intégré à son site WordPress. Vous pourriez chercher une extension toute faite et prier pour qu’elle corresponde. Ou vous pouvez écrire la vôtre, taillée exactement à la demande, et la facturer comme un développement sur mesure. La deuxième voie commence toujours par la même étape : poser le squelette de l’extension. C’est un fichier, quelques constantes, deux ou trois fonctions branchées au bon endroit — et c’est ce qui distingue une extension professionnelle d’un bout de code collé dans le mauvais fichier. À la fin de ce tutoriel, vous aurez une extension Annuaire Quartier qui s’active, se désactive et se désinstalle proprement, prête à recevoir toute la logique des tutoriels suivants.

🎯 Ce que vous allez apprendre

  • Écrire l’en-tête normalisé qui fait reconnaître votre dossier comme une extension.
  • Protéger vos fichiers contre l’accès direct et définir des constantes propres.
  • Brancher les routines d’activation, de désactivation et de désinstallation.
  • Organiser le code en dossiers pour qu’il reste lisible quand le projet grossit.
  • Charger le text domain pour préparer la traduction dès le premier jour.

🛠️ Ce que vous allez construire

Un dossier annuaire-quartier/ contenant un fichier principal avec son en-tête, une structure de sous-dossiers (includes/, admin/, assets/, languages/), et trois comportements de cycle de vie. Une fois déposé dans wp-content/plugins/, il apparaîtra dans la liste des extensions de l’administration, prêt à être activé. Il ne fera encore presque rien de visible — c’est normal : un squelette est fait pour porter le reste.

Prérequis

  • Un WordPress local en version 7.0 (ou 6.x récent), avec un accès aux fichiers.
  • PHP 7.4 minimum, idéalement 8.1 ou plus récent.
  • Un éditeur de code et des notions de PHP (fonctions, tableaux associatifs).
  • Test express : si vous savez écrire une fonction PHP et l’appeler, vous êtes prêt. Sinon, révisez les bases du langage avant de continuer.
  • ⏱️ Temps estimé : ~30 minutes.

Étape 1 — Créer le dossier et le fichier principal

WordPress reconnaît une extension à un détail précis : un fichier PHP, placé dans wp-content/plugins/, qui contient un bloc de commentaire d’en-tête au format attendu. La convention veut que le dossier et le fichier principal portent le même nom que le slug de l’extension. On crée donc wp-content/plugins/annuaire-quartier/ et, dedans, annuaire-quartier.php.

<?php
/**
 * Plugin Name:       Annuaire Quartier
 * Description:       Annuaire des artisans et services de proximité.
 * Version:           1.0.0
 * Requires at least: 6.6
 * Requires PHP:      7.4
 * Author:            Votre Nom
 * License:           GPL-2.0-or-later
 * License URI:       https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:       annuaire-quartier
 * Domain Path:       /languages
 */

Chaque ligne a un rôle. Plugin Name est le seul champ obligatoire — c’est lui qui fait apparaître l’extension dans la liste. Requires at least et Requires PHP évitent qu’on active votre code sur un environnement trop ancien. Text Domain est l’identifiant qui reliera plus tard vos chaînes à leurs traductions ; par convention, il est identique au slug. Enregistrez le fichier, puis allez dans Extensions dans l’administration : « Annuaire Quartier » doit apparaître dans la liste. Si ce n’est pas le cas, vérifiez l’emplacement du fichier et l’orthographe exacte de Plugin Name:.

Point d’étape — Votre extension apparaît dans la liste des extensions et peut être activée. Si elle est absente, le dossier n’est pas au bon endroit ou l’en-tête est mal formé (un espace manquant après : suffit à le casser).

Étape 2 — Verrouiller l’accès direct

Vos fichiers PHP sont accessibles par leur URL directe. Si quelqu’un appelle .../plugins/annuaire-quartier/annuaire-quartier.php dans son navigateur, le fichier s’exécute hors du contexte de WordPress, ce qui peut révéler des erreurs ou, pire, exécuter du code sans les garde-fous habituels. La parade tient en une ligne, à placer juste après l’en-tête : on vérifie que la constante ABSPATH, définie uniquement quand WordPress a démarré, existe bien.

// Bloque l'accès direct au fichier.
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

Si la constante n’est pas définie, c’est que le fichier a été appelé en dehors de WordPress : on coupe immédiatement avec exit. Ce réflexe se met en tête de chaque fichier PHP de l’extension, pas seulement le principal. C’est une habitude qui ne coûte rien et ferme une porte que les scanners de sécurité vérifient systématiquement.

Étape 3 — Définir des constantes propres

Tout au long du projet, vous aurez besoin de connaître le chemin du dossier de l’extension (pour inclure des fichiers) et son URL (pour charger des feuilles de style ou des scripts). Plutôt que de recalculer ces valeurs partout, on les fige une fois dans des constantes préfixées. Le préfixe AQ_ (pour Annuaire Quartier) évite toute collision avec une autre extension.

define( 'AQ_VERSION', '1.0.0' );
define( 'AQ_PATH', plugin_dir_path( __FILE__ ) );
define( 'AQ_URL', plugin_dir_url( __FILE__ ) );

plugin_dir_path( __FILE__ ) renvoie le chemin absolu du dossier sur le serveur, terminé par un séparateur ; plugin_dir_url( __FILE__ ) renvoie l’URL publique correspondante. À partir de là, inclure un fichier devient lisible : require_once AQ_PATH . 'includes/fichier.php';. La constante AQ_VERSION, elle, servira à versionner les fichiers chargés et à détecter les mises à jour. On définit la version à un seul endroit pour ne jamais avoir à la chercher.

Étape 4 — Brancher l’activation et la désactivation

WordPress déclenche un signal précis au moment où l’utilisateur clique sur « Activer » et un autre sur « Désactiver ». On s’y branche avec register_activation_hook() et register_deactivation_hook(). Ces deux fonctions sont particulières : elles attendent le chemin du fichier principal en premier argument, d’où le __FILE__. L’activation est l’endroit où l’on prépare le terrain — créer une option, planifier une tâche, régénérer les permaliens. La désactivation nettoie ce qui est temporaire.

function aq_activate() {
    // Mémorise la version installée pour gérer les futures mises à jour.
    add_option( 'aq_version', AQ_VERSION );

    // Les permaliens des futurs types de contenu doivent être régénérés.
    flush_rewrite_rules();
}
register_activation_hook( __FILE__, 'aq_activate' );

function aq_deactivate() {
    // Nettoie les règles de réécriture laissées par l'extension.
    flush_rewrite_rules();
}
register_deactivation_hook( __FILE__, 'aq_deactivate' );

Pourquoi flush_rewrite_rules() dès maintenant, alors qu’aucun type de contenu n’existe encore ? Parce que c’est exactement le piège que rencontrent tous les débutants au tutoriel suivant : on déclare un type « artisan », on visite sa page, et on tombe sur une erreur 404. La cause est que WordPress met en cache ses règles d’URL et ne les recalcule pas tout seul. En régénérant les permaliens à l’activation, on s’évite ce piège à l’avance. Attention toutefois : cette fonction est coûteuse, on ne l’appelle jamais à chaque chargement de page, uniquement à l’activation et à la désactivation.

Point d’étape — Activez puis désactivez l’extension depuis l’administration : aucune erreur ne doit apparaître. Dans la base, une option aq_version contenant 1.0.0 a été créée à l’activation. Vous pouvez le vérifier avec un outil d’inspection de la base ou une extension de débogage.

Étape 5 — Organiser le code en dossiers

Un fichier unique suffit pour un squelette, mais devient vite illisible. On adopte tout de suite une structure que les développeurs WordPress reconnaissent au premier coup d’œil. Le fichier principal ne contiendra que l’en-tête, les constantes, les hooks de cycle de vie, et des inclusions ; toute la logique vivra dans des fichiers dédiés.

annuaire-quartier/
├── annuaire-quartier.php   (fichier principal, point d'entrée)
├── uninstall.php           (nettoyage à la désinstallation)
├── includes/               (logique : types de contenu, API…)
├── admin/                  (écrans d'administration)
├── assets/                 (CSS, JS, images)
└── languages/              (fichiers de traduction)

Pour charger un fichier de logique, on l’inclut depuis le fichier principal en s’appuyant sur la constante de chemin définie plus tôt. On utilise require_once plutôt que include : si le fichier est introuvable, on veut une erreur franche plutôt qu’un comportement silencieux et imprévisible.

// Chargement des modules de l'extension.
require_once AQ_PATH . 'includes/class-aq-artisan.php';
require_once AQ_PATH . 'includes/rest-api.php';

Ces fichiers n’existent pas encore — on les créera dans les tutoriels suivants. Pour l’instant, vous pouvez commenter ces lignes ou créer des fichiers vides protégés par la garde ABSPATH. L’important est que la convention soit posée : quand vous reviendrez ajouter le type « artisan », vous saurez exactement où le mettre.

Étape 6 — Préparer la traduction

Même un projet francophone gagne à être traduisible. WordPress charge les traductions d’une extension à partir d’un text domain — l’identifiant déclaré dans l’en-tête. On indique à WordPress où chercher les fichiers de langue en appelant load_plugin_textdomain() sur le hook init, c’est-à-dire une fois que WordPress est prêt mais avant l’affichage.

function aq_load_textdomain() {
    load_plugin_textdomain(
        'annuaire-quartier',
        false,
        dirname( plugin_basename( __FILE__ ) ) . '/languages'
    );
}
add_action( 'init', 'aq_load_textdomain' );

Le premier argument est le text domain, le troisième pointe vers le dossier languages/ relatif à l’extension. À partir de là, chaque chaîne que vous afficherez sera enveloppée dans __( 'Mon texte', 'annuaire-quartier' ) pour une valeur de retour, ou esc_html_e( 'Mon texte', 'annuaire-quartier' ) pour un affichage direct échappé. Ce réflexe, pris dès le squelette, vous évite la corvée de tout reprendre le jour où une version arabe est demandée — un besoin réel pour beaucoup de sites de la région.

Étape 7 — Écrire la routine de désinstallation

Désactiver une extension la met en sommeil ; la désinstaller doit effacer ses traces. WordPress cherche, à la désinstallation, un fichier nommé uninstall.php à la racine de l’extension et l’exécute. C’est là qu’on supprime les options et les données créées. Ce fichier doit vérifier une constante spécifique, WP_UNINSTALL_PLUGIN, qui n’est définie que dans ce contexte précis : c’est la garantie qu’on ne supprime rien par accident.

<?php
// uninstall.php
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
    exit;
}

// Supprime les options créées par l'extension.
delete_option( 'aq_version' );

Plus tard, quand l’extension stockera des artisans et leurs champs, c’est ici qu’on choisira de tout effacer ou de tout conserver. Beaucoup d’extensions proposent même un réglage « supprimer les données à la désinstallation » pour laisser le choix à l’utilisateur. Pour l’instant, on se contente de retirer l’option de version. Désinstallez l’extension depuis l’administration et vérifiez que l’option aq_version a bien disparu de la base.

Point d’étape — Après désinstallation, l’option aq_version n’existe plus dans la base. Votre extension range sa chambre en partant : c’est la marque d’un code soigné.

Étape 8 — Vérification de bout en bout

Reprenons le cycle complet pour prouver que le squelette tient. Activez l’extension : aucune erreur, l’option de version apparaît. Désactivez-la : pas d’erreur, les permaliens sont nettoyés. Réactivez-la, puis désinstallez-la : l’option disparaît. Si ces trois temps se déroulent sans le moindre avertissement PHP (avec WP_DEBUG activé), votre fondation est solide. Tout le reste de la série viendra se greffer dessus sans jamais remettre cette base en question.

🐞 Pièges fréquents

Symptôme / erreur Cause probable Correctif
L’extension n’apparaît pas dans la liste Fichier mal placé ou en-tête cassé Vérifier le chemin wp-content/plugins/annuaire-quartier/annuaire-quartier.php et l’espace après Plugin Name:
Cannot redeclare function aq_activate() Deux fonctions du même nom, ou fichier inclus deux fois Préfixer tous les noms, utiliser require_once
Page blanche à l’activation Erreur fatale PHP silencieuse Activer WP_DEBUG dans wp-config.php pour voir le message réel
uninstall.php ignoré Fichier dans un sous-dossier au lieu de la racine Le placer à la racine de l’extension, à côté du fichier principal
Données non supprimées à la désinstallation Garde WP_UNINSTALL_PLUGIN mal écrite ou logique manquante Vérifier la constante et la présence des appels delete_option()

✅ Récapitulatif

Vous venez de construire le squelette d’une extension WordPress digne de ce nom : un en-tête reconnu, une protection contre l’accès direct, des constantes propres, des routines d’activation et de désactivation, une structure de dossiers claire, le chargement du text domain et une désinstallation propre. Ce n’est pas spectaculaire à l’écran, mais c’est la fondation que tout développeur WordPress pose avant d’écrire la moindre fonctionnalité. Annuaire Quartier est désormais un conteneur prêt à accueillir sa logique.

🧾 Aide-mémoire

Élément Rôle
Plugin Name: Seul champ d’en-tête obligatoire ; fait reconnaître l’extension
if ( ! defined( 'ABSPATH' ) ) exit; Bloque l’accès direct au fichier
plugin_dir_path() / plugin_dir_url() Chemin serveur / URL publique de l’extension
register_activation_hook() Code exécuté à l’activation
register_deactivation_hook() Code exécuté à la désactivation
flush_rewrite_rules() Régénère les permaliens (activation/désactivation uniquement)
load_plugin_textdomain() Charge les traductions de l’extension
uninstall.php + WP_UNINSTALL_PLUGIN Nettoyage à la désinstallation

💪 À vous de jouer

Ajoutez à l’activation un contrôle de version minimale : si la version de WordPress installée est inférieure à 6.6, désactivez l’extension et affichez un message d’erreur clair plutôt que de laisser le code planter plus loin.

Voir une solution
function aq_activate() {
    if ( version_compare( get_bloginfo( 'version' ), '6.6', '<' ) ) {
        deactivate_plugins( plugin_basename( __FILE__ ) );
        wp_die( 'Annuaire Quartier requiert WordPress 6.6 ou plus récent.' );
    }
    add_option( 'aq_version', AQ_VERSION );
    flush_rewrite_rules();
}
register_activation_hook( __FILE__, 'aq_activate' );

get_bloginfo( 'version' ) renvoie la version de WordPress ; version_compare() la compare proprement ; wp_die() stoppe l’activation avec un message lisible.

Tutoriels de la série

Pour aller plus loin

FAQ

Puis-je mettre tout mon code dans un seul fichier ?

Techniquement oui, mais c’est déconseillé dès que le projet dépasse quelques fonctions. La structure en dossiers rend le code lisible, facilite le travail à plusieurs et accélère le débogage. Le coût d’adoption est nul si on la met en place dès le squelette.

Quelle différence entre désactiver et désinstaller ?

La désactivation met l’extension en pause : son code ne s’exécute plus mais ses données restent. La désinstallation supprime l’extension et déclenche uninstall.php pour effacer ses données. Un utilisateur peut désactiver pour diagnostiquer un conflit sans rien perdre.

Dois-je vraiment préfixer toutes mes fonctions ?

Oui. WordPress charge toutes les extensions actives dans le même espace de noms global. Deux fonctions nommées get_artisans() dans deux extensions provoquent une erreur fatale. Un préfixe court comme aq_, ou l’encapsulation dans une classe, résout le problème.

Pourquoi régénérer les permaliens à l’activation alors qu’il n’y a pas encore de type de contenu ?

Par anticipation : c’est le piège classique du tutoriel suivant. En prenant l’habitude de régénérer à l’activation, vous évitez les 404 qui surviennent quand un nouveau type de contenu est déclaré et que WordPress n’a pas recalculé ses règles d’URL.

Mon extension peut-elle être traduite si je n’ai pas créé de fichiers de langue ?

Le chargement du text domain prépare le terrain sans rien imposer. Tant qu’aucun fichier de traduction n’est présent, les chaînes s’affichent dans leur langue d’origine. Le jour où vous ajoutez une traduction dans languages/, elle est prise en compte sans modifier le code.

Partager