Intermediaire 1 min de lecture · 159 mots

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

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

run();
}
add_action( 'plugins_loaded', 'mon_plugin_init' );

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 :

plugin_name = 'mon-plugin';
        $this->version = MON_PLUGIN_VERSION;

        $this->load_dependencies();
        $this->set_locale();
        $this->define_admin_hooks();
        $this->define_public_hooks();
    }

    /**
     * Charger les dépendances
     */
    private function load_dependencies() {
        require_once MON_PLUGIN_PATH . 'includes/class-loader.php';
        require_once MON_PLUGIN_PATH . 'admin/class-admin.php';
        require_once MON_PLUGIN_PATH . 'public/class-public.php';

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

    /**
     * Définir la localisation
     */
    private function set_locale() {
        $this->loader->add_action( 'plugins_loaded', $this, 'load_plugin_textdomain' );
    }

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

    /**
     * Enregistrer les hooks admin
     */
    private function define_admin_hooks() {
        $admin = new Admin( $this->get_plugin_name(), $this->get_version() );

        $this->loader->add_action( 'admin_enqueue_scripts', $admin, 'enqueue_styles' );
        $this->loader->add_action( 'admin_enqueue_scripts', $admin, 'enqueue_scripts' );
        $this->loader->add_action( 'admin_menu', $admin, 'add_plugin_admin_menu' );
    }

    /**
     * Enregistrer les hooks publics
     */
    private function define_public_hooks() {
        $public = new PublicFacing( $this->get_plugin_name(), $this->get_version() );

        $this->loader->add_action( 'wp_enqueue_scripts', $public, 'enqueue_styles' );
        $this->loader->add_action( 'wp_enqueue_scripts', $public, 'enqueue_scripts' );
    }

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

    public function get_plugin_name() {
        return $this->plugin_name;
    }

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

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

Le Loader : Gestionnaire de hooks centralisé

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

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

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

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

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

        return $hooks;
    }

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

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

Activation et désactivation du plugin

La classe Activator

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

 true )
            );
        }

        // Vérifier la version WordPress minimale
        if ( version_compare( get_bloginfo( 'version' ), '6.0', '<' ) ) {
            deactivate_plugins( MON_PLUGIN_BASENAME );
            wp_die(
                esc_html**( 'Ce plugin nécessite WordPress 6.0 ou supérieur.', 'mon-plugin' ),
                esc_html**( 'Erreur d'activation', 'mon-plugin' ),
                array( 'back_link' => true )
            );
        }

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

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

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

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

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

        $charset_collate = $wpdb->get_charset_collate();
        $table_name = $wpdb->prefix . 'mon_plugin_data';

        $sql = "CREATE TABLE IF NOT EXISTS $table_name (
            id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
            user_id bigint(20) UNSIGNED NOT NULL,
            data_key varchar(255) NOT NULL,
            data_value longtext NOT NULL,
            created_at datetime DEFAULT CURRENT_TIMESTAMP,
            updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
            PRIMARY KEY  (id),
            KEY user_id (user_id),
            KEY data_key (data_key)
        ) $charset_collate;";

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

    /**
     * Ajouter les options par défaut
     */
    private static function add_default_options() {
        $default_options = array(
            'version' => MON_PLUGIN_VERSION,
            'activated_at' => current_time( 'mysql' ),
            'enable_feature_x' => true,
            'cache_duration' => 3600,
        );

        add_option( 'mon_plugin_options', $default_options );
    }
}

La classe Deactivator

Créez includes/class-deactivator.php :

query(
            $wpdb->prepare(
                "DELETE FROM {$wpdb->options}
                WHERE option_name LIKE %s
                OR option_name LIKE %s",
                $wpdb->esc_like( '*transient_mon_plugin*' ) . '%',
                $wpdb->esc_like( '*transient_timeout_mon_plugin*' ) . '%'
            )
        );
    }
}

Bonnes pratiques de sécurité

Validation et sanitization des données

Toujours valider et nettoyer les entrées utilisateur :

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

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

prefix . 'mon_plugin_data';

        // MAUVAIS : Vulnérable aux injections SQL
        // $result = $wpdb->get_row( "SELECT * FROM $table_name WHERE user_id = $user_id AND data_key = '$data_key'" );

        // BON : Requête préparée
        $result = $wpdb->get_row(
            $wpdb->prepare(
                "SELECT * FROM $table_name WHERE user_id = %d AND data_key = %s",
                $user_id,
                $data_key
            )
        );

        return $result;
    }

    /**
     * Insérer des données de manière sécurisée
     */
    public function save_user_data( $user_id, $data_key, $data_value ) {
        global $wpdb;

        $table_name = $wpdb->prefix . 'mon_plugin_data';

        // Utiliser wpdb::insert pour plus de sécurité
        $inserted = $wpdb->insert(
            $table_name,
            array(
                'user_id'    => $user_id,
                'data_key'   => $data_key,
                'data_value' => $data_value,
            ),
            array(
                '%d', // user_id est un entier
                '%s', // data_key est une chaîne
                '%s', // data_value 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 :

cache_group );
    }

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

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

    /**
     * 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 get_expensive_data( $user_id ) {
        $transient_key = 'mon_plugin_data*' . $user_id;

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

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

            // Mettre en cache pour 1 heure
            set_transient( $transient_key, $data, HOUR_IN_SECONDS );
        }

        return $data;
    }

    /**
     * Invalider le cache lors de la mise à jour
     */
    public function invalidate_user_cache( $user_id ) {
        $transient_key = 'mon_plugin_data*' . $user_id;
        delete_transient( $transient_key );
    }
}

Chargement conditionnel des assets

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

plugin_name = $plugin_name;
        $this->version = $version;
    }

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

        wp_enqueue_style(
            $this->plugin_name . '-admin',
            MON_PLUGIN_URL . 'admin/css/admin.css',
            array(),
            $this->version,
            'all'
        );
    }

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

        // Enregistrer le script
        wp_enqueue_script(
            $this->plugin_name . '-admin',
            MON_PLUGIN_URL . '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
        wp_localize_script(
            $this->plugin_name . '-admin',
            'monPluginData',
            array(
                'ajaxUrl' => admin_url( 'admin-ajax.php' ),
                'nonce'   => wp_create_nonce( 'mon_plugin_ajax' ),
                '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
        wp_set_script_translations( $this->plugin_name . '-admin', 'mon-plugin' );
    }
}

Erreurs courantes à éviter

1. Ne pas utiliser de prefixes

MAUVAIS :

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

BON :

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

2. Charger du code inutilement

MAUVAIS :

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

BON :

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

3. Ne pas nettoyer lors de la désinstallation

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

query(
    "DELETE FROM {$wpdb->options}
    WHERE option_name LIKE '*transient_mon_plugin*%'
    OR option_name LIKE '*transient_timeout_mon_plugin*%'"
);

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

// Supprimer les capacités personnalisées
$role = get_role( 'administrator' );
if ( $role ) {
    $role->remove_cap( 'manage_mon_plugin' );
}

Testabilité et debugging

Logging personnalisé

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

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.

Laisser un commentaire