Skip to content
Nibiru docsv0.9.2

Architecture (MMVC)

How modules, controllers, views, models, the registry and the dispatcher orbit each other.

Stable Reading time ~ 3 min Edit on GitHub

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.

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.
The Nibiru runtime, drawn as a lotus.
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.
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.

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.

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

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.

A module bundles its own traits, plugins, interfaces and settings. The 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

Section titled “5. Singletons that hold the cosmos together”
SingletonJob
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.

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:

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

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.

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

Section titled “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.