# 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\\` | `\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\\` maps to disk path from `$basePath//TableName.php` - Module folder: `\Nibiru\Module\\` maps to disk path from `application/module//` - 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` ```php 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:** ```php 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:** ```php 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`): ```php 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`): ```php 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:** ```php 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`): ```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:** ```php 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`): ```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 `
` 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)` | `` wrapper | Uses `{FIELDS}` placeholder | | `addInputTypeText($attributes)` | `` | | | `addInputTypeSubmit($attributes)` | `` | | | `addInputTypePassword($attributes)` | `` | | | `addInputTypeEmail($attributes)` | `` | | | `addInputTypeCheckbox($attributes)` | `` | | | `addInputTypeRadio($attributes)` | `` | | | `addInputTypeSwitch($attributes)` | HTML5 toggle | | | `addInputTypeDate($attributes)` | `` | | | `addInputTypeDatetime($attributes)` | `` | | | `addInputTypeColor($attributes)` | `` | | | `addInputTypeNumber($attributes)` | `` | | | `addInputTypeRange($attributes)` | `` | | | `addTypeSearch($attributes)` | `` | | | `addTypeTelefon($attributes)` | `` | | | `addTypeUrl($attributes)` | `` | | | `addTypeFileUpload($attributes)` | `` | | | `addTypeHidden($attributes)` | `` | | | `addTypeImageSubmit($attributes)` | `` | | | `addTypeReset($attributes)` | `` | | | `addTypeButton($attributes)` | ``; production code uses `value` for inner HTML (e.g. `' Speichern'`) | | `TypeNumber` | `typenumber.php` | `addTypeNumber` | `` | | `TypeRange` | `typerange.php` | `addTypeRange` | slider | | `TypeReset` | `typereset.php` | `addTypeReset` | `` | | `TypeSearch` | `typesearch.php` | `addTypeSearch` | `` | | `TypeTelefon` | `typetelefon.php` | `addTypeTelefon` | `` (note German spelling in class name) | | `TypeUrl` | `typeurl.php` | `addTypeUrl` | `` | | `TypeColor` | `typecolor.php` | `addInputTypeColor` | `` | | `TypeDatetime` | `typedatetime.php` | `addInputTypeDatetime` | `` | | `TypeFileUpload` | `typefileupload.php` | `addTypeFileUpload` | ``; the parent `` needs `enctype="multipart/form-data"` | | `TypeHidden` | `typehidden.php` | `addTypeHidden` | `` | | `TypeImageSubmit` | `typeimagesubmit.php` | `addTypeImageSubmit` | factory parameter is misspelled `$attrbutes` at `core/f/form.php:363` — works fine, no fix needed | | `TypeLabel` | `typelabel.php` | `addTypeLabel` | `` | **Select / option (the order-dependent pair):** | Type class | File | Factory method | Template (`core/c/typeselect.php:37`, `typeoption.php:35`) | |---|---|---|---| | `TypeSelect` | `typeselect.php` | `addSelect` | `` | | `TypeOption` | `typeoption.php` | `addSelectOption` | `` | 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 ``, just open/close tags for layout):** | Type class | File | Factory method | Template | |---|---|---|---| | `TypeOpenDiv` | `typeopendiv.php` | `addOpenDiv` | `
VALUE` | | `TypeCloseDiv` | `typeclosediv.php` | `addCloseDiv` | `
` | | `TypeOpenSpan` | `typeopenspan.php` | (no direct factory; legacy) | `` | | `TypeCloseSpan` | `typeclosespan.php` | (no direct factory; legacy) | `` | | `TypeOpenAny` | `typeopenany.php` | `addOpenAny` | `VALUE` — caller passes `'any' => 'span'` (or any tag name) and it becomes `` | | `TypeCloseAny` | `typecloseany.php` | `addCloseAny` | `` | **Form wrapper (called last):** | Type class | File | Factory method | Template | |---|---|---|---| | `Form` | (inline `\Nibiru\Form\Form`) | `addForm` | `FIELDS
` | ### 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-497` — `addSelectOption` appends to a separate buffer: ```php public static function addSelectOption( $attributes ) { self::setElement( new TypeOption() ); self::assembleOptions( self::getElement()->loadElement( $attributes ) ); } ``` `core/f/form.php:463-472` — `addSelect` then renders the select template, replacing its `OPTIONS` token with the accumulated option HTML, and **clears the option buffer** (`assembleOptions(false)`): ```php 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 `
`. From `core/f/form.php:120-140`: ```php private static function setDiv( $div = false ) { if($div != false) { if( is_array( $div ) ) { foreach ( $div as $key=>$selector ) { self::$div = '
' . "\n" . 'ELEMENT
' . "\n"; } } … } } ``` And `assemble()` at `core/f/form.php:86-97`: ```php 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: ```html
``` 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, '
ELEMENT
') [§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 `