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

1621 lines
68 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`
```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 `<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`:
```php
Form::addInputTypeText(['NAME' => 'username', 'ID' => 'user'], ['class' => 'form-group']);
```
This wraps the element: `<div class="form-group">ELEMENT</div>`
**Usage pattern:**
```php
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:**
```php
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:**
```php
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:**
```php
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.php``Pdo 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`):
```php
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:
```php
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`:
```php
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):**
```php
$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):**
```php
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] = true`**destructive on every request**: the existing `.php` file is `unlink`ed 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`:
```php
// $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:**
```php
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):**
```php
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
<?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
<?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`)
```ini
[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`):
```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`):
```php
public function __construct()
{
Pdo::settingsSection('DATABASE');
self::initTable( self::TABLE );
}
```
Model B (autogenerated with `--config-section=DATABASE_SECONDARY`):
```php
public function __construct()
{
Pdo::settingsSection('DATABASE_SECONDARY');
self::initTable( self::TABLE );
}
```
**Usage:**
```php
// 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`
```ini
[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).
```php
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`):
```php
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`):
```php
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:
```php
$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\Form``loadAttributeValues()` 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`:
```php
$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:
```php
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):
```php
$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`:
```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-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 `<div>`. 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 = '<div ' . $key . '="' . $selector . '">' . "\n" . 'ELEMENT</div>' . "\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
<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:
```php
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)
```php
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():**
```php
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`:
```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)
```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)
```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)
```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:
```php
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
```php
$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
```php
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
```php
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
```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
```php
Config::getInstance()->getConfig()['MISSING_SECTION']['key'];
// Returns NULL, no error
```
Then later:
```php
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.php``Config::getInstance()`, environment detection
- `core/c/settings.php``Settings::getInstance()`, INI parsing
- `core/c/router.php``Router::getInstance()`, URL routing
- `core/c/dispatcher.php``Dispatcher::getInstance()->run()`, request dispatch
**Database:**
- `core/c/pdo.php``Pdo`, database switcher singleton
- `core/c/mysql.php``Mysql`, base connection class
- `core/c/table.php``Table`, autogenerator CLI parser
- `core/c/model.php``Model`, autogenerator executor
- `core/f/db.php``Db::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.php``View::getInstance()`, Smarty wrapper
- `core/c/controller.php``Controller::getInstance()`, request access
- `core/f/form.php``Form::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.php``Auto::loader()`, modern autoloader
- `core/c/autoloader.php` — deprecated autoloader (legacy)
- `core/c/module.php``Module` 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
```php
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
```php
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
```php
Pdo::settingsSection('DATABASE_SECONDARY');
$data = Pdo::queryString('SELECT * FROM table_name');
```
### Build a Form
```php
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
```php
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
```php
View::assign(['page_title' => 'Home', 'user_name' => 'John']);
// In template: {$page_title}, {$user_name}
```
---
**End of Document**