Essayez sans attendre l'hébergement proposé par WordPress
-15% sur le premier mois avec le code 2025PRESS15AFF

Essayer maintenant

Créer un plugin WordPress de A à Z avec une architecture orientée objet : principes SOLID appliqués

Développer un plugin WordPress « à l’arrache », on l’a tous fait au moins une fois — et on a tous regretté de devoir maintenir ce code six mois plus tard. Les principes SOLID, c’est exactement la réponse à ce problème : une façon de structurer son code pour qu’il reste lisible, évolutif et testable, même quand le projet grossit. Dans cet article, on va construire ensemble un plugin de A à Z en appliquant ces principes concrètement, avec du vrai code PHP et une architecture orientée objet pensée pour WordPress.

Les principes SOLID appliqués au développement WordPress

SOLID, c’est un acronyme qui regroupe cinq principes de conception orientée objet. Sur le papier, ça peut sembler très académique. Mais en pratique, ces principes répondent à des problèmes très concrets qu’on rencontre dès qu’un plugin grossit un peu. On va les passer en revue un par un, en gardant toujours WordPress en tête.

S — Single Responsibility : une classe, une responsabilité

Une classe = une seule raison de changer. C’est la règle de base. Et c’est souvent la première qu’on viole sans s’en rendre compte.

Dans un plugin WordPress, on est vite tenté de tout mettre dans une grosse classe Plugin : les hooks, les requêtes en base, l’affichage admin, les assets… Résultat : un fichier de 800 lignes impossible à déboguer. Le principe S nous dit non.

Concrètement, ça donne quelque chose comme ça :

  • Plugin_Admin : gère uniquement les pages d’administration et les menus
  • Plugin_Assets : enregistre et charge les scripts/styles
  • Plugin_Database : s’occupe des requêtes vers la base de données
  • Plugin_Hooks : enregistre les actions et filtres WordPress

Une classe Plugin_Admin n’a donc rien à faire avec $wpdb. Si demain vous changez la structure de votre table, vous n’avez pas à toucher à la partie admin. C’est exactement l’objectif : limiter l’impact des modifications.

O — Open/Closed : ouvert à l’extension, fermé à la modification

Un composant doit pouvoir être étendu sans être modifié. Dit comme ça, c’est un peu paradoxal. Mais WordPress l’illustre parfaitement, et depuis le début.

Les add_action() et add_filter() sont en eux-mêmes une implémentation du principe Open/Closed. WordPress ne vous demande jamais de modifier son cœur pour y ajouter du comportement : vous accrochez votre code à des points d’extension prévus à cet effet. C’est exactement ça, le principe O appliqué à grande échelle.

Pour votre plugin, ça veut dire concevoir vos propres points d’extension. Plutôt que de tout hardcoder dans une méthode, exposez des filtres :

$price = apply_filters( 'myplugin_product_price', $price, $product_id );

Ainsi, un autre développeur (ou vous-même dans six mois) peut modifier ce comportement sans jamais toucher au code source du plugin. C’est une bonne pratique fondamentale, et elle paie à long terme.

L — Liskov Substitution, I — Interface Segregation et D — Dependency Inversion

Ces trois principes sont un peu plus abstraits, mais ils deviennent très naturels dès qu’on travaille avec des interfaces PHP.

Liskov Substitution stipule qu’une classe enfant doit pouvoir remplacer sa classe parente sans casser le programme. En PHP, on le traduit souvent via les interfaces. Par exemple, définissez une interface Registrable :

interface Registrable {
    public function register(): void;
}

Toutes vos classes de fonctionnalités l’implémentent. Votre loader ne sait pas ce qu’il charge — il sait juste que chaque objet est Registrable. On peut donc substituer n’importe quelle implémentation sans toucher au reste.

Interface Segregation, c’est le principe qui dit qu’une interface ne devrait pas forcer à implémenter des méthodes inutiles. Par contre, une grosse interface Plugin_Contract avec vingt méthodes, c’est le problème typique. Préférez des interfaces granulaires : Registrable, Renderable, Bootable… chaque classe implémente ce dont elle a besoin, rien de plus.

Dependency Inversion, enfin : dépendez des abstractions, pas des implémentations concrètes. Donc, plutôt que d’instancier vos dépendances dans le constructeur, injectez-les :

class Plugin_Admin {
    public function __construct( private readonly Plugin_Database $db ) {}
}

(On peut aller plus loin avec un container d’injection de dépendances, mais pour un plugin de taille moyenne, l’injection manuelle suffit largement.) Ainsi, vos classes restent testables et découplées — ce qui est, au fond, tout l’intérêt de la démarche.

Construire l’architecture du plugin pas à pas

Bon, on rentre maintenant dans le vif du sujet. Les principes SOLID, c’est bien beau en théorie — mais comment ça se traduit concrètement dans un plugin WordPress ? C’est ce qu’on va voir ensemble, étape par étape, en construisant un plugin fictif My_Pro_Plugin qui gère un custom post type. Ce plugin va nous servir de fil rouge tout au long de cette section.

Définir la structure des fichiers et des dossiers

La première chose à faire avant d’écrire une seule ligne de PHP, c’est de poser une arborescence claire. Une structure bien pensée, c’est la base d’un projet maintenable sur le long terme.

Voici l’organisation que j’utilise pour My_Pro_Plugin :

my-pro-plugin/
├── my-pro-plugin.php       # Fichier principal (bootstrap)
├── composer.json           # Autoloading PSR-4 + dépendances
├── src/
│   ├── Plugin.php          # Classe principale — orchestre tout
│   ├── Container.php       # Service container maison
│   ├── Contracts/
│   │   └── Registrable.php # Interface commune à tous les modules
│   ├── Admin/
│   │   └── Admin_Page.php  # Page d'administration
│   └── Frontend/
│       └── Post_Type.php   # Enregistrement du custom post type
└── vendor/                 # Autoload Composer (ne pas toucher !)

Chaque dossier a une responsabilité claire : Contracts/ pour les interfaces, Admin/ pour tout ce qui touche au back-office, Frontend/ pour le reste. C’est le principe S (Single Responsibility) appliqué à l’organisation des fichiers.

Pour l’autoloading, on configure Composer avec PSR-4 dans le composer.json :

{
    "autoload": {
        "psr-4": {
            "MyProPlugin\\": "src/"
        }
    }
}

Ensuite, un simple composer install (ou dump-autoload) et toutes vos classes sont disponibles sans un seul require manuel. Un vrai gain de temps !

La classe principale du plugin : le point d’entrée

La classe Plugin est le chef d’orchestre. Elle ne fait rien d’autre que d’initialiser le container, d’enregistrer les services et de déclencher leur configuration. C’est le pattern Bootstrap : court, lisible, et surtout sans logique métier.

Voici le fichier principal my-pro-plugin.php :

<?php
/**
 * Plugin Name: My Pro Plugin
 * Description: Un plugin WordPress avec une architecture OOP et les principes SOLID.
 * Version:     1.0.0
 * Author:      Etienne
 */

defined( 'ABSPATH' ) || exit;

require_once __DIR__ . '/vendor/autoload.php';

use MyProPlugin\Plugin;

( new Plugin() )->boot();

Et maintenant, la classe Plugin elle-même :

<?php

namespace MyProPlugin;

use MyProPlugin\Admin\Admin_Page;
use MyProPlugin\Frontend\Post_Type;
use MyProPlugin\Contracts\Registrable;

/**
 * Classe principale du plugin.
 * Orchestre l'initialisation des modules via le Service Container.
 */
class Plugin {

    /**
     * @var Container
     */
    private Container $container;

    /**
     * Liste des services à enregistrer.
     * Pour ajouter un module, il suffit de l'ajouter ici.
     *
     * @var array<class-string<Registrable>>
     */
    private array $services = [
        Admin_Page::class,
        Post_Type::class,
    ];

    /**
     * Point d'entrée du plugin.
     */
    public function boot(): void {
        $this->container = new Container();

        foreach ( $this->services as $service_class ) {
            /** @var Registrable $service */
            $service = $this->container->make( $service_class );
            $service->register();
        }
    }
}

Vous le voyez : la classe Plugin ne sait pas ce que font Admin_Page ou Post_Type. Elle sait juste qu’ils implémentent Registrable et qu’ils ont une méthode register(). C’est exactement le principe de l’inversion de dépendances (D) en action.

Gérer les dépendances avec un Service Container maison

Le Service Container (ou conteneur d’injection de dépendances), c’est un peu le « carnet d’adresses » de votre plugin. Il sait comment instancier chaque classe et peut résoudre leurs dépendances automatiquement.

Le concept derrière tout ça, c’est l’Inversion of Control (IoC) : ce n’est plus votre classe qui crée ses propres dépendances avec new MaDependance(), c’est le container qui s’en charge. Résultat : vos classes sont découplées et beaucoup plus faciles à tester.

Pour My_Pro_Plugin, pas besoin d’un container ultra-complexe. Voici une implémentation minimaliste qui fait le job :

<?php

namespace MyProPlugin;

use ReflectionClass;

/**
 * Service Container minimaliste avec résolution automatique des dépendances.
 */
class Container {

    /**
     * @var array<string, callable>
     */
    private array $bindings = [];

    /**
     * Lie une abstraction à une implémentation concrète.
     */
    public function bind( string $abstract, callable $factory ): void {
        $this->bindings[ $abstract ] = $factory;
    }

    /**
     * Résout et instancie une classe avec ses dépendances.
     *
     * @throws \ReflectionException
     */
    public function make( string $class ): object {
        // Si une liaison manuelle existe, on l'utilise
        if ( isset( $this->bindings[ $class ] ) ) {
            return ( $this->bindings[ $class ] )( $this );
        }

        // Sinon, on résout automatiquement via Reflection
        $reflector    = new ReflectionClass( $class );
        $constructor  = $reflector->getConstructor();

        if ( null === $constructor ) {
            return new $class();
        }

        $dependencies = array_map(
            fn( $param ) => $this->make( $param->getType()->getName() ),
            $constructor->getParameters()
        );

        return $reflector->newInstanceArgs( $dependencies );
    }
}

Ce container fait environ 40 lignes, mais il est capable de résoudre automatiquement les dépendances d’une classe grâce à la Reflection API de PHP. Pas mal pour un container « maison » !

Attention cependant : cette implémentation est volontairement simplifiée. Elle ne gère pas les interfaces, les unions de types ou les paramètres scalaires. Pour des projets plus ambitieux, orientez-vous vers des solutions éprouvées comme PHP-DI ou le container de Laravel (utilisable en standalone). Mais pour un plugin WordPress de taille raisonnable, ce mini-container est largement suffisant.

Enregistrer les hooks via une interface Registrable

C’est ici que le principe O (Open/Closed) prend tout son sens. L’interface Registrable est le contrat que tous vos modules doivent respecter : ils exposent une méthode register() qui sera appelée par la classe Plugin. Et c’est tout.

<?php

namespace MyProPlugin\Contracts;

/**
 * Contrat pour tous les modules du plugin.
 * Chaque module doit implémenter cette interface.
 */
interface Registrable {
    /**
     * Enregistre les hooks WordPress du module.
     */
    public function register(): void;
}

Simple, non ? Une seule méthode. Et voici deux implémentations concrètes pour notre plugin.

D’abord, Post_Type qui enregistre le custom post type :

<?php

namespace MyProPlugin\Frontend;

use MyProPlugin\Contracts\Registrable;

/**
 * Module chargé d'enregistrer le custom post type "Projet".
 */
class Post_Type implements Registrable {

    public function register(): void {
        add_action( 'init', [ $this, 'register_post_type' ] );
    }

    public function register_post_type(): void {
        register_post_type( 'mpp_project', [
            'label'  => __( 'Projets', 'my-pro-plugin' ),
            'public' => true,
            'supports' => [ 'title', 'editor', 'thumbnail' ],
        ] );
    }
}

Ensuite, Admin_Page pour la page de réglages :

<?php

namespace MyProPlugin\Admin;

use MyProPlugin\Contracts\Registrable;

/**
 * Module chargé d'ajouter la page d'administration du plugin.
 */
class Admin_Page implements Registrable {

    public function register(): void {
        add_action( 'admin_menu', [ $this, 'add_menu_page' ] );
    }

    public function add_menu_page(): void {
        add_menu_page(
            __( 'My Pro Plugin', 'my-pro-plugin' ),
            __( 'My Pro Plugin', 'my-pro-plugin' ),
            'manage_options',
            'my-pro-plugin',
            [ $this, 'render' ]
        );
    }

    public function render(): void {
        echo '<div class="wrap"><h1>My Pro Plugin — Réglages</h1></div>';
    }
}

Vous voyez l’avantage ? Si demain vous devez ajouter un module de shortcodes, vous créez une classe Shortcodes qui implémente Registrable, et vous l’ajoutez dans le tableau $services de la classe Plugin. Vous ne touchez à rien d’autre. C’est exactement ce que promet le principe Open/Closed : ouvert à l’extension, fermé à la modification.

Bonnes pratiques et pièges à éviter pour un plugin maintenable

Bon, on a vu les principes SOLID, on a construit notre arborescence, mis en place un Service Container… C’est bien beau tout ça sur le papier. Mais dans la vraie vie, il y a des pièges classiques dans lesquels je suis tombé (et que je vois encore régulièrement dans des plugins en production). Voici un tour d’horizon des erreurs à éviter et des réflexes à adopter.

Évitez les classes God — et je dis ça par expérience. Une classe Plugin qui gère les hooks, les requêtes en base de données, l’interface d’administration ET le rendu frontend, c’est le cauchemar à maintenir. C’est exactement ce que le principe S (Single Responsibility) cherche à éviter. Si votre classe dépasse les 200-300 lignes, c’est souvent le signe qu’elle en fait trop. Découpez sans pitié.

Nommez correctement vos classes et vos fichiers. Deux approches coexistent dans l’écosystème WordPress : l’ancienne convention avec un préfixe unique (MPP_Admin, MPP_Logger…) qui évite les conflits, et l’approche moderne avec les namespaces PSR-4 (MyProPlugin\Admin\AdminController). Les namespaces sont clairement préférables dès qu’on utilise un autoloader : ils isolent votre code de celui des autres plugins, sans avoir à préfixer chaque classe à la main. Plus propre, plus lisible.

Ne pas abuser de l’héritage. C’est un réflexe naturel quand on découvre l’OOP : on hérite partout. Mais « composition over inheritance », c’est une règle d’or. Concrètement : plutôt que de faire hériter votre classe AdminController d’une classe Logger, injectez un objet Logger dans le constructeur. Votre classe reste indépendante, testable, et vous pouvez swapper l’implémentation du logger sans toucher au reste. C’est exactement l’esprit du principe D qu’on a vu en section 1.

Testez votre code — même un peu. Je ne vais pas rentrer dans le détail ici (ça mériterait un article entier), mais sachez que l’architecture OOP qu’on vient de construire est pensée pour être testable. Avec WP_Mock et PHPUnit, on peut mocker les fonctions WordPress et tester chaque classe de façon isolée. L’injection de dépendances via notre Service Container rend ça vraiment accessible. Un test, c’est aussi une documentation vivante de ce que doit faire votre code.

La sécurité reste non négociable. Peu importe la beauté de votre architecture, les bases ne changent pas : vérifiez les nonce pour chaque action formulaire, contrôlez les capabilities avant d’exécuter une action sensible, et ne faites jamais confiance à une donnée entrante sans sanitization/validation. sanitize_text_field(), current_user_can(), wp_verify_nonce()… Ces fonctions doivent devenir des réflexes, pas des options.


Et voilà — vous avez maintenant toutes les cartes en main pour développer un plugin WordPress sérieux, maintenable et évolutif. Mon conseil ? Ne repartez pas de zéro sur votre prochain projet. Prenez un plugin existant que vous maintenez (on en a tous un qui traîne…) et refactorisez-le progressivement : une classe à la fois, un principe SOLID à la fois. C’est souvent plus formateur que de construire quelque chose de neuf sur une base déjà propre.

Et si le sujet des tests unitaires vous intéresse, j’ai prévu un prochain article sur WP_Mock et PHPUnit appliqués à un plugin WordPress — avec des exemples concrets. Restez connecté !