Architecture (MMVC)
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.
The 30-second mental model
Section titled “The 30-second mental model”The five citizens
Section titled “The five citizens”1. Controllers (application/controller/)
Section titled “1. Controllers (application/controller/)”A controller is a class extending Nibiru\Adapter\Controller. The dispatcher invokes a fixed sequence on every request:
navigationAction()— populate menus / breadcrumb data.<_action>Action()— only if?_action=foois set.pageAction()— final render-time data assignment.
After all three return, Display::display() hands the assigned variables to Smarty.
2. Views (application/view/templates/)
Section titled “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/)
Section titled “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>/)
Section titled “4. Modules (application/module/<name>/)”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”| 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?
Section titled “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:
// 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
Section titled “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.
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
Dbadapter 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/SplObserverare 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
Section titled “Where to go next”- Bootstrap & Dispatcher — the lifecycle in code.
- Routing — URL → controller mapping rules.
- Modules — build your first one.