# 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.

- Auteur : WordPress Développement
- Publié le : 2026-05-08
- Mis à jour le : 2026-09-30
- Catégorie : Extensions
- URL : https://www.wpmoderne.fr/extensions/decouper-extension-modules-interfaces-php/

## L’essentiel

- 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

`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.
