vendredi 25 septembre 2026

À propos

Contact

Extensions

Singleton ou Service Container : quelle architecture OOP pour une extension

Trois façons d'organiser les classes d'une extension WordPress, avec leurs avantages réels et leurs limites, loin des dogmes venus tels quels des frameworks généralistes.

Par Clément Hadrot • 25 octobre 2022 • 5 min de lecture • Aucun commentaire
Singleton ou Service Container : quelle architecture OOP pour une extension

Chaque développeur venu du monde des frameworks généralistes (Symfony, Laravel) et découvrant WordPress se pose tôt ou tard la même question : comment organiser proprement les classes d’une extension un peu ambitieuse ? Le Singleton, longtemps la norme de facto dans l’écosystème WordPress, cède progressivement du terrain face à des approches plus proches de l’injection de dépendances. Aucune des deux n’est universellement supérieure : le contexte de l’extension doit guider le choix.

Cet article compare trois architectures concrètes, avec leurs compromis réels plutôt qu’une préférence dogmatique importée d’un autre écosystème.

Le Singleton : simple, mais pas sans coût

namespace Acme\MonExtension;

class Plugin {
    private static ?Plugin $instance = null;

    public static function instance(): Plugin {
        if ( null === self::$instance ) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    private function __construct() {
        // initialisation
    }

    private function __clone() {}
}

add_action( 'plugins_loaded', [ Plugin::class, 'instance' ] );

L’intérêt du Singleton dans un contexte WordPress est réel : une extension n’a, par définition, besoin que d’une seule instance de son point d’entrée principal, et l’accès global via Plugin::instance() évite de faire circuler l’objet dans tout le code. Le revers de la médaille apparaît dès qu’on écrit des tests unitaires : l’état statique partagé entre les tests rend l’isolation difficile, et un test qui modifie une propriété de l’instance peut affecter le test suivant si le Singleton n’est pas explicitement réinitialisé entre chaque cas.

Le service container léger, un entre-deux pragmatique

Plutôt qu’un Singleton unique qui centralise tout, un conteneur de services léger permet d’enregistrer et de résoudre des dépendances sans imposer la complexité d’un framework DI complet avec autowiring par réflexion.

namespace Acme\MonExtension;

class Container {
    private array $services = [];
    private array $instances = [];

    public function set( string $id, callable $factory ): void {
        $this->services[ $id ] = $factory;
    }

    public function get( string $id ) {
        if ( ! isset( $this->instances[ $id ] ) ) {
            $this->instances[ $id ] = ( $this->services[ $id ] )( $this );
        }
        return $this->instances[ $id ];
    }
}

$container = new Container();

$container->set( 'logger', function () {
    return new Logger();
} );

$container->set( 'stock_service', function ( Container $c ) {
    return new StockService( $c->get( 'logger' ) );
} );

Cette approche conserve la testabilité : dans un test unitaire, il suffit d’enregistrer un faux logger (un mock) sous l’identifiant logger avant de résoudre stock_service, sans avoir à modifier le code métier. C’est un gain concret par rapport au Singleton pour toute extension qui prévoit une suite de tests sérieuse.

L'essentiel à retenir : Le Singleton simplifie mais complique les tests unitaires ; Un service container léger reste préférable à un framework DI complet ; WordPress lui-même impose des contraintes que Symfony n'a pas

Les hooks WordPress, une contrainte que les frameworks n’ont pas

Une différence structurelle sépare WordPress des frameworks MVC classiques : le système de hooks fonctionne par callbacks enregistrés globalement, pas par injection dans un contrôleur appelé une seule fois par requête. Cela signifie que l’instanciation d’une classe et son enregistrement auprès des hooks WordPress doivent se faire tôt, généralement sur plugins_loaded ou init, quelle que soit l’architecture choisie.

add_action( 'plugins_loaded', function () use ( $container ) {
    $container->get( 'stock_service' )->enregistrer_hooks();
} );

class StockService {
    public function __construct( private Logger $logger ) {}

    public function enregistrer_hooks(): void {
        add_action( 'save_post_produit', [ $this, 'verifier_stock' ], 10, 1 );
    }

    public function verifier_stock( int $post_id ): void {
        $this->logger->info( "Vérification du stock pour le produit {$post_id}" );
    }
}

Ce détail explique pourquoi l’autowiring complet par réflexion, courant dans Symfony, apporte peu de valeur ajoutée en contexte WordPress : le nombre de classes à instancier au démarrage reste généralement limité, et le coût de performance d’une réflexion PHP à chaque requête (sans cache de compilation dédié, contrairement à un conteneur Symfony compilé) n’est pas toujours justifié pour une extension de taille moyenne.

Comparatif des trois approches

ApprocheSimplicitéTestabilitéAdapté à
Singleton simpleÉlevéeFaible sans précautionPetite extension, peu de dépendances
Service container maisonMoyenneBonneExtension de taille moyenne à grande
Framework DI complet (autowiring)Faible (courbe d’apprentissage)Très bonneSuite d’extensions partageant une base commune

Sur nos projets, le seuil de bascule vers un service container maison se situe généralement autour de la dizaine de classes de service actives : en dessous, le Singleton reste largement suffisant et plus rapide à écrire ; au-delà, la testabilité gagnée compense largement la légère complexité additionnelle.

Un compromis à éviter : le Singleton qui devient un fourre-tout

Le vrai risque architectural n’est pas le choix initial entre ces approches, mais l’évolution non maîtrisée d’un Singleton simple vers une classe qui accumule, au fil des versions, la responsabilité de dix domaines fonctionnels différents. Ce symptôme, souvent appelé classe « Dieu », rend le code difficile à faire évoluer indépendamment par domaine. Un signal d’alerte fiable : si la classe Plugin dépasse quelques centaines de lignes ou plus de cinq responsabilités distinctes, c’est le moment de déléguer vers des classes de service dédiées, avec ou sans conteneur.

En résumé

Aucune architecture n’est universellement supérieure en contexte WordPress. Le Singleton reste un choix raisonnable pour une extension modeste ; un service container léger apporte une testabilité réelle dès que la complexité augmente ; un framework DI complet ne se justifie que pour une suite d’extensions partageant une base de code commune conséquente. Le vrai signal à surveiller reste la croissance non maîtrisée des responsabilités d’une classe unique, quelle que soit l’architecture retenue au départ.

Partager :

À propos de l'auteur

Clément Hadrot

Développeur WordPress, passionné par Elementor, le FSE et l’automatisation par IA.

Voir tous ses articles

Dans la même veine

À lire aussi