Intermediaire 2 min de lecture · 259 mots

Créer un plugin WordPress personnalisé : Architecture et bonnes pratiques

Estimated reading time: 1 minute

Introduction

Le développement de plugins WordPress a considérablement évolué en 2025. Avec plus de 92,81% des vulnérabilités WordPress provenant de plugins tiers, une architecture solide et des pratiques de développement rigoureuses ne sont plus optionnelles. Ce guide complet vous accompagnera dans la création d’un plugin WordPress professionnel, en mettant l’accent sur l’architecture orientée objet, la sécurité et les performances.

Architecture de base d’un plugin WordPress

Structure de fichiers recommandée

Une architecture modulaire facilite la maintenance et l’évolution de votre plugin. Voici la structure que nous recommandons pour 2025 :

mon-plugin/
├── mon-plugin.php
├── uninstall.php
├── readme.txt
├── includes/
│   ├── class-mon-plugin.php
│   ├── class-activator.php
│   ├── class-deactivator.php
│   └── class-loader.php
├── admin/
│   ├── class-admin.php
│   ├── css/
│   └── js/
├── public/
│   ├── class-public.php
│   ├── css/
│   └── js/
└── languages/

Le fichier principal du plugin

Le fichier principal (mon-plugin.php) doit être minimal et servir uniquement de point d’entrée. Voici un exemple complet et testé :


  Plugin Name: Mon Plugin Professionnel
  Plugin URI: https://exemple.com/mon-plugin
  Description: Un plugin WordPress construit avec les meilleures pratiques 2025
  Version: 1.0.0
  Requires at least: 6.0
  Requires PHP: 8.0
  Author: Votre Nom
  Author URI: https://exemple.com
  License: GPL v2 or later
  License URI: https://www.gnu.org/licenses/gpl-2.0.html
  Text Domain: mon-plugin
  Domain Path: /languages
 /

// Sécurité : Empêcher l'accès direct
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

// Définir les constantes du plugin
define( 'MONPLUGINVERSION', '1.0.0' );
define( 'MONPLUGINPATH', plugindirpath( FILE ) );
define( 'MONPLUGINURL', plugindirurl( FILE ) );
define( 'MONPLUGINBASENAME', pluginbasename( FILE ) );

// Autoloader personnalisé
splautoloadregister( function ( $class ) {
    $prefix = 'MonPlugin';
    $basedir = MONPLUGINPATH . 'includes/';

    $len = strlen( $prefix );
    if ( strncmp( $prefix, $class, $len ) !== 0 ) {
        return;
    }

    $relativeclass = substr( $class, $len );
    $file = $basedir . 'class-' . strtolower( strreplace( '', '-', $relativeclass ) ) . '.php';

    if ( fileexists( $file ) ) {
        require $file;
    }
} );

// Hooks d'activation et de désactivation
registeractivationhook( FILE, array( 'MonPluginActivator', 'activate' ) );
registerdeactivationhook( FILE, array( 'MonPluginDeactivator', 'deactivate' ) );

// Initialiser le plugin
function monplugininit() {
    $plugin = MonPluginCore::getinstance();
    $plugin->run();
}
addaction( 'pluginsloaded', 'monplugininit' );

Architecture orientée objet (OOP)

La classe principale (Singleton Pattern)

L’utilisation du pattern Singleton garantit qu’une seule instance du plugin existe. Créez includes/class-core.php :

name;
    private $version;

    /
      Singleton pattern
     /
    public static function getinstance() {
        if ( null === self::$instance ) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    /
      Constructeur privé pour empêcher l'instanciation directe
     /
    private function construct() {
        $this->pluginname = 'mon-plugin';
        $this->version = MONPLUGINVERSION;

        $this->loaddependencies();
        $this->setlocale();
        $this->defineadminhooks();
        $this->definepublichooks();
    }

    /
      Charger les dépendances
     /
    private function loaddependencies() {
        requireonce MONPLUGINPATH . 'includes/class-loader.php';
        requireonce MONPLUGINPATH . 'admin/class-admin.php';
        requireonce MONPLUGINPATH . 'public/class-public.php';

        $this->loader = new Loader();
    }

    /
      Définir la localisation
     /
    private function setlocale() {
        $this->loader->addaction( 'pluginsloaded', $this, 'loadplugintextdomain' );
    }

    /
      Charger les traductions
     /
    public function loadplugintextdomain() {
        loadplugintextdomain(
            'mon-plugin',
            false,
            dirname( MONPLUGINBASENAME ) . '/languages/'
        );
    }

    /
      Enregistrer les hooks admin
     /
    private function defineadminhooks() {
        $admin = new Admin( $this->getpluginname(), $this->getversion() );

        $this->loader->addaction( 'adminenqueuescripts', $admin, 'enqueuestyles' );
        $this->loader->addaction( 'adminenqueuescripts', $admin, 'enqueuescripts' );
        $this->loader->addaction( 'adminmenu', $admin, 'addpluginadminmenu' );
    }

    /
      Enregistrer les hooks publics
     /
    private function definepublichooks() {
        $public = new PublicFacing( $this->getpluginname(), $this->getversion() );

        $this->loader->addaction( 'wpenqueuescripts', $public, 'enqueuestyles' );
        $this->loader->addaction( 'wpenqueuescripts', $public, 'enqueuescripts' );
    }

    /
      Exécuter le loader
     /
    public function run() {
        $this->loader->run();
    }

    public function getpluginname() {
        return $this->pluginname;
    }

    public function getversion() {
        return $this->version;
    }

    public function getloader() {
        return $this->loader;
    }
}

Le Loader : Gestionnaire de hooks centralisé

Le Loader centralise tous les hooks WordPress. Créez includes/class-loader.php :

construct() {
        $this->actions = array();
        $this->filters = array();
    }

    /
      Ajouter une action WordPress
     /
    public function addaction( $hook, $component, $callback, $priority = 10, $acceptedargs = 1 ) {
        $this->actions = $this->add( $this->actions, $hook, $component, $callback, $priority, $acceptedargs );
    }

    /
      Ajouter un filtre WordPress
     /
    public function addfilter( $hook, $component, $callback, $priority = 10, $acceptedargs = 1 ) {
        $this->filters = $this->add( $this->filters, $hook, $component, $callback, $priority, $acceptedargs );
    }

    /
      Méthode privée pour ajouter hooks/filters
     /
    private function add( $hooks, $hook, $component, $callback, $priority, $acceptedargs ) {
        $hooks[] = array(
            'hook'          => $hook,
            'component'     => $component,
            'callback'      => $callback,
            'priority'      => $priority,
            'acceptedargs' => $acceptedargs
        );

        return $hooks;
    }

    /
      Enregistrer tous les hooks avec WordPress
     /
    public function run() {
        foreach ( $this->filters as $hook ) {
            addfilter(
                $hook['hook'],
                array( $hook['component'], $hook['callback'] ),
                $hook['priority'],
                $hook['acceptedargs']
            );
        }

        foreach ( $this->actions as $hook ) {
            addaction(
                $hook['hook'],
                array( $hook['component'], $hook['callback'] ),
                $hook['priority'],
                $hook['acceptedargs']
            );
        }
    }
}

Activation et désactivation du plugin

La classe Activator

Créez includes/class-activator.php pour gérer l’activation :


      Actions lors de l'activation
     /
    public static function activate() {
        // Vérifier la version PHP minimale
        if ( versioncompare( PHPVERSION, '8.0', '<' ) ) {
            deactivateplugins( MONPLUGINBASENAME );
            wpdie(
                eschtml( 'Ce plugin nécessite PHP 8.0 ou supérieur.', 'mon-plugin' ),
                eschtml( 'Erreur d'activation', 'mon-plugin' ),
                array( 'backlink' => true )
            );
        }

        // Vérifier la version WordPress minimale
        if ( versioncompare( getbloginfo( 'version' ), '6.0', '<' ) ) {
            deactivateplugins( MONPLUGINBASENAME );
            wpdie(
                eschtml( 'Ce plugin nécessite WordPress 6.0 ou supérieur.', 'mon-plugin' ),
                eschtml( 'Erreur d'activation', 'mon-plugin' ),
                array( 'backlink' => true )
            );
        }

        // Créer les tables de base de données si nécessaire
        self::createdatabasetables();

        // Ajouter les options par défaut
        self::adddefaultoptions();

        // Flush les règles de réécriture
        flushrewriterules();

        // Logger l'activation
        errorlog( 'Mon Plugin activé - Version ' . MONPLUGINVERSION );
    }

    /
      Créer les tables de base de données
     /
    private static function createdatabasetables() {
        global $wpdb;

        $charsetcollate = $wpdb->getcharsetcollate();
        $tablename = $wpdb->prefix . 'monplugindata';

        $sql = "CREATE TABLE IF NOT EXISTS $tablename (
            id bigint(20) UNSIGNED NOT NULL AUTOINCREMENT,
            userid bigint(20) UNSIGNED NOT NULL,
            datakey varchar(255) NOT NULL,
            datavalue longtext NOT NULL,
            createdat datetime DEFAULT CURRENTTIMESTAMP,
            updatedat datetime DEFAULT CURRENTTIMESTAMP ON UPDATE CURRENTTIMESTAMP,
            PRIMARY KEY  (id),
            KEY userid (userid),
            KEY datakey (datakey)
        ) $charsetcollate;";

        requireonce ABSPATH . 'wp-admin/includes/upgrade.php';
        dbDelta( $sql );
    }

    /
      Ajouter les options par défaut
     /
    private static function adddefaultoptions() {
        $defaultoptions = array(
            'version' => MONPLUGINVERSION,
            'activatedat' => currenttime( 'mysql' ),
            'enablefeaturex' => true,
            'cacheduration' => 3600,
        );

        addoption( 'monpluginoptions', $defaultoptions );
    }
}

La classe Deactivator

Créez includes/class-deactivator.php :


      Actions lors de la désactivation
     /
    public static function deactivate() {
        // Nettoyer les caches
        wpcacheflush();

        // Nettoyer les transients
        self::cleantransients();

        // Flush les règles de réécriture
        flushrewriterules();

        // Logger la désactivation
        errorlog( 'Mon Plugin désactivé' );
    }

    /
      Nettoyer les transients du plugin
     /
    private static function cleantransients() {
        global $wpdb;

        $wpdb->query(
            $wpdb->prepare(
                "DELETE FROM {$wpdb->options}
                WHERE optionname LIKE %s
                OR optionname LIKE %s",
                $wpdb->esclike( 'transientmonplugin' ) . '%',
                $wpdb->esclike( 'transienttimeoutmonplugin' ) . '%'
            )
        );
    }
}

Bonnes pratiques de sécurité

Validation et sanitization des données

Toujours valider et nettoyer les entrées utilisateur :


      Valider et sauvegarder les options
     /
    public function savesettings() {
        // Vérifier le nonce
        if ( ! isset( $POST['monpluginnonce'] ) ||
             ! wpverifynonce( $POST['monpluginnonce'], 'monpluginsavesettings' ) ) {
            wpdie( eschtml( 'Action non autorisée.', 'mon-plugin' ) );
        }

        // Vérifier les permissions
        if ( ! currentusercan( 'manageoptions' ) ) {
            wpdie( eschtml( 'Vous n'avez pas les permissions nécessaires.', 'mon-plugin' ) );
        }

        // Sanitize les données
        $options = array();

        // Texte simple
        if ( isset( $POST['textfield'] ) ) {
            $options['textfield'] = sanitizetextfield( wpunslash( $POST['textfield'] ) );
        }

        // Email
        if ( isset( $POST['emailfield'] ) ) {
            $options['emailfield'] = sanitizeemail( wpunslash( $POST['emailfield'] ) );
        }

        // URL
        if ( isset( $POST['urlfield'] ) ) {
            $options['urlfield'] = escurlraw( wpunslash( $POST['urlfield'] ) );
        }

        // Entier
        if ( isset( $POST['numberfield'] ) ) {
            $options['numberfield'] = absint( $POST['numberfield'] );
        }

        // Textarea (autoriser certaines balises HTML)
        if ( isset( $POST['textareafield'] ) ) {
            $options['textareafield'] = wpksespost( wpunslash( $POST['textareafield'] ) );
        }

        // Checkbox (booléen)
        $options['checkboxfield'] = isset( $POST['checkboxfield'] ) ? true : false;

        // Sauvegarder avec validation supplémentaire
        updateoption( 'monpluginoptions', $options );

        // Rediriger avec message de succès
        wpsaferedirect( addqueryarg( 'settings-updated', 'true', wpgetreferer() ) );
        exit;
    }
}

Requêtes préparées pour la base de données

Utilisez toujours des requêtes préparées pour éviter les injections SQL :


      Récupérer des données de manière sécurisée
     /
    public function getuserdata( $userid, $datakey ) {
        global $wpdb;

        $tablename = $wpdb->prefix . 'monplugindata';

        // MAUVAIS : Vulnérable aux injections SQL
        // $result = $wpdb->getrow( "SELECT  FROM $tablename WHERE userid = $userid AND datakey = '$datakey'" );

        // BON : Requête préparée
        $result = $wpdb->getrow(
            $wpdb->prepare(
                "SELECT  FROM $tablename WHERE userid = %d AND datakey = %s",
                $userid,
                $datakey
            )
        );

        return $result;
    }

    /
      Insérer des données de manière sécurisée
     /
    public function saveuserdata( $userid, $datakey, $datavalue ) {
        global $wpdb;

        $tablename = $wpdb->prefix . 'monplugindata';

        // Utiliser wpdb::insert pour plus de sécurité
        $inserted = $wpdb->insert(
            $tablename,
            array(
                'userid'    => $userid,
                'datakey'   => $datakey,
                'datavalue' => $datavalue,
            ),
            array(
                '%d', // userid est un entier
                '%s', // datakey est une chaîne
                '%s', // datavalue est une chaîne
            )
        );

        return $inserted !== false;
    }
}

Optimisation des performances

Mise en cache intelligente

Implémentez un système de cache pour réduire les requêtes à la base de données :

group = 'monplugin';
    private $cacheduration = 3600; // 1 heure

    /
      Récupérer une valeur du cache
     /
    public function get( $key ) {
        return wpcacheget( $key, $this->cachegroup );
    }

    /
      Définir une valeur dans le cache
     /
    public function set( $key, $value, $duration = null ) {
        $duration = $duration ?? $this->cacheduration;
        return wpcacheset( $key, $value, $this->cachegroup, $duration );
    }

    /
      Supprimer une valeur du cache
     /
    public function delete( $key ) {
        return wpcachedelete( $key, $this->cachegroup );
    }

    /
      Récupérer ou générer une valeur avec cache
     /
    public function remember( $key, $callback, $duration = null ) {
        $value = $this->get( $key );

        if ( false === $value ) {
            $value = $callback();
            $this->set( $key, $value, $duration );
        }

        return $value;
    }

    /
      Exemple d'utilisation avec transients pour cache persistant
     /
    public function getexpensivedata( $userid ) {
        $transientkey = 'monplugindata' . $userid;

        // Essayer de récupérer depuis le transient
        $data = gettransient( $transientkey );

        if ( false === $data ) {
            // Données non en cache, les générer
            global $wpdb;
            $data = $wpdb->getresults(
                $wpdb->prepare(
                    "SELECT  FROM {$wpdb->prefix}monplugindata WHERE userid = %d",
                    $userid
                )
            );

            // Mettre en cache pour 1 heure
            settransient( $transientkey, $data, HOURINSECONDS );
        }

        return $data;
    }

    /
      Invalider le cache lors de la mise à jour
     /
    public function invalidateusercache( $userid ) {
        $transientkey = 'monplugindata' . $userid;
        deletetransient( $transientkey );
    }
}

Chargement conditionnel des assets

Ne chargez les scripts et styles que là où ils sont nécessaires :

name;
    private $version;

    public function construct( $pluginname, $version ) {
        $this->pluginname = $pluginname;
        $this->version = $version;
    }

    /
      Charger les styles uniquement sur les pages du plugin
     /
    public function enqueuestyles( $hook ) {
        // Charger uniquement sur les pages admin du plugin
        if ( 'toplevelpagemon-plugin' !== $hook &&
             strpos( $hook, 'mon-plugin' ) === false ) {
            return;
        }

        wpenqueuestyle(
            $this->pluginname . '-admin',
            MONPLUGINURL . 'admin/css/admin.css',
            array(),
            $this->version,
            'all'
        );
    }

    /
      Charger les scripts avec dépendances
     /
    public function enqueuescripts( $hook ) {
        if ( 'toplevelpagemon-plugin' !== $hook ) {
            return;
        }

        // Enregistrer le script
        wpenqueuescript(
            $this->pluginname . '-admin',
            MONPLUGINURL . 'admin/js/admin.js',
            array( 'jquery', 'wp-i18n' ), // Dépendances
            $this->version,
            true // Charger dans le footer
        );

        // Passer des données PHP à JavaScript de manière sécurisée
        wplocalizescript(
            $this->pluginname . '-admin',
            'monPluginData',
            array(
                'ajaxUrl' => adminurl( 'admin-ajax.php' ),
                'nonce'   => wpcreatenonce( 'monpluginajax' ),
                'strings' => array(
                    'confirmDelete' => ( 'Êtes-vous sûr de vouloir supprimer cet élément ?', 'mon-plugin' ),
                    'error'         => _( 'Une erreur est survenue.', 'mon-plugin' ),
                ),
            )
        );

        // Activer les traductions pour le script
        wpsetscripttranslations( $this->pluginname . '-admin', 'mon-plugin' );
    }
}

Erreurs courantes à éviter

1. Ne pas utiliser de prefixes

MAUVAIS :

function getdata() {
    // Risque de conflit avec d'autres plugins
}

BON :

function monplugingetdata() {
    // Unique et sûr
}

2. Charger du code inutilement

MAUVAIS :

// Charger tout le temps, même sur le front-end
requireonce 'admin/class-admin.php';

BON :

// Charger uniquement dans l'admin
if ( isadmin() ) {
    requireonce 'admin/class-admin.php';
}

3. Ne pas nettoyer lors de la désinstallation

Créez un fichier uninstall.php pour nettoyer proprement :

UNINSTALLPLUGIN' ) ) {
    exit;
}

// Supprimer les options
deleteoption( 'monpluginoptions' );

// Supprimer les transients
global $wpdb;
$wpdb->query(
    "DELETE FROM {$wpdb->options}
    WHERE optionname LIKE 'transientmonplugin%'
    OR optionname LIKE 'transienttimeoutmonplugin%'"
);

// Supprimer les tables (optionnel - demander à l'utilisateur)
// $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}monplugindata" );

// Supprimer les capacités personnalisées
$role = getrole( 'administrator' );
if ( $role ) {
    $role->removecap( 'managemonplugin' );
}

Testabilité et debugging

Logging personnalisé

Implémentez un système de logging pour faciliter le debugging :


      Logger un message de debug
     /
    public static function debug( $message, $context = array() ) {
        if ( ! self::$enabled || ! WPDEBUG ) {
            return;
        }

        $logmessage = sprintf(
            '[Mon Plugin] %s: %s',
            date( 'Y-m-d H:i:s' ),
            isarray( $message ) ? printr( $message, true ) : $message
        );

        if ( ! empty( $context ) ) {
            $logmessage .= ' | Context: ' . printr( $context, true );
        }

        errorlog( $logmessage );
    }

    /
      Logger une erreur
     /
    public static function error( $message, $context = array() ) {
        self::debug( 'ERROR: ' . $message, $context );
    }

    /
      Logger un warning
     /
    public static function warning( $message, $context = array() ) {
        self::debug( 'WARNING: ' . $message, $context );
    }
}

Conclusion

Le développement de plugins WordPress en 2025 nécessite une approche professionnelle centrée sur l’architecture orientée objet, la sécurité et les performances. En suivant ces pratiques :

  • Utilisez une architecture modulaire avec des namespaces
  • Implémentez des patterns de conception (Singleton, Factory, Observer)
  • Sécurisez toutes les entrées avec validation et sanitization
  • Utilisez des requêtes préparées pour la base de données
  • Optimisez les performances avec du caching intelligent
  • Chargez les assets de manière conditionnelle
  • Documentez votre code et implémentez du logging
  • Nettoyez proprement lors de la désinstallation
  • Rappelez-vous que 92,81% des vulnérabilités WordPress proviennent des plugins. Votre responsabilité en tant que développeur est de créer du code sûr, performant et maintenable.

    Ressources supplémentaires

  • Plugin Handbook officiel WordPress
  • WordPress Coding Standards
  • Plugin Security Guidelines
  • WordPress Performance Best Practices

Mots-clés: développement plugin WordPress, architecture WordPress, POO WordPress, sécurité WordPress, performances WordPress, bonnes pratiques WordPress 2025, plugin WordPress personnalisé, WordPress namespace, WordPress autoloader

Une remarque, un retour ?

Cet article est vivant — corrections, contre-arguments et retours de production sont les bienvenus. Trois canaux, choisissez celui qui vous convient.