Le WordPress d'aujourd'hui, décodé pour les développeurs

Extensions

Découper une extension en modules chargés à la demande avec des interfaces PHP

interface Module_Facturation { public function est_actif() : bool; } — cette seule ligne peut suffire à ne plus charger que ce qu'un site utilise réellement.

Par WordPress Développement • 30 septembre 2026 • 7 min de lecture • Aucun commentaire
Découper une extension en modules chargés à la demande avec des interfaces PHP

interface Module_Facturation { public function est_actif() : bool; public function initialiser() : void; } — cette déclaration, aussi modeste soit-elle, change complètement la façon dont une extension volumineuse peut décider, à chaque requête, quels blocs de code charger réellement en mémoire plutôt que de tout instancier par précaution.

Ce billet ne traite pas le découpage d’une extension en plusieurs extensions séparées, publiées et activées indépendamment. Il porte sur l’architecture interne d’une seule et même extension devenue volumineuse au fil des ans, dont toutes les fonctionnalités ne sont pas utilisées sur tous les sites qui l’installent.

Le problème : tout charger, même ce qui ne sert jamais

Une extension qui a grandi par ajouts successifs — module de facturation, module d’export, module de notifications, module de rapports — finit souvent par tout charger au démarrage, via un unique fichier principal qui inclut chaque classe sans condition. Un site qui n’utilise que le module de notifications paie pourtant le coût mémoire et le temps d’autoload de l’ensemble, y compris des modules jamais sollicités.

Définir un contrat commun via une interface

La première étape consiste à définir une interface partagée par tous les modules, indépendamment de leur fonctionnalité propre. Cette interface ne décrit pas ce que fait le module, mais comment l’extension principale doit interagir avec lui : peut-il être activé sur ce site, et comment s’initialise-t-il si c’est le cas.

interface Module_Interface {
    public function est_actif() : bool;
    public function initialiser() : void;
}

final class Module_Facturation implements Module_Interface {
    public function est_actif() : bool {
        return class_exists( 'WooCommerce' );
    }

    public function initialiser() : void {
        require_once __DIR__ . '/facturation/class-gestionnaire-factures.php';
        add_action( 'woocommerce_order_status_completed', [ new Gestionnaire_Factures(), 'generer' ] );
    }
}

Un registre central qui n’instancie que ce qui est actif

L'essentiel à retenir : Une interface commune permet d'interroger un module sans le charger entièrement ; Le chargement conditionnel réduit la mémoire utilisée à chaque requête ; Ce découpage reste interne, contrairement à plusieurs extensions séparées

Le fichier principal de l’extension ne connaît plus le détail de chaque module : il se contente de parcourir un registre de classes candidates, d’interroger leur méthode est_actif(), et de n’appeler initialiser() que pour celles qui répondent positivement. Le fichier PHP du module lui-même n’est chargé — via require_once — qu’à l’intérieur de cette méthode initialiser(), jamais avant.

Voici un registre minimal. Il associe chaque classe de module au fichier qui la déclare, charge ce petit fichier, instancie la classe, puis interroge est_actif(). Le code lourd du module, lui, reste sur disque tant que initialiser() n’est pas appelé.

final class Registre_Modules {
    /** @var array<string, string> classe => fichier */
    private array $candidats = [];

    public function ajouter( string $classe, string $fichier ) : void {
        $this->candidats[ $classe ] = $fichier;
    }

    public function demarrer() : void {
        foreach ( $this->candidats as $classe => $fichier ) {
            require_once $fichier;

            $module = new $classe();

            if ( ! $module instanceof Module_Interface ) {
                continue;
            }

            if ( $module->est_actif() ) {
                $module->initialiser();
            }
        }
    }
}

$registre = new Registre_Modules();
$registre->ajouter( 'Module_Facturation', __DIR__ . '/modules/class-module-facturation.php' );
$registre->ajouter( 'Module_Notifications', __DIR__ . '/modules/class-module-notifications.php' );

add_action( 'plugins_loaded', [ $registre, 'demarrer' ] );

Le point essentiel est le crochet choisi. Au moment où plugins_loaded se déclenche, tous les fichiers principaux des extensions actives ont déjà été inclus. Un test comme class_exists( 'WooCommerce' ) y est donc fiable, alors qu’il ne l’est pas au moment où le fichier de votre extension est lu : selon l’ordre de chargement, l’autre extension peut ne pas encore être présente.

Décider de l’activation par un réglage

Tous les modules ne dépendent pas de la présence d’une autre extension. Certains se justifient par un choix de l’administrateur du site. La méthode est_actif() peut alors lire une option enregistrée dans la base, ce qui laisse à l’écran de réglages le soin d’activer ou de désactiver chaque fonctionnalité.

final class Module_Notifications implements Module_Interface {
    public function est_actif() : bool {
        $actifs = get_option( 'mon_extension_modules_actifs', [] );

        return is_array( $actifs ) && in_array( 'notifications', $actifs, true );
    }

    public function initialiser() : void {
        require_once __DIR__ . '/notifications/class-expediteur.php';
        ( new Expediteur() )->enregistrer_crochets();
    }
}

Le troisième argument de in_array() impose une comparaison stricte, et le test is_array() protège contre une option corrompue ou modifiée à la main. Une méthode est_actif() doit rester rapide et sans effet de bord : elle est appelée à chaque requête, y compris pour les modules qui ne seront finalement pas chargés.

Les pièges de ce découpage

  • Un fichier de module trop gros : si le fichier qui déclare la classe contient déjà toute la logique, l’économie disparaît. Gardez-y le strict minimum et déplacez le reste dans des fichiers chargés par initialiser().
  • Les dépendances entre modules : si le module de rapports lit des données produites par le module de facturation, il doit vérifier explicitement sa présence, sous peine d’erreurs silencieuses quand l’un est actif et pas l’autre.
  • Les contextes d’exécution : un module réservé à l’administration peut renvoyer is_admin() dans est_actif() pour ne rien charger sur le site public. Attention toutefois : cette fonction est aussi vraie pour les requêtes AJAX de l’administration.
  • Le chargement automatique : si vous utilisez un chargeur de classes comme celui de Composer, vérifiez qu’il ne charge pas d’office l’ensemble des fichiers via une entrée files dans composer.json, ce qui annulerait le gain recherché.

Un cas concret : mesurer le gain

Sans mesure, on ne sait pas si le découpage a servi. Une comparaison simple consiste à relever la mémoire de pointe en fin de requête avant et après le découpage, sur un site n’utilisant qu’un seul module. La fonction memory_get_peak_usage() de PHP suffit, appelée sur le crochet shutdown et consignée dans le journal d’erreurs pendant la phase de test.

add_action( 'shutdown', function () {
    if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
        error_log( sprintf(
            'Mémoire de pointe : %.2f Mo',
            memory_get_peak_usage( true ) / 1048576
        ) );
    }
} );

Le résultat dépend fortement du poids des modules écartés : sur une extension dont les modules pèsent quelques dizaines de kilo-octets de code chacun, le gain reste modeste ; il devient sensible lorsque des modules embarquent de grosses bibliothèques ou déclarent de nombreux crochets. Ne vous engagez dans ce découpage que si la mesure le justifie.

L’interface apporte aussi un bénéfice indépendant de la mémoire : les tests. Un module factice qui implémente Module_Interface peut être ajouté au registre dans un test unitaire pour vérifier que initialiser() n’est appelé que lorsque est_actif() renvoie vrai, sans charger WordPress entier.

Un module qui ne se charge pas ne coûte rien, à condition que le registre n’ait pas déjà tout chargé pour savoir s’il devait le faire.

Conclusion

Une interface de deux méthodes, un registre de quelques lignes et un chargement différé dans initialiser() suffisent à transformer une extension monolithique en ensemble de modules qui ne se chargent que lorsqu’ils servent. Ce découpage reste interne à l’extension : une seule installation, un seul cycle de mise à jour, mais une empreinte mémoire adaptée à chaque site. Commencez par le module le plus lourd et le moins utilisé, mesurez, puis étendez la démarche aux autres. Et gardez à l’esprit que la limite de cette approche n’est pas technique mais organisationnelle : si des modules doivent être vendus, versionnés ou activés séparément, ce sont bien des extensions distinctes qu’il vous faut.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Partager :

À propos de l'auteur

WordPress Développement

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

Voir tous ses articles

Dans la même veine

À lire aussi