---
title: "Módulos"
description: "Construyendo módulos de Nibiru — el segundo M en MMVC. Traits, plugins, interfaces, configuraciones, observadores."
---

Un **módulo** es una unidad de dominio autónoma. Proporciona su propia configuración, sus plugins (servicios), sus tratos (métodos reutilizables), sus interfaces y, opcionalmente, una porción MVC propia. Los módulos son cómo Nibiru evita el problema del *controlador gordo* sin un contenedor de servicios.

<figure>
  <img src="/img/modules-anatomy.png" alt="Anatomía botánica de un módulo Nibiru, dibujado como las pétalos en capas de una lotus — pétalos externos, pétalos medios, pétalos internos, capa de sepalo, receptáculo." />
  <figcaption>Un módulo, capa por capa.</figcaption>
</figure>

## Anatomía
```
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
```
El `Registro` recorre `application/module/` al iniciar, descubre cada archivo `settings/*.ini` de los módulos, analiza la sección que coincide con el nombre del módulo (en mayúsculas) y lo almacena en caché para su consulta mediante `Registry::getInstance()->loadModuleConfigByName('users')`.

## Un módulo mínimo
```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);
    }
}
```
La interfaz `IModule` está intencionalmente marcadora — el comportamiento real se encuentra en tus tratos y complementos.

## Plugins: servicios sin estado

Un **plugin** es una clase que los controladores pueden instanciar para acceder a la funcionalidad del módulo:
```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';
    }
}
```
En un controlador:
```php
$this->user = new \Nibiru\Module\Users\Plugin\User();
if (!$this->user->isAuthorized()) {
    View::forwardTo('/login');
}
```
Los plugins heredan de la clase módulo, por lo que comparten acceso al registro, configuraciones y mecanismo de observadores.

## Características: métodos reutilizables

Un **trait** lleva cuerpos de métodos reutilizables que la clase del módulo quiere. Patrón común: fábricas de formularios.
```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 clase principal `Users` la incluye mediante `use Traits\Users;`.

## Configuración del módulo (INI)

Cada módulo puede llevar sus propios archivos INI. El Registro analiza cada archivo `*.ini` en `configuraciones/` y busca una sección con el nombre del módulo (en mayúsculas):
```ini
; application/module/users/settings/users.ini
[USERS]
session.lifetime    = 7200
password.min.length = 12
allowed.roles[]     = "admin"
allowed.roles[]     = "editor"
allowed.roles[]     = "standard"
```
Léelo desde cualquier lugar:
```php
$cfg = \Nibiru\Registry::getInstance()->loadModuleConfigByName('users');
$cfg->session_lifetime;     // 7200
$cfg->password_min_length;  // 12
$cfg->allowed_roles;        // [admin, editor, standard]
```
Capas de entorno: un archivo llamado `users.production.ini` se prefiere sobre `users.ini` cuando `APPLICATION_ENV=production`.

## El patrón observador

Módulos que implementan `SplSubject` pueden difundir eventos a observadores adjuntos sin acoplarse a ellos. Desde el ejemplo, el módulo `analytics` notifica a cualquier rastreador adjunto en cada vista de página:
```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()
```
Cada observador recibe la instancia de análisis en su método `update($subject)` y extrae los datos del evento que le interesan.

## Módulos de producción reales

Desde las aplicaciones de demostración:

- **`auth`** (TPMS) — gestión de sesión, inicio de sesión con código QR, acceso basado en roles. Implementa `SplSubject` para que los eventos de inicio y cierre de sesión se propaguen a los módulos de registro y auditoría.
- **`cms`** (prod.maschinen-stockert.de) — almacén de contenido claveado por *ruta del controlador* + idioma. Permite que no desarrolladores actualicen el texto del sitio.
- **`graph_mail`** (TPMS) — envoltorio de la API Microsoft Graph para correos electrónicos transaccionales.
- **`pdfgenerator`** (prod.maschinen-stockert.de) — genera catálogos de máquinas a partir de plantillas impulsadas por la base de datos.
- **`machineryscout`** — gestión del índice de Elasticsearch con rasgos para indexación, consulta y re-indexación.
- **`assetmanager`** — canal central CSS/JS, utilizado para intercambiar temas según el idioma.
- **`analytics`** — rastreador impulsado por observador (Matomo, etc.).

## Registro del módulo: cómo el marco encuentra su módulo

Un folder de módulo en el disco es necesario pero no suficiente. El framework también tiene que saber **cómo cargarlo** — eso se hace con tres arreglos posicionales en tu configuración `[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
```
Los nombres son **nombres de carpetas en minúsculas**, exactamente como aparecen bajo `application/module/`. El cargador automático del framework `Auto::loader()` (llamado desde el [Dispatcher](/en/core/dispatcher/)) recorre cada módulo en orden, requiriendo sus archivos.

### Convención de espacio de nombres del plugin

Las clases de plugin residen bajo el espacio de nombres **plural** `Plugins`:
```php
// application/module/billing/plugins/invoice.php
namespace Nibiru\Module\Billing\Plugins;     // ← plural
class Invoice extends \Nibiru\Module\Billing\Billing { /* ... */ }
```
Desacuerda el espacio de nombres y obtendrás fallos en la carga automática. La plantilla de la CLI (`./nibiru -m billing`) genera el espacio de nombres correcto para ti.

### Descubrimiento de configuraciones

No **registras** los archivos INI de tu módulo — el [Registro](/es/core/registry/) los descubre automáticamente recorriendo `application/module/<name>/settings/*.ini` después de que `[AUTOLOADER]` carga la clase del módulo. Cada sección `[<MODULE>]` (en mayúsculas) de un INI se vuelve disponible como:
```php
$cfg = \Nibiru\Registry::getInstance()->loadModuleConfigByName('billing');
$cfg->invoice_prefix;            // [BILLING] invoice.prefix → property
```
El Registro prefiere `<módulo>.<entorno>.ini` (por ejemplo, `facturación.producción.ini`) cuando `APPLICATION_ENV` coincide.

### Migraciones

Si tu módulo tiene tablas de base de datos, coloca los archivos SQL en `application/settings/config/database/` numerados después del rango existente. Convención utilizada por el módulo AI:
```
200-ai_rag_collection.sql
201-ai_rag_chunk.sql
202-ai_conversation.sql
203-ai_message.sql
```
Ejecute con `./nibiru -mi local`. El marco genera automáticamente modelos en `application/model/<table>.php` cuando se establece `[GENERATOR] database = true`, listos para usar a través de `\Nibiru\Pdo::fetchAll(…)`.

## Generando un módulo
```bash
./nibiru -m billing
```
Esqueletos:
```
application/module/billing/
├── billing.php
├── interfaces/billing.php
├── plugins/
├── settings/billing.ini
└── traits/
```
Añade el interruptor `-g` (`./nibiru -m billing -g`) cuando quieras que los ganchos de registro de Graylog estén preconfigurados en la estructura básica.

## Cuando *no* hacer un módulo

No promuevas cada controlador en un módulo. El umbral es: *¿tienes al menos una traza, un plugin y una clave INI?* Si sí, merece su propia carpeta de módulo. Si no, un archivo compartido de trazas o un pequeño ayudante `application/lib/` es suficiente.
