---
title: "Modules"
description: "Construire des modules Nibiru — le deuxième M dans MMVC. Caractéristiques, plug-ins, interfaces, paramètres, observateurs."
---

Un **module** est une unité de domaine autonome. Il possède sa propre configuration, ses plugins (services), ses traits (méthodes réutilisables), ses interfaces et éventuellement une tranche MVC de son propre. Les modules sont la façon dont Nibiru évite le problème du *fat-controller* sans un conteneur de services.

<figure>
  <img src="/img/modules-anatomy.png" alt="Anatomie botanique d'un module Nibiru, dessiné comme les pétales en couches d'un lotus — pétales extérieurs, pétales moyens, pétales intérieurs, couche de sépales, réceptacle." />
  <figcaption>Un module, par couche.</figcaption>
</figure>

## Anatomie
```
application/module/<name>/
├── <name>.php          # main class (implements IModule, optionally SplSubject)
├── interfaces/         # contracts for plugins / external consumers
│   └── <name>.php
├── plugins/            # stateless services usable from controllers
│   ├── <thing>.php
│   └── <other>.php
├── settings/           # auto-discovered .ini files
│   ├── <name>.ini
│   └── <name>.production.ini
└── traits/             # reusable method groups
    └── <name>.php
```
Le `Registry` parcourt `application/module/` au démarrage, découvre chaque fichier `settings/*.ini` du module, analyse la section correspondant au nom du module (en majuscules) et le met en cache pour une recherche via `Registry::getInstance()->loadModuleConfigByName('users')`.

## Un module minimal
```php
<?php
namespace Nibiru\Module\Users;

use Nibiru\Module as ModuleAdapter;
use Nibiru\Interfaces\IModule;
use Nibiru\Registry;

class Users extends ModuleAdapter implements IModule, \SplSubject
{
    use Traits\Users;

    const CONFIG_MODULE_NAME = 'users';

    protected static \stdClass $usersRegistry;
    protected \SplObjectStorage $observers;

    public function __construct() {
        $this->setUsersRegistry();
        $this->observers = new \SplObjectStorage();
    }

    public function attach(\SplObserver $o): void { $this->observers->attach($o); }
    public function detach(\SplObserver $o): void { $this->observers->detach($o); }
    public function notify(): void {
        foreach ($this->observers as $o) { $o->update($this); }
    }

    protected function setUsersRegistry(): void {
        self::$usersRegistry = Registry::getInstance()
            ->loadModuleConfigByName(self::CONFIG_MODULE_NAME);
    }
}
```
L'interface `IModule` est délibérément un marqueur — le comportement réel se trouve dans vos traits et vos plugins.

## Plugins : services sans état

Un **plugin** est une classe que les contrôleurs peuvent instancier pour accéder aux fonctionnalités du module :
```php
<?php
// application/module/users/plugins/user.php
namespace Nibiru\Module\Users\Plugin;

use Nibiru\Module\Users\Users;
use Nibiru\Pdo;

class User extends Users
{
    public function isAuthorized(): bool {
        return isset($_SESSION['auth']['user_id']);
    }

    public function checkForStandardUser(): bool {
        return $this->isAuthorized()
            && ($_SESSION['auth']['role'] ?? '') === 'standard';
    }
}
```
Dans un contrôleur :
```php
$this->user = new \Nibiru\Module\Users\Plugin\User();
if (!$this->user->isAuthorized()) {
    View::forwardTo('/login');
}
```
Les plugins héritent de la classe du module, donc ils partagent accès au registre, aux paramètres et à la machine observateur.

## Caractéristiques : méthodes réutilisables

Un **trait** contient des corps de méthodes réutilisables que la classe du module souhaite. Modèle courant : usines de formulaires.
```php
<?php
// application/module/users/traits/users.php
namespace Nibiru\Module\Users\Traits;

use Nibiru\Form;

trait Users
{
    public function loginForm(): string {
        Form::create();
        Form::addOpenDiv(['class' => 'form-group']);
        Form::addInputTypeText([
            'class' => 'form-control', 'name' => 'login',
            'placeholder' => 'Username',
        ]);
        Form::addCloseDiv();
        Form::addOpenDiv(['class' => 'form-group']);
        Form::addInputTypePassword([
            'class' => 'form-control', 'name' => 'password',
            'placeholder' => 'Password',
        ]);
        Form::addCloseDiv();
        return Form::addForm([
            'method' => 'POST',
            'action' => '/login',
            'name'   => 'loginForm',
        ]);
    }
}
```
La classe principale `Users` l'intègre via `use Traits\Users;`.

## Paramètres du module (INI)

Chaque module peut contenir ses propres fichiers INI. Le Registre analyse chaque fichier `*.ini` dans le dossier `settings/` et recherche une section nommée d'après le module (en majuscules) :
```ini
; application/module/users/settings/users.ini
[USERS]
session.lifetime    = 7200
password.min.length = 12
allowed.roles[]     = "admin"
allowed.roles[]     = "editor"
allowed.roles[]     = "standard"
```
Lisez-le à partir d'ailleurs :
```php
$cfg = \Nibiru\Registry::getInstance()->loadModuleConfigByName('users');
$cfg->session_lifetime;     // 7200
$cfg->password_min_length;  // 12
$cfg->allowed_roles;        // [admin, editor, standard]
```
Superpositions d'environnement : un fichier nommé `users.production.ini` est préféré par rapport à `users.ini` lorsque `APPLICATION_ENV=production`.

## Le modèle observateur

Les modules implémentant `SplSubject` peuvent diffuser des événements aux observateurs attachés sans être couplés à eux. À partir de l'exemple, le module `analytics` informe tout suivi attaché à chaque affichage de page :
```php
// in a controller
$analytics = new \Nibiru\Module\Analytics\Analytics();
$analytics->attach(new \Nibiru\Module\Analytics\Plugin\Matomo());
$analytics->attach(new \Nibiru\Module\Analytics\Plugin\Plausible());

$analytics->trackPageView();   // internally calls notify()
```
Chaque observateur reçoit l'instance d'analytics via sa méthode `update($subject)` et extrait les données d'événement dont il a besoin.

## Modules de production réels

Depuis les applications de démonstration :

- **`auth`** (TPMS) — gestion des sessions, connexion par code QR, accès basé sur les rôles. Implémente `SplSubject` afin que les événements de connexion/déconnexion puissent être diffusés aux modules d'enregistrement et de conformité.
- **`cms`** (prod.maschinen-stockert.de) — magasin de contenu cléé par *chemin du contrôleur* + langue. Permet aux non-développeurs de mettre à jour le contenu du site.
- **`graph_mail`** (TPMS) — enveloppe de l'API Microsoft Graph pour les e-mails transactionnels.
- **`pdfgenerator`** (prod.maschinen-stockert.de) — génère des catalogues de machines à partir de modèles pilotés par la base de données.
- **`machineryscout`** — gestion de l'index Elasticsearch avec des traits pour l'indexation, la requête et le re-indexage.
- **`assetmanager`** — pipeline CSS/JS centralisé, utilisé pour basculer les thèmes selon la langue.
- **`analytics`** — suivi basé sur les observateurs diffusant (Matomo, etc.).

## Enregistrement du module : comment le framework trouve votre module

Un dossier de module sur le disque est nécessaire mais non suffisant. Le framework doit également savoir **le charger** — c'est fait avec trois tableaux de positions dans votre configuration `[AUTOLOADER]` :
```ini
; application/settings/config/settings.development.ini

[AUTOLOADER]
iface.pos[]        = "users"      ; load application/module/users/interfaces/
iface.pos[]        = "billing"    ; load application/module/billing/interfaces/
trait.pos[]        = "users"      ; load application/module/users/traits/
trait.pos[]        = "billing"
class.pos[]        = "users"      ; load application/module/users/users.php
class.pos[]        = "billing"
class.plugin.pos[] = ""           ; reserved
```
Les noms sont des **noms de dossiers en minuscules**, exactement tels qu'ils apparaissent sous `application/module/`. Le chargeur automatique du framework (`Auto::loader()`) (appelé par le [Dispatcher](/en/core/dispatcher/)) parcourt chaque module dans l'ordre, en chargeant ses fichiers.

### Convention d'espace de noms pour les plugins

Les classes de plugin se trouvent dans l'espace de noms **pluriel** `Plugins`:
```php
// application/module/billing/plugins/invoice.php
namespace Nibiru\Module\Billing\Plugins;     // ← plural
class Invoice extends \Nibiru\Module\Billing\Billing { /* ... */ }
```
Ne correspondez pas à l'espace de noms et vous obtiendrez des manquements d'autoload. Le squelette CLI (`./nibiru -m billing`) génère l'espace de noms correct pour vous.

### Découverte des paramètres

Vous **ne** vous inscrivez pas les fichiers INI de votre module — le [Registry](/en/core/registry/) les découvre automatiquement en parcourant `application/module/<name>/settings/*.ini` après que `[AUTOLOADER]` a chargé la classe du module. Chaque section `[<MODULE>]` (en majuscules) d'un INI devient disponible comme :
```php
$cfg = \Nibiru\Registry::getInstance()->loadModuleConfigByName('billing');
$cfg->invoice_prefix;            // [BILLING] invoice.prefix → property
```
Le Registre préfère `<module>.<env>.ini` (par exemple `billing.production.ini`) lorsque `APPLICATION_ENV` correspond.

### Migrations

Si votre module a des tables de base de données, déposez les fichiers SQL dans `application/settings/config/database/`, numérotés après la plage existante. La convention utilisée par le module IA :
```
200-ai_rag_collection.sql
201-ai_rag_chunk.sql
202-ai_conversation.sql
203-ai_message.sql
```
Exécutez avec `./nibiru -mi local`. Le framework génère automatiquement des modèles dans `application/model/<table>.php` lorsque `[GENERATOR] database = true` est défini, prêts à l'utilisation via `\Nibiru\Pdo::fetchAll(…)`.

## Génération d'un module
```bash
./nibiru -m billing
```
Échafaudages :
```
application/module/billing/
├── billing.php
├── interfaces/billing.php
├── plugins/
├── settings/billing.ini
└── traits/
```
Ajoutez l'option `-g` (`./nibiru -m billing -g`) lorsque vous souhaitez que les accroches de journalisation Graylog soient pré-connectées dans le squelette.

## Quand ne *pas* créer un module

Ne promouvez pas chaque contrôleur en un module. Le seuil est : *avez-vous au moins un trait, une extension et une clé INI ?* Si oui, il mérite son propre dossier de module. Sinon, un fichier de traits partagés ou un petit assistant dans `application/lib/` suffit amplement.
