Controllers
Writing Nibiru controllers — the action lifecycle, View::assign, and patterns from production code.
A Nibiru controller is a class that extends Nibiru\Adapter\Controller, lives in application/controller/<name>Controller.php, and is loaded automatically by the Dispatcher when a URL like /<name>/... arrives.
Anatomy of a controller
Section titled “Anatomy of a controller”<?phpnamespace Nibiru;use Nibiru\Adapter\Controller;
class productsController extends Controller{ public function pageAction() { View::assign([ 'title' => 'Products', 'products' => $this->loadProducts(), ]); }
public function navigationAction() { JsonNavigation::getInstance()->loadJsonNavigationArray(); }
public function detailAction() { $id = (int) ($_REQUEST['id'] ?? 0); View::assign(['product' => $this->loadProduct($id)]); }
private function loadProducts(): array { /* ... */ return []; } private function loadProduct(int $id): array { /* ... */ return []; }}The action lifecycle
Section titled “The action lifecycle”When the dispatcher invokes a controller, it calls methods in this fixed order:
navigationAction()— populate global menus, breadcrumbs, role-aware nav.<verb>Action()— only if?_action=<verb>is set or the URL has a second segment that names an action.pageAction()— last call before render.
Both navigationAction() and pageAction() are always called, even for unknown actions. This is convenient (you never need to check) but can surprise you if you assume actions are exclusive.
Talking to the view
Section titled “Talking to the view”View::assign(['key' => $value, ...]) is how data reaches templates. It’s static and can be called as many times as you want — later calls overwrite earlier ones.
View::assign(['title' => 'Products']);View::assign(['products' => $list]);
// In templates/products.tpl:// {$title} → "Products"// {$products} → the arrayConvenience helpers from the base controller:
$this->getRequest('id', false); / $_REQUEST['id'] ?? false$this->getPost('email', ''); / $_POST['email'] ?? ''$this->getGet('page', 1); / $_GET['page'] ?? 1$this->getServer('REQUEST_URI'); / $_SERVER['REQUEST_URI']$this->getFiles('upload'); / $_FILES['upload']$this->getSession('auth'); / $_SESSION['auth']These exist because Controller is final-friendly: you can mock them in tests by substituting a child class.
Forwarding
Section titled “Forwarding”To redirect inside an action:
View::forwardTo('/login'); / 302 to the URL, exitsView::forwardToJsonHeader(); / sets Content-Type: application/jsonforwardToJsonHeader() is the canonical pattern for JSON endpoints — set the header, assign data, return. The view layer does the rest.
Multiple actions per controller
Section titled “Multiple actions per controller”Nibiru is happy to host any number of actions per controller. The TPMS erpController from production has pageAction, navigationAction, plus syncAction, statusAction, dryRunAction, cancelAction, etc. — each invoked via ?_action=sync or /erp/sync.
// /erp/sync → $_REQUEST['_action'] = 'sync'public function syncAction(): void{ View::forwardToJsonHeader(); if ($_SERVER['REQUEST_METHOD'] !== 'POST') { View::assign(['data' => ['success' => false, 'error' => 'POST method required']]); return; } $result = AlphaplanSyncService::getInstance()->syncAbDocuments(); View::assign(['data' => $result]);}Working with modules
Section titled “Working with modules”Controllers are thin. Most logic should live in modules and their plugins:
namespace Nibiru;use Nibiru\Adapter\Controller;use Nibiru\Module\Users\Plugin\User;
class loginController extends Controller{ private User $user;
public function __construct() { parent::__construct(); $this->user = new User(); }
public function authAction() { if (!$this->user->isAuthorized()) { View::assign(['loginForm' => $this->user->loginForm()]); } else { View::forwardTo('/index'); } }}This delegation pattern is consistent across the showcase apps: controllers orchestrate, modules do the work.
Multi-language / CMS-driven content
Section titled “Multi-language / CMS-driven content”A pattern from prod.maschinen-stockert.de — load all on-page text from a CMS table keyed by controller path:
public function pageAction() { $controllerPath = $this->getController() . '/' . $this->getRequest('_action', 'page');
$texts = Cms::init($this->getController()) ->loadCmsTemplateTextsByControllerPath($controllerPath, $this->language);
foreach ($texts as $t) { View::assign([ $t['cms_template_texts_text_identifier'] => $t['cms_template_texts_text_content'] ]); }}Result: non-developers can change copy without touching code. The CMS module owns the table and the editor UI; the controller just loads strings.
Generating controllers
Section titled “Generating controllers”The CLI scaffolds a controller and its template in one shot:
./nibiru -c products→ application/controller/productsController.php
→ application/view/templates/products.tpl
Both are populated with the canonical skeleton. You’re ready to write.