Files
nibiru-framework.com/docs/scripts/extraction/framework-reference-v2.md
stephan f8f58c4ab9 Sweep: link fixes, splash i18n parity, neuronetz badge, mobile, training source
LINK AUDIT — every internal link now resolves to a real page:
  - BrandHeader doc-nav 'Showcase' target → /{locale}/showcase/projects/
    (the showcase/ directory has no index, only patterns/projects)
  - Translated-slug bugs across de/es/fr/ja: ~30 broken links from the
    overnight translator that auto-localised path segments. Mapped back to
    the actual English slugs:
      de:  warum-nibiru → why-nibiru, kuenstliche-intelligenz/orakel → ai/oracle,
           kI/modul/* → ai/module/*, praesentation/projekte → showcase/projects
      es:  ai/modulo/* → ai/module/*, diseno/* → design/*,
           porque-nibiru / por-que-nibiru → why-nibiru,
           presentacion/proyectos → showcase/projects
      fr:  ia/module/* → ai/module/*, ia/module/formation → ai/module/training,
           pourquoi-nibiru → why-nibiru, presentation/projets → showcase/projects
      ja:  ai/milestones → ai/roadmap
  - Cross-locale leaks: bare `/ai/oracle` (no locale) → `/{locale}/ai/oracle/`
    in de/index.mdx, fr/index.mdx, fr/ai/module/overview.md, en/ai/corpus.md
  - `/en/start/` (which 404s — start/ has no index) was hardcoded in five
    design/components.md atelier-button hrefs across all locales →
    `/{locale}/start/installation/`
  - `/en/reference` removed from en/downloads.mdx — the reference doc tree
    isn't built yet; replaced with a github link to the v2 markdown source
  - Collapsed stray double-slashes (`/de/why-nibiru//` etc.) introduced by
    the slug-replacement sed sweep

Final audit shows 0 broken internal links (down from ~37).

TRAINING SOURCE NOW SHIPS — root cause of "I cannot find the training data
in the repository": the curated source files were gitignored.
  - .gitignore at scripts/extraction/ now whitelists framework-reference-v2.md
    and lora-augmentation.summary.txt alongside lora-augmentation.jsonl
  - The 1620-line v2 reference, the 323-record augmentation jsonl, and the
    summary report all enter the repo so the production Docker build sees
    them and contributors can find them by browsing gitea

NEURONETZ AI DEEPLINK BADGE — small "AI by Neuronetz ↗" pill in the splash
footer's bottom strip. Logo mark mirrored locally to
public/img/external/neuronetz-mark.svg (pulled from neuronetz.ai/favicon.svg)
so the page doesn't hot-link off-domain on every paint. Magenta border on
hover; opens neuronetz.ai in a new tab with rel=noopener.

SPLASH I18N PARITY — de/es/fr/ja index.mdx now import + render the same
component stack as en (CometTrail · MmvcStage · MissionControl · LaunchSequence
· SpacecraftGrid · EditorialContent · LandingFooter · ToTop · LandingScripts),
so every locale shows the full splash structure. The component bodies
themselves are still English (proper i18n is the next step); for now this
brings structural parity.

MOBILE RESPONSIVE SWEEP:
  - LandingFooter: 4-col grid stacks 2-col @ 768px and 1-col @ 480px;
    bottom strip wraps vertical at 480px
  - MmvcStage: 5-step progress rail tightens its gaps under 720px and
    drops the bar segments entirely under 480px so the labels fit
  - Docs bridge §11: tighter H1/H2 spacing, breadcrumbs/doc-meta on
    narrow viewports, pagination cards stack 1-col, help-strip stacks
    vertical, tables get horizontal-scroll on overflow
  - Doc-header: nav-version chip hides under 480px so the search-pill
    + brand fit comfortably

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 19:36:25 +02:00

68 KiB
Raw Blame History

Nibiru PHP Framework: Complete Reference v2

Last Updated: May 2026
Version: 2.0 (Second-pass deep extraction)

0. Executive Summary: What Changed from v1

Version 1 was a high-level architecture summary: "MMVC pattern, controllers, adapters, singletons." Useless for training.

V2 is microscopic: Every public static factory, every magic namespace convention, every chained idiom a developer learns only by reading code. This is what "tiny things — that is what makes the entire framework" means.

New in V2

  • Exact file:line citations for every claim (no "presumably" or "appears to").
  • Real production usage examples from /loach.mssql.database-connection/, /develop.maschinen-stockert.de/, /nibiru-modules/.
  • The runtime database switcher: Pdo::$section static, how it's called, why "last call wins."
  • The autogenerator pipeline: CLI flag → INI section → driver → adapter → generated model's extends Db line → model's __construct() calling Pdo::settingsSection().
  • Form factory placeholder exhaustively: [CLASSNAME], [TABLE], [FOLDERNAME], [DBSECTION], [ADAPTER], [CONNECTOR].
  • Module lifecycle: registry scan, plugin/trait ordering from INI, environment-specific overrides.
  • INI key reference table: every [DATABASE*], [ENGINE], [AUTOLOADER] key the framework reads.
  • Production-only patterns: what exists in develop.maschinen-stockert.de but not framework core.

1. Namespace Map

Core Namespace Prefixes

Prefix Maps to Rule Example
\Nibiru\Factory\* core/f/*.php Static factories; call signature is ClassName::staticMethod() Db::loadModel('WarehouseLoach\\Timeanddate') returns IDb
\Nibiru\Model\* application/model/ (autogenerated) PSR-4: folder name = namespace part. Format: \Nibiru\Model\<FolderName>\<TableName> \Nibiru\Model\WarehouseLoach\Timeanddate is file /application/model/WarehouseLoach/timeanddate.php
\Nibiru\Adapter\* core/a/*.php or autogenerated Adapters for different DB drivers. MySQL\Db, Postgres\Db, Postgresql\Db Models extends Db where Db is \Nibiru\Adapter\MySQL\Db
\Nibiru\Adapter\IDb core/i/IDb.php Interface all models implement Methods: loadTableAsArray(), updateRowById(), insertArrayIntoTable()
\Nibiru\Module\* application/module/[NAME]/ Module classes; main class at [NAME]/[name].php \Nibiru\Module\Users\Users is file /application/module/users/users.php
\Nibiru\Module\[Name]\Interfaces\* application/module/[NAME]/interfaces/ Module-specific interfaces Loaded by Auto::loader()->loadModules() if listed in INI iface.pos[]
\Nibiru\Module\[Name]\Traits\* application/module/[NAME]/traits/ Module-specific traits Loaded if listed in INI trait.pos[]; execution order = INI array order
\Nibiru\Module\[Name]\Plugins\* application/module/[NAME]/plugins/ Module plugin classes; run on controller/model actions Loaded last in INI order; pattern is use Nibiru\Factory\Db; ... Db::loadModel(...)
\Nibiru (core) core/c/*.php Singletons: Config, Controller, View, Pdo, Router, Dispatcher, Engine All follow getInstance() pattern

Autoloader rule (PSR-4 variant):

  • Model folder (dbmodel in INI): \Nibiru\Model\<FolderName>\<TableName> maps to disk path from $basePath/<FolderName>/TableName.php
  • Module folder: \Nibiru\Module\<ModuleName>\ maps to disk path from application/module/<modulename>/
  • Interfaces/traits/plugins: scanned by Auto::loader()->loadModuleComponents() based on INI registry

2. Singletons & Factories

2.1 Config — Singleton

File: /home/stephan/PhpstormProjects/Nibiru/core/c/config.php:12

public static function getInstance(): Settings

What it does:

  • Reads environment (getenv('APPLICATION_ENV') or defines APPLICATION_ENV constant).
  • Calls parent::setConfig(self::getEnv()) which parses INI file for that environment.
  • Returns singleton holding the entire application config array.

INI file resolution:

  • Template: settings.ENV.ini
  • Actual: settings.development.ini, settings.production.ini, settings.cli.ini, settings.preproduction.ini (or custom)
  • Parsed with parse_ini_file(..., true) (associative array keyed by section name)

Usage:

Config::getInstance()->getConfig()['DATABASE']['username']
Config::getInstance()->getConfig()['ENGINE']['templates']
Config::getInstance()->getConfig()['AUTOLOADER']['class.pos']

Production example: /home/stephan/PhpstormProjects/develop.maschinen-stockert.de/application/settings/config/settings.development.ini:14-20


2.2 Pdo — Database Connection Switcher Singleton

File: /home/stephan/PhpstormProjects/Nibiru/core/c/pdo.php:12

Signature:

public static function settingsSection( $section = IOdbc::SETTINGS_DATABASE )
public static function getInstance( $section = false ): Mysql

The idiom (critical):

  1. Static section variable (Pdo::$section, line 14) — last write wins per request
  2. Every model's __construct() calls Pdo::settingsSection('[DBSECTION]') or [CONNECTOR]::settingsSection('[DBSECTION]')
  3. All subsequent Pdo queries use parent::getInstance(self::getSettingsSection())->getConn()

Why: Single static per request; can switch databases by instantiating a different model, or calling Pdo::settingsSection() directly.

Example from generated model (/home/stephan/PhpstormProjects/loach.mssql.database-connection/application/model/WarehouseLoach/timeanddate.php:30-34):

public function __construct()
{
    Pdo::settingsSection('DATABASE');
    self::initTable( self::TABLE );
}

Example from plugin (/home/stephan/PhpstormProjects/loach.mssql.database-connection/application/module/users/plugins/acl.php:14):

use Nibiru\Factory\Db;
// ... later in code:
$acl = Db::loadModel('WarehouseLoach\Acl');  // instantiates model, calls __construct(), sets Pdo::$section to 'DATABASE'

INI sections read by Mysql::__construct() at line 32:

if($section)
{
    $settings = Config::getInstance()->getConfig()[$section];
}
else
{
    $settings = Config::getInstance()->getConfig()[self::SETTINGS_DATABASE];  // defaults to [DATABASE]
}

Real INI sections (from /home/stephan/PhpstormProjects/loach.mssql.database-connection/application/settings/config/settings.production.ini):

[DATABASE]
is.active = true
username = "loach"
password = "..."
hostname = "sto-loach-production-mariadb-1"
basename = "warehouse_loach"
driver = "mysql"
port = "3306"
encoding = "UTF8"

Pattern: Each application can have [DATABASE], [DATABASE_LOACH], [DATABASE_EMMIDIA], etc., and switch between them via Pdo::settingsSection('DATABASE_LOACH').

Key methods (core/c/pdo.php):

  • query($string) — exec or fetch; routes via parent::getInstance(self::getSettingsSection())
  • queryString($string, $associative) — fetch all
  • fetchTableAsArray($tablename, $limit, $order) — table dump with pagination
  • selectDatasetByFieldAndValue($tablename, $fieldAndValue, $sortOrder) — WHERE clause
  • fetchRowInArrayById($tablename, $id) — single row by PK
  • fetchRowInArrayByWhere($tablename, $column_name, $parameter_name) — single row by column
  • insertArrayIntoTable($tablename, $array_name, $encrypted) — insert with optional DES_ENCRYPT()
  • updateRowById($tablename, $columnNames, $data, $id, $encrypted) — update by PK

All check self::getSettingsSection() before executing.


2.3 Db — Static Model Factory

File: /home/stephan/PhpstormProjects/Nibiru/core/f/db.php:11

Signature:

public static function loadModel( $modelName = ""): IDb

Pipeline:

  1. Call Db::loadModel('WarehouseLoach\\Timeanddate')
  2. Internally: _setModel("WarehouseLoach\\Timeanddate")
  3. Constructs namespace: $fmodel = "\\Nibiru\\Model\\".$model = \Nibiru\Model\WarehouseLoach\Timeanddate
  4. Instantiates: self::$_model = new $fmodel;
  5. Returns: self::getModel() which is the static $_model

Returns: IDb interface, guaranteeing these methods exist:

  • loadTableAsArray()
  • selectRowsetById($id)
  • updateRowById(array $rowData, int $id, string $encrypted = "")
  • insertArrayIntoTable($dataset = array())
  • selectDatasetByFieldWhere($fieldWhere = array(), $sortOrder = false)
  • selectRowByFieldWhere($field = array())
  • lastInsertId()
  • deleteRowById(int $id = 0)
  • updateRowByFieldWhere($wherefield, $wherevalue, $rowfield, $rowvalue)

Real usage (/home/stephan/PhpstormProjects/loach.mssql.database-connection/application/module/pdf/plugins/pdf.php):

use Nibiru\Factory\Db;
// ... later:
Db::loadModel('WarehouseLoach\Timeanddate')->insertArrayIntoTable([...]);
$timeanddate_id = Db::loadModel('WarehouseLoach\Timeanddate')->lastInsertId();

2.4 Form — Static Form Factory

File: /home/stephan/PhpstormProjects/Nibiru/core/f/form.php:41

Core flow:

  1. Form::create() — clears self::$form static buffer
  2. Call methods in sequence: Form::addInputTypeText(...), Form::addSelect(...), etc.
  3. Each appends HTML to self::$form; final call is Form::addForm($attributes) which wraps with <form> tags
  4. addForm() calls displaySelect() to replace OPTIONS placeholder with options added via addSelectOption()

Available input types (methods in Form factory):

Method Maps to Template Variable
addForm($attributes) <form> wrapper Uses {FIELDS} placeholder
addInputTypeText($attributes) <input type="text">
addInputTypeSubmit($attributes) <input type="submit">
addInputTypePassword($attributes) <input type="password">
addInputTypeEmail($attributes) <input type="email">
addInputTypeCheckbox($attributes) <input type="checkbox">
addInputTypeRadio($attributes) <input type="radio">
addInputTypeSwitch($attributes) HTML5 toggle
addInputTypeDate($attributes) <input type="date">
addInputTypeDatetime($attributes) <input type="datetime-local">
addInputTypeColor($attributes) <input type="color">
addInputTypeNumber($attributes) <input type="number">
addInputTypeRange($attributes) <input type="range">
addTypeSearch($attributes) <input type="search">
addTypeTelefon($attributes) <input type="tel">
addTypeUrl($attributes) <input type="url">
addTypeFileUpload($attributes) <input type="file">
addTypeHidden($attributes) <input type="hidden">
addTypeImageSubmit($attributes) <input type="image">
addTypeReset($attributes) <input type="reset">
addTypeButton($attributes) <button>
addTypeLabel($attributes) <label>
addSelect($attributes) <select> wrapper Uses {OPTIONS} placeholder
addSelectOption($attributes) <option> Accumulates in self::$option
addOpenDiv($attributes) <div>
addCloseDiv() </div>
addOpenAny($attributes) Generic <tag>
addCloseAny($attributes) Generic </tag>

Placeholder system for elements (no variable substitution in form factory itself; handled by type classes):

Each type class (e.g., TypeText, TypeEmail) has a loadElement($attributes) method that generates HTML. The $attributes array is passed through; type classes handle placeholder replacement.

Wrapper div option:

All add* methods accept optional second parameter $div:

Form::addInputTypeText(['NAME' => 'username', 'ID' => 'user'], ['class' => 'form-group']);

This wraps the element: <div class="form-group">ELEMENT</div>

Usage pattern:

Form::create();
Form::addInputTypeText(['NAME' => 'email', 'ID' => 'email_field']);
Form::addInputTypePassword(['NAME' => 'password']);
Form::addInputTypeSubmit(['VALUE' => 'Login']);
$html = Form::addForm(['METHOD' => 'POST', 'ACTION' => '/login']);
echo $html;

2.5 Controller — Singleton, extends View

File: /home/stephan/PhpstormProjects/Nibiru/core/c/controller.php:14

Signature:

public static function getInstance(): View|Controller

Key methods:

  • getRequest($param, bool $params = false)$_REQUEST (POST/GET unified)
  • getPost($param)$_POST
  • getGet($param)$_GET
  • getSession($param, bool $params = false, bool $checkForActiveSession = false)
  • getServer($param)$_SERVER
  • getFiles($param)$_FILES
  • getController() — current controller name (extracted from URL segment 1)

Lifecycle: Called first by Dispatcher::run() (line 48 of dispatcher.php). Then action method is called if exists.


2.6 View — Singleton, Smarty Wrapper

File: /home/stephan/PhpstormProjects/Nibiru/core/c/view.php:11

Signature:

public static function getInstance(): View
public static function assign( $varname = array() )
public static function forwardTo( $page )
public static function forwardToJsonHeader( string $encoding = "" )
public function display( $page )
public function getEngine(): \Smarty

Smarty setup (lines 66-79):

  • Template dir: core/c/../../{ENGINE[templates]}
  • Compile dir: core/c/../../{ENGINE[templates_c]}
  • Cache dir: core/c/../../{ENGINE[cache]}
  • Config dir: core/c/../../{ENGINE[config_dir]}
  • Debug template: {ENGINE[debug_template]}
  • Caching: if ENGINE[caching]==true, set to CACHING_LIFETIME_CURRENT

assign() pattern:

View::assign(['user_name' => 'John', 'role' => 'admin']);
// in Smarty template: {$user_name}, {$role}

display() logic:

  • Checks if $page ends with .tpl (defined by View::NIBIRU_FILE_END)
  • Calls Controller::getInstance()->action(Smarty, $page)
  • Smarty renders the template

3. Database Layer (Deep)

3.1 Drivers & Adapters

Nibiru supports multiple databases. Each has:

  • Interface (IPostgres, IPostgresql, IPdo, IOdbc)
  • Connection class (Postgres, Postgresql, Pdo, Odbc)
  • Adapter (generated or static)

Files:

  • core/i/IPdo.php — interface for MySQL/PDO
  • core/c/pdo.phpPdo extends Mysql
  • core/i/IMysql.php — base constants (table names, field names, etc.)
  • core/a/mysql.db.php\Nibiru\Adapter\MySQL\Db base class
  • core/c/postgres.php — Postgres ODBC driver
  • core/c/postgresql.php — Postgresql PDO driver

Driver selection: Determined by INI [GENERATOR][driver] or [DATABASE*][driver] field:

  • driver = "mysql" → uses Pdo, adapter is \Nibiru\Adapter\MySQL\Db
  • driver = "psql" → uses Postgres, adapter is \Nibiru\Adapter\Postgres\Db
  • driver = "postgresql" → uses Postgresql, adapter is \Nibiru\Adapter\Postgresql\Db

Base adapter (core/a/mysql.db.php:15):

abstract class Db implements IDb
{
    private static $table = array();
    
    protected static function initTable( $table = array() )
    protected static function getTable()
}

All generated models extend this and call self::initTable( self::TABLE ) in __construct() where TABLE is a constant with structure:

const TABLE = array(
    'table'     => 'tablename',
    'fields'    => ['id' => 'id', 'name' => 'name', ...]
);

3.2 The Autogenerator Pipeline

Single entry point — runs on every web request (not a CLI tool). core/c/dispatcher.php:35-39:

if(Config::getInstance()->getConfig()[self::CONFIG_GENERATOR_SECTION][self::GENERATOR_DATABASE])
{
    new Model( false );
}

So enabling [GENERATOR] database = true in the INI causes new Model(false) to fire on every call to Dispatcher::run() — i.e. on every page load. With database.overwrite = false (the safe default at settings.development.ini:81) the generator is idempotent: it only writes a file when it doesn't already exist or is zero-bytes (core/c/model.php:74, 140). Production turns the whole branch off by setting [GENERATOR] database = false.

There is no separate CLI script in the framework root that boots Table or Model standalone — grep -rn "new Model(" finds only dispatcher.php:38. The Table::__construct($argv) does parse argv-style flags (--table=…, --config-section=…, --folder-out=…, --help) at core/c/table.php:48-70, so an external CLI wrapper could be written, but the shipped framework only ever calls new Model(false)false is passed straight through to Table::__construct at core/c/model.php:18, so is_array($argv) at table.php:51 is false and no flags are parsed. Dev-mode autogeneration uses INI defaults only.

Class hierarchy and call chain

Dispatcher::run()                 core/c/dispatcher.php:38   ─► new Model(false)
  └─► Model::__construct(false)   core/c/model.php:14
        ├─ early-bail at line 16 if [DATABASE][is.active] != true
        ├─ parent::__construct($argv)                core/c/model.php:18  ─► Table::__construct
        ├─► Table::__construct($argv)                core/c/table.php:48
        │     ├─ _setParams()             reads CLI flags (no-op when $argv === false)
        │     ├─ _setConfigSection()      table.php:162   reads --config-section or [GENERATOR][config-section]
        │     ├─ _setDatabaseDriver()     table.php:148   $driver = config[$configSection][driver]
        │     ├─ _setDatabase()           table.php:194   $dbName = config[$configSection][basename]
        │     ├─ _setDbNamespace()        table.php:63    PascalCase the basename
        │     ├─ _setFolderNamespace()    table.php:111   split on '-' or '_', ucfirst each segment
        │     ├─ _setFolderOut()          table.php:208   --folder-out OR __DIR__ + [GENERATOR][folder-out] + foldernamespace
        │     ├─ _setTable()              picks --table flag, or empty (= scan all)
        │     ├─ _setTemplateFile()       table.php:67    [GENERATOR][modeltemplate]
        │     ├─ _setModelTemplate()      slurps mask file
        │     └─ _setTables()             SHOW TABLES (MySQL) or information_schema (Postgres)
        ├─► createOutFolder()             core/c/model.php:28   mkdir -p with chmod 0777
        └─► createClassFiles()            core/c/model.php:37
              ├─ if $this->getTable() !== ""   →  generate that one table
              └─ else                          →  loop $tables, generate each
                  └─► generateClassByTableName($table)   core/c/model.php:53

generateClassByTableName() — the substitution engine (core/c/model.php:53-148)

Step 1 — derive the class name (lines 57-69):

$pclassname = explode('_', $table);
$classname = "";
for($i=0; count($pclassname)>$i; $i++) {
    if($i!=0) $classname .= ucfirst($pclassname[$i]);
    else      $classname  = $pclassname[$i];     // first segment kept as-is
}

The first underscore-separated segment keeps its original case; subsequent segments get ucfirst. Examples (input → derived $classname):

Table name $classname after the loop Final (after ucfirst($classname) at line 111)
timeanddate timeanddate Timeanddate
time_and_date timeAndDate TimeAndDate
user_to_acl userToAcl UserToAcl
userToAcl (no underscore) userToAcl UserToAcl

ucfirst is applied only once at substitution time (line 111) — so a table named user_to_acl becomes class UserToAcl, but a table named User_to_acl becomes UserToAcl as well (the first segment's case is irrelevant because of the final ucfirst).

Step 2 — overwrite vs. skip (lines 70-74):

if(Config::getInstance()->getConfig()[self::CONFIG_SECTION][self::DB_OVERWRITE_MODELS]) {
    unlink($this->getFolderOut() . '/' . $classname . self::PHP_FILE_ENDING);
}
if(!file_exists(...)) {
    fclose( fopen(..., 'w') );    // create empty file
    // ...build template...
}

[GENERATOR][database.overwrite] = truedestructive on every request: the existing .php file is unlinked before regeneration. Any hand-edits are lost. The user-facing safety net is keeping database.overwrite = false (the default) and only deleting the file when you want it rebuilt.

Step 3 — build the four PHP-source strings (lines 80-104):

The generator concatenates raw PHP source for [CLASSPARAMETERS], [FIELDARRAY], [SETTERS], [GETTERS] field-by-field. Sample output for a single field timeanddate_id:

// $parameters
private $timeanddate_id;

// $setters
public function _setTimeanddateid($timeanddate_id) {
    $this->timeanddate_id = $timeanddate_id;
}

// $getters
public function getTimeanddateid() {
    return $this->timeanddate_id;
}

// $fieldarray  (entry inside the array literal)
'timeanddate_id' => 'timeanddate_id',

Two non-obvious shapes:

  • Setter naming uses str_replace('_', '', ucfirst($field)) — strips ALL underscores, then ucfirst capitalizes only the first letter. So _setTimeanddateid (one cap) is correct for timeanddate_id, but user_first_name would become _setUserfirstname (also one cap, no inner caps). This is camelCase-without-the-camel — every underscore-separated word loses its boundary.
  • $fieldarray is built as a literal PHP array(...) string with hard-coded tabs/newlines (\t\t\t\t\t\t\t\t) at lines 86, 94, 101 — the indentation is baked into the generator and won't match other code style.

Step 4 — placeholder substitution (lines 106-113), 8 fixed replacements:

Placeholder Source Where in mask
[CLASSPARAMETERS] $parameters (concatenated private $field; lines) line 19
[FIELDARRAY] $fieldarray (concatenated 'k' => 'v', lines) line 23
[SETTERS] $setters line 41
[GETTERS] $getters line 47
[TABLE] raw table name lines 7, 22
[CLASSNAME] ucfirst($classname) line 16
[FOLDERNAME] ucfirst($this->getFolderNamespace()) line 2
[DBSECTION] $this->getConfigSection() line 28

Step 5 — driver→adapter→connector mapping (lines 115-132), 3-way switch:

if($this->getDatabaseDriver()==self::DB_DRIVER_POSTGRESS) {       // "psql"
    if(Config::getInstance()->getConfig()[self::CONFIG_SECTION]['odbc']) {
        $template = str_replace('[ADAPTER]',   self::ADAPTER_POSTGRES,   $template);  // "Postgres"
        $template = str_replace('[CONNECTOR]', self::ADAPTER_POSTGRES,   $template);
    } else {
        $template = str_replace('[ADAPTER]',   self::ADAPTER_POSTGRESQL, $template);  // "Postgresql"
        $template = str_replace('[CONNECTOR]', self::ADAPTER_POSTGRESQL, $template);
    }
}
if($this->getDatabaseDriver()==self::DB_DRIVER_MYSQL) {           // "mysql"
    $template = str_replace('[ADAPTER]',   self::ADAPTER_MYSQL, $template);   // "MySQL"
    $template = str_replace('[CONNECTOR]', self::ADAPTER_PDO,   $template);   // "Pdo"
}
INI driver INI [GENERATOR][odbc] [ADAPTER] [CONNECTOR]
mysql (ignored) MySQL Pdo
psql truthy Postgres Postgres (UnixODBC path)
psql falsy/missing Postgresql Postgresql (libpq path)

Constants live on Table (core/c/table.php:20-26): DB_DRIVER_POSTGRESS = "psql" (note typo: two S's), DB_DRIVER_MYSQL = "mysql", ADAPTER_POSTGRES, ADAPTER_POSTGRESQL, ADAPTER_MYSQL, ADAPTER_PDO.

Step 6 — write file (lines 133-145):

if(Config::getInstance()->getConfig()[self::CONFIG_SECTION][self::DB_OVERWRITE_MODELS]) {
    file_put_contents(...);   // overwrite mode: always write
} else {
    if(!filesize(...)) {       // non-overwrite: only write if file is empty
        file_put_contents(...);
    }
}
chmod(..., 0777);

The non-overwrite branch checks filesize() == 0 rather than file_exists() — because step 2 created an empty file with fopen('w') / fclose(). So the dev workflow is: delete the model file you want regenerated → reload the page → the file gets recreated with fresh schema.

The mask file (application/settings/db/db.class.mask, 48 lines, verbatim)

<?php
namespace Nibiru\Model\[FOLDERNAME];
use Nibiru\Adapter\[ADAPTER]\Db;
use Nibiru\Pdo;

class [CLASSNAME] extends Db
{

    [CLASSPARAMETERS]

    const TABLE = array(
        'table'     => '[TABLE]',
        'fields'    =>  [FIELDARRAY]
    );

    public function __construct()
    {
        [CONNECTOR]::settingsSection('[DBSECTION]');
        self::initTable( self::TABLE );
    }

    public function getTableInfo()
    {
        return self::TABLE;
    }

    [SETTERS]

    [GETTERS]
}

Ten distinct placeholders appear in the mask: [FOLDERNAME], [ADAPTER], [CLASSNAME], [CLASSPARAMETERS], [TABLE] (×2), [FIELDARRAY], [CONNECTOR], [DBSECTION], [SETTERS], [GETTERS].

Postgres-mode footgun in the mask at line 4: use Nibiru\Pdo; is hardcoded, regardless of driver. For MySQL output that's correct ([CONNECTOR] substitutes to Pdo, matching the import). For Postgres output, [CONNECTOR] substitutes to Postgres or Postgresql, but the import line still reads use Nibiru\Pdo; — the unqualified Postgres::settingsSection(...) call at line 18 then resolves against the current namespace (Nibiru\Model\<Folder>\Postgres), not \Nibiru\Postgres, and PHP would fatally error at request time. Anyone running with [GENERATOR][database] = true and driver = "psql" should expect to manually patch the generated use line, or change the mask to use a [CONNECTOR_USE] placeholder. (\Nibiru\Pdo, \Nibiru\Postgres, \Nibiru\Postgresql are all in the \Nibiru root namespace — confirmed at core/c/pdo.php:2, core/c/postgres.php:3, core/c/postgresql.php:2.)

Sample generated output (verified against loach.mssql.database-connection/application/model/WarehouseLoach/timeanddate.php)

For table timeanddate, folder namespace WarehouseLoach, driver mysql, config-section DATABASE:

<?php
namespace Nibiru\Model\WarehouseLoach;
use Nibiru\Adapter\MySQL\Db;
use Nibiru\Pdo;

class Timeanddate extends Db
{
    private $timeanddate_id;
    private $timeanddate_date;
    private $timeanddate_time;

    const TABLE = array(
        'table'     => 'timeanddate',
        'fields'    =>  array(
            'timeanddate_id' => 'timeanddate_id',
            'timeanddate_date' => 'timeanddate_date',
            'timeanddate_time' => 'timeanddate_time'
        )
    );

    public function __construct()
    {
        Pdo::settingsSection('DATABASE');
        self::initTable( self::TABLE );
    }

    public function getTableInfo() { return self::TABLE; }

    public function _setTimeanddateid($timeanddate_id) { $this->timeanddate_id = $timeanddate_id; }
    public function _setTimeanddatedate($timeanddate_date) { $this->timeanddate_date = $timeanddate_date; }
    public function _setTimeanddatetime($timeanddate_time) { $this->timeanddate_time = $timeanddate_time; }

    public function getTimeanddateid() { return $this->timeanddate_id; }
    public function getTimeanddatedate() { return $this->timeanddate_date; }
    public function getTimeanddatetime() { return $this->timeanddate_time; }
}

[GENERATOR] INI keys (verified at application/settings/config/settings.development.ini:78-86)

[GENERATOR]
database               = true                                              ; master switch — turn off in production
database.overwrite     = false                                             ; true = unlink + regenerate every request (destructive)
config-section         = "DATABASE"                                        ; which [DATABASE_*] block to introspect
folder-out             = "/../../application/model/"                       ; appended to __DIR__ in core/c/table.php:217
modeltemplate          = "/../../application/settings/db/db.class.mask"   ; the substitution template
; odbc                = true                                               ; (postgres-only) routes to Postgres adapter via UnixODBC

Both folder-out and modeltemplate are resolved as __DIR__ . $value in core/c/table.php:217 and :67 respectively, where __DIR__ is core/c/ — that's why both values start with /../../.

3.3 Real Multi-Database Example

Scenario: Application needs to talk to multiple databases simultaneously.

INI setup (settings.production.ini):

[DATABASE]
is.active = true
basename = "warehouse_loach"
driver = "mysql"
hostname = "db-primary"
username = "user"
password = "pass"

[DATABASE_SECONDARY]
is.active = true
basename = "other_db"
driver = "mysql"
hostname = "db-secondary"
username = "user2"
password = "pass2"

Generated models:

Model A (autogenerated with --config-section=DATABASE):

public function __construct()
{
    Pdo::settingsSection('DATABASE');
    self::initTable( self::TABLE );
}

Model B (autogenerated with --config-section=DATABASE_SECONDARY):

public function __construct()
{
    Pdo::settingsSection('DATABASE_SECONDARY');
    self::initTable( self::TABLE );
}

Usage:

// Instantiate Model A — sets Pdo::$section to 'DATABASE'
$modelA = Db::loadModel('Namespace\ModelA');
$dataA = $modelA->loadTableAsArray();  // queries `database.warehouse_loach`

// Instantiate Model B — sets Pdo::$section to 'DATABASE_SECONDARY'
$modelB = Db::loadModel('Namespace\ModelB');
$dataB = $modelB->loadTableAsArray();  // queries `database.other_db`

// Or explicitly:
Pdo::settingsSection('DATABASE_SECONDARY');
$custom = Pdo::queryString('SELECT * FROM some_table');  // now uses DATABASE_SECONDARY connection

Cost: Zero — just object instantiation (lazy singleton, only one connection per section).


4. Module System (Deep)

4.1 Module Structure

Every module is at application/module/[modulename]/:

application/module/users/
  users.php                # Main class
  interfaces/
    users.php              # IUsers interface
    progressObserver.php   # IProgressObserver interface
  traits/
    users.php              # Users trait
    userForm.php           # UserForm trait
  plugins/
    user.php               # User plugin class
    acl.php                # ACL plugin class

4.2 Module Registry (INI-based)

File: /home/stephan/PhpstormProjects/develop.maschinen-stockert.de/application/settings/config/settings.development.ini:10-72

[AUTOLOADER]
class.pos[] = "observer"
class.pos[] = "users"
class.pos[] = "memcached"
class.pos[] = "assetmanager"
class.pos[] = "cms"

iface.pos[] = "observer"
iface.pos[] = "users"
iface.pos[] = "cms"

trait.pos[] = "observer"
trait.pos[] = "users"
trait.pos[] = "userForm"
trait.pos[] = "cms"

class.plugin.pos[] = "progressTracker"
class.plugin.pos[] = "observer"
class.plugin.pos[] = "user"
class.plugin.pos[] = "acl"

What it means:

  • class.pos[] — array of module names to load (order matters; autoloader loops in this order)
  • iface.pos[] — which interfaces to scan for each module name
  • trait.pos[] — which traits to scan for each module name
  • class.plugin.pos[] — which plugins to scan/load (order matters)

4.3 Module Loading Flow

Called by: Dispatcher::run() at line 43 (after models are loaded).

Auto::loader()->loadModelFiles();   // line 42
Auto::loader()->loadModules();      // line 43

Implementation (Auto::loadModules() at core/c/auto.php, by extension):

  1. Read INI AUTOLOADER[iface.pos] — array of interface names
  2. For each interface name, scan application/module/[modulename]/interfaces/ for matching files
  3. require_once each found file
  4. Repeat for traits, then modules, then plugins

Module class requirement:

Module main class (e.g., Users) must:

  • Implement IModule interface (from core/i/IModule.php)
  • Optionally implement SplSubject (Observer pattern)
  • Have __construct() that initializes the module

Real example (/home/stephan/PhpstormProjects/develop.maschinen-stockert.de/application/module/users/users.php:20-32):

class Users extends Module implements Interfaces\Users, SplSubject
{
    use Traits\Users;
    protected static object $usersRegistry;
    protected SplObjectStorage $observers;
    
    public function __construct()
    {
        $this->setUsersRegistry();
        $this->observers = new SplObjectStorage();
    }

4.4 Plugin Pattern

Plugins live at application/module/[name]/plugins/ and are loaded last in INI order.

Pattern: A plugin class uses Db::loadModel() to interact with the database.

Example (/home/stephan/PhpstormProjects/loach.mssql.database-connection/application/module/users/plugins/acl.php:14-40):

use Nibiru\Factory\Db;

class Acl extends User
{
    public function __construct()
    {
        parent::__construct();
    }
    
    public function init(): void
    {
        if(!$this->validate())
        {
            View::forwardTo('/users/login');
        }
        else
        {
            $this->loadUserRole();
        }
    }

Later in the plugin:

$acl = Db::loadModel('WarehouseLoach\Acl');
// Now Pdo::$section = 'DATABASE' (set by Acl's __construct())

4.5 Module Lifecycle

  1. INI parse: Dispatcher reads [AUTOLOADER] section
  2. Model load: Auto::loader()->loadModelFiles() requires all model files
  3. Module load: Auto::loader()->loadModules() scans and requires interfaces, traits, modules, plugins in order
  4. Controller load: If route matches, require and instantiate the controller
  5. Action call: If _action param exists, call $controller->{$_action}Action()

Hooks (implicit):

  • Constructor of each module class
  • Constructor of each plugin class
  • Action method of controller

5. Form System (Deep)

5.1 Architecture overview — four collaborating files

Role File What it owns
Factory (static buffer + 30 add* methods) core/f/form.php \Nibiru\Factory\Form$form, $option, $div, $element statics; create(), addForm(), addInputType*(), addType*(), addSelect/addSelectOption, addOpenDiv/addCloseDiv/addOpenAny/addCloseAny, addTypeLabel
Per-element template + allowlist core/c/type*.php (28 files) \Nibiru\Form\Type<X> — declares private $_attributes (allowed keys) and _setElement() (HTML template with UPPERCASE tokens)
Substitution engine core/c/formattributes.php \Nibiru\Form\FormAttributes_setAttributes() walks the attribute array, runs str_replace(strtoupper($key), $value, $element), then strips ~50 leftover placeholder fragments
Constant catalog core/i/IForm.php 60+ FORM_* constants — every legal attribute key (name, value, class, id, placeholder, required, onchange, onblur, onfocus, selected, context, multiple, tabindex, pattern, data-bts-decimals, data-bts-step, any, …). Constants are lowercase strings; the substitution engine uppercases them at replace time
Helper trait core/t/form.php \Nibiru\Attributes\FormloadAttributeValues() pre-formats id/class/form keys into ' id="x"' snippets so they collapse cleanly when no value was supplied

The factory has zero knowledge of HTML — it only manages the buffer and dispatches to a Type class. Each Type class is a tiny self-contained record: an attribute allowlist plus a template string. Substitution lives entirely in FormAttributes::_setAttributes().

5.2 The substitution language (UPPERCASE-token templates)

A Type class declares its template in _setElement(). Tokens are bare uppercase words inside double quotes. Example from core/c/typetext.php:42:

$this->_element = '<input type="text" name="NAME" value="VALUE" placeholder="PLACEHOLDER" maxlength="MAXLENGTH" tabindex="TABINDEX" required="REQUIRED" disabled="DISABLED" data-bts-decimals="DATA-BTS-DECIMALS" data-bts-step="DATA-BTS-STEP" SPEECH ID CLASS>';

The user passes a lowercase-keyed array (['name' => 'user_email', 'class' => 'form-control']). At core/c/formattributes.php:48-58 the engine does:

foreach( $attributes as $key=>$entry ) {
    switch ($key) {
        case array_key_exists($key, $this->_attributes):
            $this->_element = str_replace(strtoupper($key), $entry, $this->getElement());
            break;
    }
}

Two non-obvious facts:

  1. The switch ($key) { case array_key_exists(...) } form is unusual PHP — it works because case compares its expression against $key and array_key_exists() returns a boolean. The engine therefore only substitutes keys that the Type class allowlisted in its $_attributes array — passing an unknown key is silently dropped.
  2. strtoupper($key) is plain ASCII upcase, so name → NAME, data-bts-decimals → DATA-BTS-DECIMALS. The template must use the same casing.

After the substitution loop, core/c/formattributes.php:59-102 runs ~50 hard-coded str_replace() calls that strip unused placeholders so the output HTML is clean. Sample (line 84):

$this->_element = str_replace(' required="REQUIRED"', '', $this->_element);

If the user didn't pass required, the literal token REQUIRED was never replaced, and this line removes the whole ' required="REQUIRED"' fragment. The leading space matters — it prevents collapsing two attributes into each other. There's also str_replace(' ', ' ', ...) at line 87 to fold any double-spaces left over.

Special-case strip rules to know about:

  • id, class, form — these tokens appear in templates as bare words (ID, CLASS, FORM), not as id="ID". The trait at core/t/form.php:14- rewrites the user's value to ' id="x"' before substitution, so the final HTML reads <input id="user_email"> rather than <input id=" id=\"user_email\"">.
  • checked (line 63-67) — only stripped if the user did not pass checked. Passing 'checked' => true leaves the literal checked="CHECKED" in place; passing false/empty strips it. Real footgun: there's no path that emits a clean checked (without the ="CHECKED" value) — production code accepts the ="CHECKED" rendering.
  • ANY (line 100) — global strip: any leftover bare ANY is removed. Used by TypeOpenAny/TypeCloseAny.

5.3 Type class anatomy (the boilerplate every Type follows)

Verbatim core/c/typetext.php:

class TypeText extends FormAttributes implements IForm
{
    private $_attributes = array(
        self::FORM_NAME                     => '',
        self::FORM_VALUE                    => '',
        self::FORM_ATTRIBUTE_SPEECH         => '',
        self::FORM_ATTRIBUTE_ID             => '',
        self::FORM_ATTRIBUTE_CLASS          => '',
        self::FORM_ATTRIBUTE_PLACEHOLDER    => '',
        self::FORM_ATTRIBUTE_REQUIRED       => '',
        self::FORM_ATTRIBUTE_MAXLENGTH      => '',
        self::FORM_ATTRIBUTE_TABINDEX       => '',
        self::FORM_ATTRIBUTE_DISABLED       => '',
        self::FORM_ATTRIBUTE_TS_DECIMALS    => '',
        self::FORM_ATTRIBUTE_TS_STEPS       => ''
    );

    public function loadElement( $attributes )
    {
        parent::__construct( $this->_attributes );
        $this->_setElement();
        $this->_setAttributes( self::loadAttributeValues( $attributes ) );
        return $this->getElement();
    }

    private function _setElement( )
    {
        $this->_element = '<input type="text" name="NAME" ... ID CLASS>' . "\n";
    }
}

The four-step recipe — __construct (register allowlist) → _setElement (load template) → loadAttributeValues (pre-format id/class/form) → _setAttributes (substitute + strip) — is identical across all 28 Types. The only thing that varies between them is the contents of $_attributes and $_element.

5.4 The 28 Type classes — full registry

All live under \Nibiru\Form\Type<X> in core/c/type<x>.php.

Input controls (factory method Form::addInputType*):

Type class File Factory method Template excerpt Allowed keys
TypeText typetext.php addInputTypeText <input type="text" name="NAME" value="VALUE" placeholder="PLACEHOLDER" maxlength="MAXLENGTH" tabindex="TABINDEX" required="REQUIRED" disabled="DISABLED" data-bts-decimals="…" data-bts-step="…" SPEECH ID CLASS> name, value, speech, id, class, placeholder, required, maxlength, tabindex, disabled, data-bts-decimals, data-bts-step
TypePassword typepassword.php addInputTypePassword <input type="password" …> name, value, id, class, placeholder, required, …
TypeEmail typeemail.php addInputTypeEmail <input type="email" …> name, value, id, class, placeholder, required, pattern
TypeTextarea typetextarea.php addInputTypeTextarea <textarea …>VALUE</textarea> name, value, id, class, rows, cols, placeholder
TypeRadio typeradio.php addInputTypeRadio <input type="radio" …> name, value, id, class, checked
TypeCheckbox typecheckbox.php addInputTypeCheckbox <input type="checkbox" …> name, value, id, class, checked
TypeSwitch typeswitch.php addInputTypeSwitch toggle/switch markup name, id, class, checked, onchange
TypeDate typedate.php addInputTypeDate <input type="date" …> name, value, id, class, min, max
TypeSubmit typesubmit.php addInputTypeSubmit <input type="submit" …> name, value, id, class

Other controls (factory method Form::addType*, no Input):

Type class File Factory method Notes
TypeButton typebutton.php addTypeButton renders <button>VALUE</button>; production code uses value for inner HTML (e.g. '<i class="ti-save"></i>&nbsp;Speichern')
TypeNumber typenumber.php addTypeNumber <input type="number" min max step …>
TypeRange typerange.php addTypeRange slider
TypeReset typereset.php addTypeReset <input type="reset" …>
TypeSearch typesearch.php addTypeSearch <input type="search" …>
TypeTelefon typetelefon.php addTypeTelefon <input type="tel" …> (note German spelling in class name)
TypeUrl typeurl.php addTypeUrl <input type="url" …>
TypeColor typecolor.php addInputTypeColor <input type="color" …>
TypeDatetime typedatetime.php addInputTypeDatetime <input type="datetime-local" …>
TypeFileUpload typefileupload.php addTypeFileUpload <input type="file" …>; the parent <form> needs enctype="multipart/form-data"
TypeHidden typehidden.php addTypeHidden <input type="hidden" …>
TypeImageSubmit typeimagesubmit.php addTypeImageSubmit factory parameter is misspelled $attrbutes at core/f/form.php:363 — works fine, no fix needed
TypeLabel typelabel.php addTypeLabel <label for="FOR" …>VALUE</label>

Select / option (the order-dependent pair):

Type class File Factory method Template (core/c/typeselect.php:37, typeoption.php:35)
TypeSelect typeselect.php addSelect <select name="NAME" onchange="ONCHANGE" onblur="ONBLUR" onfocus="ONFOCUS" required="REQUIRED" ID CLASS SELECTED>\nOPTIONS\n</select>
TypeOption typeoption.php addSelectOption <option value="VALUE" ID CLASS SELECTED>CONTEXT</option>

The OPTIONS token in the select template is what Form::displaySelect() (core/f/form.php:71) replaces with the accumulated option HTML — see §5.6.

Structural primitives (no <input>, just open/close tags for layout):

Type class File Factory method Template
TypeOpenDiv typeopendiv.php addOpenDiv <div ID CLASS>VALUE
TypeCloseDiv typeclosediv.php addCloseDiv </div>
TypeOpenSpan typeopenspan.php (no direct factory; legacy) <span ID CLASS>
TypeCloseSpan typeclosespan.php (no direct factory; legacy) </span>
TypeOpenAny typeopenany.php addOpenAny <ANY ID CLASS>VALUE — caller passes 'any' => 'span' (or any tag name) and it becomes <span …>
TypeCloseAny typecloseany.php addCloseAny </ANY>

Form wrapper (called last):

Type class File Factory method Template
Form (inline \Nibiru\Form\Form) addForm <form name="NAME" action="ACTION" method="METHOD" target="TARGET" enctype="ENCTYPE" onsubmit="ONSUBMIT" ID CLASS>FIELDS</form>

5.5 Naming inconsistency (real footgun)

The add* methods follow three different naming patterns. There is no single rule:

  • addInputType* for: Text, Submit, Textarea, Radio, Checkbox, Switch, Password, Date, Email, Color, Datetime
  • addType* for: FileUpload, Hidden, ImageSubmit, Number, Range, Reset, Search, Telefon, Url, Button, Label
  • add* (no Type prefix) for: Form, Select, SelectOption, OpenDiv, CloseDiv, OpenAny, CloseAny

There is no semantic justification — it's historical drift. When generating training data, include both addInputTypeText and addInputType* callsite samples; do not "normalize" them, because the framework callers expect the literal names.

5.6 Select / option ordering (critical)

Options must be added before the select. This is non-obvious and the opposite of HTML reading order.

core/f/form.php:493-497addSelectOption appends to a separate buffer:

public static function addSelectOption( $attributes ) {
    self::setElement( new TypeOption() );
    self::assembleOptions( self::getElement()->loadElement( $attributes ) );
}

core/f/form.php:463-472addSelect then renders the select template, replacing its OPTIONS token with the accumulated option HTML, and clears the option buffer (assembleOptions(false)):

public static function addSelect( $attributes, $div = false ) {
    if($div!=false) self::setDiv( $div );
    self::setElement( new TypeSelect() );
    self::assemble( self::displaySelect( self::getElement()->loadElement( $attributes ) ) );
    self::assembleOptions( false );  // reset for the next select
}

displaySelect() at line 71: return str_replace( 'OPTIONS', self::$option, $select );

Reverse the order — call addSelect before addSelectOption — and the select renders empty.

5.7 Per-element div wrapping (the second $div parameter)

Almost every add* method takes an optional second argument $div = false. When passed an associative array, the next assembled element is wrapped in a <div>. From core/f/form.php:120-140:

private static function setDiv( $div = false ) {
    if($div != false) {
        if( is_array( $div ) ) {
            foreach ( $div as $key=>$selector ) {
                self::$div = '<div ' . $key . '="' . $selector . '">' . "\n" . 'ELEMENT</div>' . "\n";
            }
        } 
    }
}

And assemble() at core/f/form.php:86-97:

private static function assemble( $element ) {
    if( self::getDiv() ) {
        if(is_string(self::getDiv())) {
            $element = str_replace('ELEMENT', $element, self::getDiv());
            self::setDiv(false);  // one-shot
        }
    }
    self::$form .= $element;
}

So Form::addInputTypeText(['name' => 'x'], ['class' => 'form-group']) emits:

<div class="form-group">
<input type="text" name="x">
</div>

The wrapper is single-shot — it resets to false after one use. This is a third placeholder layer (ELEMENT) on top of OPTIONS and FIELDS.

5.8 The full assembly buffer pipeline (three placeholder layers)

addInputTypeText() ─► TypeText.loadElement()
                       ├─ FormAttributes._setAttributes()
                       │   ├─ str_replace(NAME, value, …)      [§5.2 substitution]
                       │   └─ ~50 cleanup str_replace          [§5.2 strip-unused]
                       └─ returns clean HTML
                ─► assemble()
                       └─ if $div set: str_replace(ELEMENT, html, '<div …>ELEMENT</div>')   [§5.7]
                ─► appends to self::$form

addSelectOption()  ─► TypeOption.loadElement() ─► appends to self::$option
addSelectOption()  ─► (more options) ─► appends to self::$option
addSelect()        ─► TypeSelect.loadElement() (template contains OPTIONS)
                ─► displaySelect: str_replace(OPTIONS, self::$option, html)                 [§5.6]
                ─► assemble() ─► appends to self::$form

addForm()          ─► Form.loadElement() (template contains FIELDS)
                ─► display: str_replace(FIELDS, self::$form, html)                          [final wrap]
                ─► returns the complete HTML string (does NOT echo, caller assigns)

Three replace targets, three different scopes:

Token Scope Replaced by
OPTIONS inside one <select> assembleOptions() buffer at core/f/form.php:60
ELEMENT one element only (single-shot div wrapper) assemble() at core/f/form.php:92
FIELDS inside the <form> wrapper display() at core/f/form.php:106

5.9 Real production usage (/home/stephan/PhpstormProjects/nibiru-modules/application/module/users/traits/userForm.php)

This trait builds a Bootstrap-style two-column user-edit form. It demonstrates every idiom in §5.2-5.8 in production code. Lines 12, 19, 22-89:

use Nibiru\Factory\Form;

trait UserForm {
    public static function userForm(string $action = '', array $user = []) {
        Form::create();
        self::openHalfWidthElement('Benutzer Informationen');   // emits 3 nested addOpenDiv
        self::openInnerHalfWithElement();                       // 2 more addOpenDiv
        foreach(self::FORM_CREATE_USER as $entry) {
            self::addTextElement($entry['visibleName'], $entry['valueName'], $entry['icon'], $user[$entry['valueName']] ?? '');
        }
        // …password section, account section, ACL select…
        self::addSelectDropdown($acl->loadAclRoles(), 'lock', $user['user_role_id']);
        self::closeHalfWidthElement();
        self::addSubmitFormButton('Speichern', 'save-alt');
        return Form::addForm([
            'name'   => 'userForm',
            'action' => $action,
            'class'  => 'row',
            'method' => 'post',
            'target' => '_self',
        ]);
    }
}

Note Form::addForm() is called last and is the only call that returns HTML — every other add* method appends to the buffer and returns void. The returned string is what the controller hands to Smarty.

The helper addTextElement (lines 137-175) is the canonical "labelled input with icon" pattern — Label + OpenDiv (input-group) + OpenAny (span for icon) + CloseAny + InputTypeText + CloseDiv. Reading those lines top-to-bottom is the fastest way to learn the structural primitives.

addSelectDropdown (lines 183-225) shows the option-then-select ordering — a foreach adds addSelectOption calls, then a single addSelect flushes them.


6. Request/Response Lifecycle

6.1 Front Controller Entry

File: index.php (assumed at app root)

require_once 'core/framework.php';  // loads Dispatcher::run() at end

6.2 Dispatcher::run() (core/c/dispatcher.php:33)

  1. Set timezone from INI SETTINGS[timezone]
  2. If INI GENERATOR[database]==true, run autogenerator: new Model(false)
  3. Call Router::getInstance()->route() — parse URL, extract controller/action
  4. Load all model files via Auto::loader()->loadModelFiles()
  5. Load all modules via Auto::loader()->loadModules()
  6. Check if controller file exists at application/controller/{ControllerName}Controller.php
  7. If yes:
    • Require the file
    • Instantiate the controller: $controller = new {ControllerName}Controller()
    • Check $_REQUEST['_action'] — if present and valid method exists, call $controller->{_action}Action()
    • Always call $controller->pageAction() at end (line 60)
    • Always call $controller->navigationAction() before action (line 52)
  8. If no, route to error controller (line 75-80)

6.3 Controller Lifecycle

Constructor:

  • Controller::getInstance() is called
  • Sets up request parameters

navigationAction():

  • Called first (Dispatcher line 52)
  • Typically loads navigation JSON or breadcrumbs

{_action}Action():

  • Called if _action param exists and method exists
  • Developer-defined custom logic

pageAction():

  • Called last (Dispatcher line 60)
  • Typically renders the page template

6.4 View Rendering

End of Dispatcher::run():

Debug::getInstance();           // add debug info if enabled
Display::getInstance()->display();  // render via Smarty

Display class (implied, not shown here but follows the pattern):

  • Calls View::getInstance()->display($template_name)
  • Which calls Smarty's display()

7. INI Key Reference (Exhaustive)

[ENGINE] Section

Key Type Read by Default Purpose
cache path View::_setEngine() /../../application/view/cache/ Smarty cache dir
templates path View::_setEngine() /../../application/view/templates/ Smarty template dir
templates_c path View::_setEngine() /../../application/view/templates_c/ Smarty compile dir
config_dir path View::_setEngine() /../../application/view/configs/ Smarty config dir
debug_template path View::_setEngine() /../../application/view/.../debug.tpl Debugbar template
debugbar bool View::_setEngine() false Enable Smarty debugbar
caching bool View::_setEngine() false Enable Smarty caching
error_controller string Dispatcher::run() (none; if missing, soft 404 fails) Controller for 404s
error_template string Dispatcher::run() (none; if missing, soft 404 fails) Template for 404s

[AUTOLOADER] Section

Key Type Read by Default Purpose
class.pos[] array Auto::loader()->loadModules() (none) Module names in load order
iface.pos[] array Auto::loader()->loadModules() (none) Interface names to scan
trait.pos[] array Auto::loader()->loadModules() (none) Trait names to scan
class.plugin.pos[] array Auto::loader()->loadModules() (none) Plugin names to scan (loaded last)

[GENERATOR] Section

Key Type Read by Default Purpose
database bool Dispatcher::run() false Enable model autogeneration
database.overwrite bool Model::generateClassByTableName() false Overwrite existing models
modeltemplate path Table::__construct() (none) Path to db.class.mask template
basename string Table::_setDatabase() (from [DATABASE][basename]) Database name
driver string Table::_setDatabaseDriver() (from [DATABASE][driver]) mysql / psql / postgresql
folder-out path Table::_setFolderOut() /../application/model/ Where to write generated files
config-section string Table::_setConfigSection() DATABASE INI section to read DB settings from
odbc bool Table::_setTables() false Use ODBC for Postgres (psql)

[DATABASE] and [DATABASE_*] Sections

Key Type Read by Default Purpose
is.active bool Mysql::__construct() (none; required) Enable this connection
username string Mysql::__construct() (none; required) DB user
password string Mysql::__construct() (none; required) DB password
hostname string Mysql::__construct() (none; required) DB host
basename string Mysql::__construct() (none; required) DB name
driver string Mysql::__construct() (none; required) mysql / pdo / psql / postgresql
port string Mysql::__construct() 3306 DB port
encoding string Mysql::__construct() utf8 Character encoding
db_info_print bool (optional) 0 Debug flag (not core)

Real example from loach.mssql.database-connection/application/settings/config/settings.production.ini:

[DATABASE]
is.active = true
username = "loach"
password = "08mj7hchqegtjf7znc6978d"
hostname = "sto-loach-production-mariadb-1"
basename = "warehouse_loach"
driver = "mysql"
port = "3306"
encoding = "UTF8"
db_info_print = 0

[SETTINGS] Section

Key Type Read by Default Purpose
timezone string Dispatcher::run() UTC PHP timezone
pageurl string (none; custom use) (none) Current domain
navigation string (none; custom use) (none) Navigation file
dbmodel path Auto::loader() /../../application/model/ Model folder
module path Auto::loader() /../../application/module/[NAME] Module folder (with [NAME] placeholder)
interfaces path Auto::loader() /../../application/module/[NAME]/interfaces/ Interfaces folder
traits path Auto::loader() /../../application/module/[NAME]/traits/ Traits folder
plugins path Auto::loader() /../../application/module/[NAME]/plugins/ Plugins folder
entriesperpage int (none; custom use) (none) Pagination size

[ROUTING] Section

Key Type Read by Default Purpose
route[index] string Router::getInstance() / Home route
route[controller] string Router::getInstance() /controller Controller route prefix

8. Namespace Conventions Summary

Auto-generated Model Namespace

Table name: timeanddate
Database folder (from INI basename): warehouse_loach
Folder namespace: WarehouseLoach (converted from warehouse_loach via underscore→PascalCase)
Full namespace: \Nibiru\Model\WarehouseLoach\Timeanddate
File path: application/model/WarehouseLoach/Timeanddate.php

Module Namespace

Module name (from INI): users
Main class: \Nibiru\Module\Users\Users
File path: application/module/users/users.php

Interfaces: \Nibiru\Module\Users\Interfaces\{InterfaceName}
File path: application/module/users/interfaces/{interfacename}.php

Traits: \Nibiru\Module\Users\Traits\{TraitName}
File path: application/module/users/traits/{traitname}.php

Plugins: \Nibiru\Module\Users\Plugins\{PluginName}
File path: application/module/users/plugins/{pluginname}.php


9. Constants Used in Framework

IMysql Constants (core/i/IMysql.php)

const SETTINGS_DATABASE = "DATABASE";
const PLACE_NO_QUERY = "NO QUERY";
const NO_ID = false;
const PLACE_TABLE_NAME = "NO TABLENAME";
const PLACE_ARRAY_NAME = "NO ARRAY";
const PLACE_QUERY_LIMIT = "NO LIMIT";
const PLACE_SORT_ORDER = "NO ORDER";
const PLACE_DSN = "NO CONNECTION STRING";
const PLACE_IS_ACTIVE = "is.active";
const PLACE_USERNAME = "username";
const PLACE_PASSWORD = "password";
const PLACE_HOSTNAME = "hostname";
const PLACE_DRIVER = "driver";
const PLACE_DATABASE = "basename";
const PLACE_PORT = "port";
const PLACE_ENCODING = "encoding";
const PLACE_PRIMARY_KEY = "PRI";
const PLACE_COLUMN_NAME = "NO COLUMN NAME";
const PLACE_SEARCH_TERM = "NO SEARCH PARAMETER";
const PLACE_FIELD_NAME = "NO FIELD NAME";
const PLACE_WHERE_VALUE = "NO WHERE VALUE";
const PLACE_DES_ENCRYPT = false;
const PLACE_SQL_UPDATE = "UPDATE";
const PLACE_SQL_INSERT = "INSERT";

Used in: Pdo, Mysql, all database operations.

IView Constants (core/i/IView.php)

const NIBIRU_SETTINGS = "SETTINGS";
const NIBIRU_ROUTING = "ROUTING";
const NIBIRU_SECURITY = "SECURITY";
const NIBIRU_CONTENT_TYPE_JSON = "Content-Type: application/json";
const NIBIRU_CONTENT_TYPE_CONNECTION = "Connection: close";
const NIBIRU_CONTENT_ENCODING = "Content-Encoding: gzip";
const NIBIRU_CONTENT_TRANSFER_ENCODING = "Content-Transfer-Encoding: {transfer}";
const NIBIRU_FILE_END = ".tpl";

Used in: Config, View, all template rendering.

IOdbc Constants (core/i/IOdbc.php)

const SETTINGS_DATABASE = "DATABASE";
const PLACE_READONLY = "readonly";
const FILTER_COLUMN_NAME = "COLUMN_NAME";

Used by: Odbc adapter, Postgres drivers.


10. Production Patterns NOT in Framework Core

These patterns exist in /develop.maschinen-stockert.de/, /loach.mssql.database-connection/, but are not part of Nibiru core. They're downstream conventions:

10.1 CMS Module Pattern

The CMS module at application/module/cms/ models pages, blocks, templates, text content separately. It's a complete sub-framework. Not in Nibiru core.

10.2 Module Plugin Initialization Pattern

Production code often has plugins with an init() method that must be called. Example:

use Nibiru\Module\Cms\Plugins\Cms;
use Nibiru\Module\Assetmanager\Plugins\Assetmanager;

$cms = Cms::init(Assetmanager::init()->loadTemplate());
$cms->loadPageTemplate();

This is not a framework requirement, just a reusable downstream pattern.

10.3 Observer Pattern

Several modules implement SplSubject and support attach(), detach(), notify() for observer registration. Not core.


11. Gotchas & Footguns

11.1 Pdo::$section is Last-Write-Wins

$userModel = Db::loadModel('App\User');    // Pdo::$section = 'DATABASE'
$logs = Pdo::queryString('SELECT ...');     // queries DATABASE

$adminModel = Db::loadModel('App\Admin');  // Pdo::$section = 'DATABASE_ADMIN'
$logs = Pdo::queryString('SELECT ...');     // queries DATABASE_ADMIN, not DATABASE!

// Old queries using Pdo directly are affected!

Mitigation: Always instantiate the model you need, or explicitly call Pdo::settingsSection() before raw Pdo queries.

11.2 Form Factory Builds Statefully

Form::create();
Form::addInputTypeText(...);
// ERROR: calling Form::addForm() before final field:
$html = Form::addForm(...);  // includes only text field, missing password!

Mitigation: Always add all fields BEFORE calling addForm().

11.3 Select/Option Order Matters

Form::create();
Form::addSelect(...);  // ERROR: OPTIONS buffer is empty!
Form::addSelectOption(...);  // too late
$html = Form::addForm(...);

Mitigation: Always add options BEFORE select.

11.4 Module Loading Order from INI

[AUTOLOADER]
class.pos[] = "late_module"
class.pos[] = "early_module"

Late module's constructor runs first. If it depends on early module being initialized, you'll get a silent failure.

Mitigation: Order in INI carefully; test initialization order.

11.5 Table/Model Field Casing

Autogenerator converts field names naively:

  • user_id → setter _setUserId(), getter getUserId()
  • user_ID → setter _setUserID(), getter getUserID() (double caps)

Mitigation: Enforce lowercase-with-underscores in database schema.

11.6 Missing INI Keys Crash Silently

Config::getInstance()->getConfig()['MISSING_SECTION']['key'];
// Returns NULL, no error

Then later:

array_key_exists($null_value, $array);  // Warning: wrong number of args

Mitigation: Always check array_key_exists() before accessing nested INI values.

11.7 Auto::loader() is Not PSR-4

The autoloader does NOT follow PSR-4's composer.json mapping. It scans directories with patterns. If you forget to register a module in INI class.pos[], it won't load.

Mitigation: Always add module to INI after creating.


12. Request Handling Trace (end-to-end)

  1. Browser: GET /users/login
  2. Web server routes to index.php
  3. index.php requires core/framework.php
  4. framework.php at line 119 calls Nibiru\Dispatcher::getInstance()->run()
  5. Dispatcher::run():
    • Calls Router::getInstance()->route() → parses URL → sets $_REQUEST['_action'] = 'login'
    • Calls Auto::loader()->loadModelFiles() → requires all model files
    • Calls Auto::loader()->loadModules() → requires all module files
    • Checks application/controller/usersController.php exists
    • Requires and instantiates: $controller = new usersController()
    • Checks $_REQUEST['_action'] == 'login' → calls $controller->loginAction() if exists
    • Calls $controller->navigationAction() (before action)
    • Calls $controller->pageAction() (after action)
    • Calls Debug::getInstance()
    • Calls Display::getInstance()->display()
  6. Controller methods:
    • navigationAction(): Load nav JSON
    • loginAction(): Validate form, check $_POST['username'], $_POST['password']
    • pageAction(): Load login template
  7. View renders template via Smarty

Appendix: File Index

Core Framework Files

Configuration & Bootstrapping:

  • core/framework.php — entry point, includes all core files
  • core/c/config.phpConfig::getInstance(), environment detection
  • core/c/settings.phpSettings::getInstance(), INI parsing
  • core/c/router.phpRouter::getInstance(), URL routing
  • core/c/dispatcher.phpDispatcher::getInstance()->run(), request dispatch

Database:

  • core/c/pdo.phpPdo, database switcher singleton
  • core/c/mysql.phpMysql, base connection class
  • core/c/table.phpTable, autogenerator CLI parser
  • core/c/model.phpModel, autogenerator executor
  • core/f/db.phpDb::loadModel() static factory
  • core/a/mysql.db.php\Nibiru\Adapter\MySQL\Db base class
  • core/i/IMysql.php, core/i/IOdbc.php, core/i/IDb.php — database interfaces

Views & Forms:

  • core/c/view.phpView::getInstance(), Smarty wrapper
  • core/c/controller.phpController::getInstance(), request access
  • core/f/form.phpForm::create() static factory
  • core/c/type*.php — form type classes (TypeText, TypeEmail, etc.)
  • core/i/IForm.php, core/i/IView.php, core/i/IController.php — interfaces

Modules & Autoloading:

  • core/c/auto.phpAuto::loader(), modern autoloader
  • core/c/autoloader.php — deprecated autoloader (legacy)
  • core/c/module.phpModule base class
  • core/a/module.php\Nibiru\Adapter\Module base adapter
  • core/i/IModule.php — module interface

Utilities:

  • core/c/debug.php — debug output
  • core/c/display.php — final rendering
  • core/c/engine.php — template engine setup
  • core/l/autoload.php — PSR-4 autoloader hook

Application Files

Controllers:

  • application/controller/*Controller.php — request handlers

Models (autogenerated):

  • application/model/<FolderName>/*.php — generated model classes

Modules:

  • application/module/<name>/<name>.php — main module class
  • application/module/<name>/interfaces/*.php — module interfaces
  • application/module/<name>/traits/*.php — module traits
  • application/module/<name>/plugins/*.php — plugin classes

Configuration & Views:

  • application/settings/config/settings.[ENV].ini — environment config
  • application/settings/db/db.class.mask — model template
  • application/view/templates/ — Smarty templates

Appendix: Quick Reference

Load a Model & Query Data

use Nibiru\Factory\Db;

$model = Db::loadModel('App\Users');
$users = $model->loadTableAsArray();
$user = $model->selectRowsetById(5);
$admins = $model->selectDatasetByFieldWhere(['field' => 'role', 'value' => 'admin']);

Insert Data

Db::loadModel('App\Logs')->insertArrayIntoTable([
    'log_message' => 'User logged in',
    'log_timestamp' => date('Y-m-d H:i:s')
]);
$newId = Db::loadModel('App\Logs')->lastInsertId();

Switch Database

Pdo::settingsSection('DATABASE_SECONDARY');
$data = Pdo::queryString('SELECT * FROM table_name');

Build a Form

use Nibiru\Factory\Form;

Form::create();
Form::addInputTypeText(['NAME' => 'email', 'ID' => 'email_field']);
Form::addInputTypePassword(['NAME' => 'password']);
Form::addInputTypeSubmit(['VALUE' => 'Login']);
echo Form::addForm(['METHOD' => 'POST', 'ACTION' => '/auth']);

Access Request Data in Controller

class MyController extends Controller {
    public function myAction() {
        $email = $this->getPost('email');
        $page = $this->getGet('page');
        $param = $this->getRequest('param');
        $user = $this->getSession('user');
    }
}

Assign Template Variables

View::assign(['page_title' => 'Home', 'user_name' => 'John']);
// In template: {$page_title}, {$user_name}

End of Document