---
title: Modules
description: Building Nibiru modules — the second M in MMVC. Traits, plugins, interfaces, settings, observers.
---

A **module** is a self-contained domain unit. It owns its config, its plugins (services), its traits (reusable methods), its interfaces, and optionally an MVC slice of its own. Modules are how Nibiru avoids the *fat-controller* problem without a service container.

<figure>
  <img src="/img/modules-anatomy.png" alt="Botanical anatomy of a Nibiru module, drawn as the layered petals of a lotus — outer petals, middle petals, inner petals, sepal layer, receptacle." />
  <figcaption>A module, layer by layer.</figcaption>
</figure>

## Anatomy

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

The `Registry` walks `application/module/` at boot, discovers each module's `settings/*.ini`, parses the section matching the module name (uppercased), and caches it for lookup via `Registry::getInstance()->loadModuleConfigByName('users')`.

## A minimal module

```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);
    }
}
```

The `IModule` interface is intentionally a marker — actual behaviour is all in your traits and plugins.

## Plugins: stateless services

A **plugin** is a class controllers can instantiate to access module functionality:

```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';
    }
}
```

In a controller:

```php
$this->user = new \Nibiru\Module\Users\Plugin\User();
if (!$this->user->isAuthorized()) {
    View::forwardTo('/login');
}
```

Plugins inherit from the module class, so they share access to the registry, settings, and observer machinery.

## Traits: reusable methods

A **trait** carries reusable method bodies the module class wants. Common pattern: form factories.

```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',
        ]);
    }
}
```

The main `Users` class brings it in via `use Traits\Users;`.

## Module settings (INI)

Each module can carry its own INI files. The Registry parses every `*.ini` in `settings/` and looks for a section named after the module (uppercased):

```ini
; application/module/users/settings/users.ini
[USERS]
session.lifetime    = 7200
password.min.length = 12
allowed.roles[]     = "admin"
allowed.roles[]     = "editor"
allowed.roles[]     = "standard"
```

Read it back from anywhere:

```php
$cfg = \Nibiru\Registry::getInstance()->loadModuleConfigByName('users');
$cfg->session_lifetime;     // 7200
$cfg->password_min_length;  // 12
$cfg->allowed_roles;        // [admin, editor, standard]
```

Environment overlays: a file named `users.production.ini` is preferred over `users.ini` when `APPLICATION_ENV=production`.

## The observer pattern

Modules implementing `SplSubject` can broadcast events to attached observers without coupling to them. From the showcase, the `analytics` module notifies any attached tracker on every page-view:

```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()
```

Each observer's `update($subject)` receives the analytics instance and pulls the event data it cares about.

## Real production modules

From the showcase apps:

- **`auth`** (TPMS) — session management, QR-code login, role-based access. Implements `SplSubject` so login/logout events can fan out to logging and audit modules.
- **`cms`** (prod.maschinen-stockert.de) — content store keyed by *controller path* + language. Lets non-developers update site copy.
- **`graph_mail`** (TPMS) — Microsoft Graph API wrapper for transactional email.
- **`pdfgenerator`** (prod.maschinen-stockert.de) — generates machine catalogs from DB-driven templates.
- **`machineryscout`** — Elasticsearch index management with traits for indexing, querying, and re-indexing.
- **`assetmanager`** — central CSS/JS pipeline, used to swap themes per language.
- **`analytics`** — observer-driven tracker fan-out (Matomo, etc.).

## Module registration: how the framework finds your module

A module folder on disk is necessary but not sufficient. The framework also has to know **to load it** — that's done with three position arrays in your `[AUTOLOADER]` config:

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

The names are **lowercase folder names**, exactly as they appear under `application/module/`. The framework's `Auto::loader()` (called from the [Dispatcher](/en/core/dispatcher/)) walks each module in order, requiring its files.

### Plugin namespace convention

Plugin classes live under the **plural** namespace `Plugins`:

```php
// application/module/billing/plugins/invoice.php
namespace Nibiru\Module\Billing\Plugins;     // ← plural
class Invoice extends \Nibiru\Module\Billing\Billing { /* ... */ }
```

Mismatch the namespace and you'll get autoloader misses. The CLI scaffold (`./nibiru -m billing`) generates the correct namespace for you.

### Settings discovery

You **don't** register your module's INI files — the [Registry](/en/core/registry/) discovers them automatically by walking `application/module/<name>/settings/*.ini` after `[AUTOLOADER]` loads the module class. Each INI's `[<MODULE>]` (uppercased) section becomes available as:

```php
$cfg = \Nibiru\Registry::getInstance()->loadModuleConfigByName('billing');
$cfg->invoice_prefix;            // [BILLING] invoice.prefix → property
```

The Registry prefers `<module>.<env>.ini` (e.g. `billing.production.ini`) when `APPLICATION_ENV` matches.

### Migrations

If your module has database tables, drop SQL files in `application/settings/config/database/` numbered after the existing range. Convention used by the AI module:

```
200-ai_rag_collection.sql
201-ai_rag_chunk.sql
202-ai_conversation.sql
203-ai_message.sql
```

Run with `./nibiru -mi local`. The framework auto-generates models in `application/model/<table>.php` when `[GENERATOR] database = true` is set, ready to use via `\Nibiru\Pdo::fetchAll(…)`.

## Generating a module

```bash
./nibiru -m billing
```

Scaffolds:

```
application/module/billing/
├── billing.php
├── interfaces/billing.php
├── plugins/
├── settings/billing.ini
└── traits/
```

Add the `-g` switch (`./nibiru -m billing -g`) when you want Graylog logging hooks pre-wired into the scaffold.

## When *not* to make a module

Don't promote every controller into a module. The threshold is: *do you have at least one trait, one plugin, and one INI key?* If yes, it earns its own module folder. If not, a shared trait file or a small `application/lib/` helper is plenty.
