---
title: Architecture (MMVC)
description: How modules, controllers, views, models, the registry and the dispatcher orbit each other.
---

Nibiru is **MMVC**: Model — View — Controller — and a second **M** for **Module**. The first three are familiar; the second M is what gives Nibiru its flavour.

<figure>
  <img src="/img/architecture-lotus.png" alt="Cross-section illustration of the Nibiru runtime drawn as a lotus, with each petal labeled as a system component: Router, Controller, View, Model, Module, Registry." />
  <figcaption>The Nibiru runtime, drawn as a lotus.</figcaption>
</figure>

## The 30-second mental model

<figure class="lifecycle">
  <img src="/img/lifecycle.svg" alt="Request lifecycle: Browser to index.php to framework.php to Dispatcher, fanning out to Router, Modules, Autoloader, then to applicationController (navigationAction, custom action, pageAction), then to Smarty render." />
  <figcaption>Every request follows this path. The dispatcher fans out to Router / Modules / Autoloader, then invokes the controller's actions in order, then hands the assigned data to Smarty.</figcaption>
</figure>

## The five citizens

### 1. Controllers (`application/controller/`)

A controller is a class extending `Nibiru\Adapter\Controller`. The dispatcher invokes a fixed sequence on every request:

1. `navigationAction()` — populate menus / breadcrumb data.
2. `<_action>Action()` — only if `?_action=foo` is set.
3. `pageAction()` — final render-time data assignment.

After all three return, `Display::display()` hands the assigned variables to Smarty.

### 2. Views (`application/view/templates/`)

Smarty `.tpl` files. The dispatcher resolves `<controller>.tpl` automatically; nested actions go under `templates/<controller>/<action>.tpl`. Every variable passed via `View::assign(['x' => ...])` is available as `{$x}`.

### 3. Models (`application/model/`)

Models are **auto-generated** from your DB schema by `Model::__construct(false)` — one PHP class per table. They extend `Nibiru\Adapter\<Driver>\Db` and expose CRUD helpers. You can hand-edit a generated model and disable the regenerator with `[GENERATOR] database = false`.

### 4. Modules (`application/module/<name>/`)

A module bundles **its own** traits, plugins, interfaces and settings. The [Registry](/core/registry/) auto-discovers each module's `settings/*.ini` and exposes the parsed config via `Registry::getInstance()->loadModuleConfigByName('users')`. Modules can implement `SplSubject` for the **observer pattern**, letting other parts of the system attach observers and react to state changes.

### 5. Singletons that hold the cosmos together

| Singleton | Job |
|---|---|
| `Config::getInstance()` | Reads `settings.<env>.ini` and merges in module configs. |
| `Router::getInstance()` | Parses the URL into controller/action/params; recognises SEO URL forms. |
| `Registry::getInstance()` | Module discovery + config caching. |
| `Dispatcher::getInstance()` | The conductor. `dispatch::run()` is your application's heartbeat. |
| `View::getInstance()` | Wraps Smarty. `View::assign()` is the global template-variable inbox. |

## Why MMVC, not MVC?

Plain MVC works until your controllers start sharing logic — auth checks, form factories, third-party API clients. The usual answers are *services + DI container*, but that's a lot of ceremony for a rapid-prototyping framework.

Nibiru's answer: **modules**. A module *owns* a domain (`users`, `cms`, `analytics`, `tpms-quotes`), exposing its services via plugins controllers can instantiate directly:

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

The module owns its config, its DB tables, its templates, its forms — and is removable as a unit.

## The observer pattern, in practice

Some modules implement `SplSubject` so other code can react to events without coupling. From the showcase, the `analytics` module on `prod.maschinen-stockert.de` does exactly this: any controller can `attach()` a tracker (Matomo plugin) and the analytics module `notify()`s on every page-view, without the controller knowing what trackers exist.

```php
class Analytics implements \SplSubject {
    private \SplObjectStorage $observers;
    public function __construct() { $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); }
    }
}
```

## What's deliberately not in the framework

- **No DI container.** Singletons + plugins do the job.
- **No ORM.** Models are generated from the schema; queries use the `Db` adapter or raw SQL via the active driver.
- **No template inheritance through Twig/Blade tricks.** Smarty `{include}` is the unit of composition.
- **No event bus.** `SplSubject`/`SplObserver` are first-class.
- **No first-class background jobs.** The CLI is your scheduler — drive it from cron or systemd timers.

Less to learn, more to ship.

## Where to go next

- [Bootstrap & Dispatcher](/core/dispatcher/) — the lifecycle in code.
- [Routing](/core/routing/) — URL → controller mapping rules.
- [Modules](/core/modules/) — build your first one.
