Portainer vs Dockge vs Yacht : quelle UI pour gérer Docker dans ton homelab ?
J'avais Portainer depuis le premier jour où j'ai installé Docker sur heighliner. C'est la réponse…
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.
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 (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' );
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 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']
);
}
}
}
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 );
}
}
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*' ) . '%'
)
);
}
}
Toujours valider et nettoyer les entrées utilisateur :
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;
}
}
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 );
}
}
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' );
}
}
MAUVAIS :
function get_data() {
// Risque de conflit avec d'autres plugins
}
BON :
function mon_plugin_get_data() {
// Unique et sûr
}
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';
}
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' );
}
Implémentez un système de logging pour faciliter le debugging :
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 :
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.
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
Cet article est vivant - corrections, contre-arguments et retours de production sont les bienvenus. Trois canaux, choisis celui qui te convient.