Skip to content
Nibiru docsv0.9.2

Controllers

Writing Nibiru controllers — the action lifecycle, View::assign, and patterns from production code.

Stable Reading time ~ 3 min Edit on GitHub

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.

<?php
namespace 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 []; }
}

When the dispatcher invokes a controller, it calls methods in this fixed order:

  1. navigationAction() — populate global menus, breadcrumbs, role-aware nav.
  2. <verb>Action() — only if ?_action=<verb> is set or the URL has a second segment that names an action.
  3. 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.

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 array

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

To redirect inside an action:

View::forwardTo('/login'); / 302 to the URL, exits
View::forwardToJsonHeader(); / sets Content-Type: application/json

forwardToJsonHeader() is the canonical pattern for JSON endpoints — set the header, assign data, return. The view layer does the rest.

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

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.

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.

The CLI scaffolds a controller and its template in one shot:

Terminal window
./nibiru -c products

application/controller/productsController.phpapplication/view/templates/products.tpl

Both are populated with the canonical skeleton. You’re ready to write.