---
title: Patterns from Production
description: Concrete patterns used across Nibiru apps in production — copy-paste-ready.
---

Seven patterns that show up repeatedly across Nibiru production apps. Each is small, copy-paste-ready, and rooted in a real codebase.

## 1. Thin controller → module-plugin delegation

Keep controllers tiny. Push logic into module plugins.

```php
// thin
class erpController extends Controller {
    public function syncAction(): void {
        View::forwardToJsonHeader();
        $result = \Nibiru\Module\Erp\Plugin\Sync::run();
        View::assign(['data' => $result]);
    }
}
```

```php
// fat
class Sync extends Erp {
    public static function run(): array {
        $svc = AlphaplanSyncService::getInstance();
        try {
            return ['success' => true, 'changes' => $svc->syncAbDocuments()];
        } catch (\Throwable $e) {
            return ['success' => false, 'error' => $e->getMessage()];
        }
    }
}
```

The controller is reviewable in 5 seconds; the plugin is unit-testable.

## 2. CMS as content source

Decouple text from layout. Editors update copy via the CMS module's UI; templates stay developer-owned.

```php
public function pageAction() {
    $path = $this->getController() . '/' . $this->getRequest('_action', 'page');
    foreach (Cms::init($this->getController())
                  ->loadCmsTemplateTextsByControllerPath($path, $this->language)
             as $t)
    {
        View::assign([
            $t['cms_template_texts_text_identifier']
                => $t['cms_template_texts_text_content']
        ]);
    }
}
```

Templates reference the identifiers as if they were ordinary variables:

```smarty
<h1>{$hero_title}</h1>
<p>{$hero_intro}</p>
```

## 3. Observer-driven analytics

Multiple trackers without controller coupling.

```php
$analytics = new Analytics();
$analytics->attach(new Plugin\Matomo());
$analytics->attach(new Plugin\Plausible());
$analytics->trackPageView();   // calls notify() internally
```

Each observer's `update($subject)` pulls only the fields it cares about. Adding a tracker is a one-line change.

## 4. Multi-navigation composition

Build pages with several named nav arrays instead of one monolithic structure.

```php
public function navigationAction() {
    foreach (['head', 'main', 'social', 'footer'] as $name) {
        JsonNavigation::getInstance()->loadJsonNavigationArray($name);
    }
}
```

```smarty
<header>{include file="navigation.tpl" array=$head}</header>
<aside>{include file="navigation.tpl" array=$main}</aside>
<footer>{include file="navigation.tpl" array=$footer}</footer>
```

Each JSON file is small, scoped, easy to edit, and conflict-free in PRs.

## 5. JSON endpoints with `forwardToJsonHeader`

Standard contract for AJAX:

```php
public function searchAction() {
    View::forwardToJsonHeader();
    $q = trim($_REQUEST['q'] ?? '');
    if (strlen($q) < 2) {
        View::assign(['data' => ['results' => []]]);
        return;
    }
    View::assign(['data' => [
        'results' => MachineryScout::index()->search($q),
    ]]);
}
```

Headers are set automatically; no manual `header('Content-Type: application/json')`.

## 6. Multi-stage workflow via actions

State machines mapped onto controller actions:

```php
class quotesController extends Controller {
    public function pageAction()    { /* list view */ }
    public function detailAction()  { /* one quote */ }
    public function acceptAction()  { /* state transition: open → accepted */ }
    public function rejectAction()  { /* state transition: open → rejected */ }
    public function archiveAction() { /* state transition: any → archived */ }
}
```

`/quotes/accept/42` runs `acceptAction()` with `$_REQUEST['id'] = 42`. Each transition is a tiny action; persistence and notification go through a `QuotesService` plugin.

## 7. Schema-first models with one custom method per intent

Generate the model from schema, then add intent-named methods that wrap your queries:

```php
class users extends Db
{
    const TABLE = ['table' => 'users', 'field' => [/* … */]];
    public function __construct() { self::initTable(self::TABLE); }

    public function findByLogin(string $login): ?array {
        return Pdo::fetchRow('SELECT * FROM users WHERE user_login = :l',
                              [':l' => $login]) ?: null;
    }

    public function activeStandardUsers(): array {
        return Pdo::fetchAll(
            'SELECT * FROM users WHERE user_account_active = 1 AND user_role = :r',
            [':r' => 'standard']
        );
    }
}
```

Future-you reading the call site sees `findByLogin($login)` — the intent — not raw SQL.

## Anti-patterns to avoid

- **Static-buffer leakage in forms.** Always `Form::create()` before building.
- **Logic in `navigationAction()`.** It runs on every request, including JSON endpoints.
- **Mass `View::assign()`** without a structured array. Use `View::assign(['…'])` once.
- **Custom routes for what the SEO URL already does.** `/products/<slug>/<id>` is free.
- **Editing generated models.** They get overwritten. Custom methods → child class or `database.overwrite = false`.
