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>
68 KiB
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::$sectionstatic, how it's called, why "last call wins." - The autogenerator pipeline: CLI flag → INI section → driver → adapter → generated model's
extends Dbline → model's__construct()callingPdo::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.debut 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 (
dbmodelin INI):\Nibiru\Model\<FolderName>\<TableName>maps to disk path from$basePath/<FolderName>/TableName.php - Module folder:
\Nibiru\Module\<ModuleName>\maps to disk path fromapplication/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 definesAPPLICATION_ENVconstant). - 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):
- Static section variable (
Pdo::$section, line 14) — last write wins per request - Every model's
__construct()callsPdo::settingsSection('[DBSECTION]')or[CONNECTOR]::settingsSection('[DBSECTION]') - 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 viaparent::getInstance(self::getSettingsSection())queryString($string, $associative)— fetch allfetchTableAsArray($tablename, $limit, $order)— table dump with paginationselectDatasetByFieldAndValue($tablename, $fieldAndValue, $sortOrder)— WHERE clausefetchRowInArrayById($tablename, $id)— single row by PKfetchRowInArrayByWhere($tablename, $column_name, $parameter_name)— single row by columninsertArrayIntoTable($tablename, $array_name, $encrypted)— insert with optionalDES_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:
- Call
Db::loadModel('WarehouseLoach\\Timeanddate') - Internally:
_setModel("WarehouseLoach\\Timeanddate") - Constructs namespace:
$fmodel = "\\Nibiru\\Model\\".$model=\Nibiru\Model\WarehouseLoach\Timeanddate - Instantiates:
self::$_model = new $fmodel; - 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:
Form::create()— clearsself::$formstatic buffer- Call methods in sequence:
Form::addInputTypeText(...),Form::addSelect(...), etc. - Each appends HTML to
self::$form; final call isForm::addForm($attributes)which wraps with<form>tags addForm()callsdisplaySelect()to replaceOPTIONSplaceholder with options added viaaddSelectOption()
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)—$_POSTgetGet($param)—$_GETgetSession($param, bool $params = false, bool $checkForActiveSession = false)getServer($param)—$_SERVERgetFiles($param)—$_FILESgetController()— 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 toCACHING_LIFETIME_CURRENT
assign() pattern:
View::assign(['user_name' => 'John', 'role' => 'admin']);
// in Smarty template: {$user_name}, {$role}
display() logic:
- Checks if
$pageends with.tpl(defined byView::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/PDOcore/c/pdo.php—Pdo extends Mysqlcore/i/IMysql.php— base constants (table names, field names, etc.)core/a/mysql.db.php—\Nibiru\Adapter\MySQL\Dbbase classcore/c/postgres.php— Postgres ODBC drivercore/c/postgresql.php— Postgresql PDO driver
Driver selection: Determined by INI [GENERATOR][driver] or [DATABASE*][driver] field:
driver = "mysql"→ usesPdo, adapter is\Nibiru\Adapter\MySQL\Dbdriver = "psql"→ usesPostgres, adapter is\Nibiru\Adapter\Postgres\Dbdriver = "postgresql"→ usesPostgresql, 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] = true → destructive 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, thenucfirstcapitalizes only the first letter. So_setTimeanddateid(one cap) is correct fortimeanddate_id, butuser_first_namewould become_setUserfirstname(also one cap, no inner caps). This is camelCase-without-the-camel — every underscore-separated word loses its boundary. $fieldarrayis built as a literal PHParray(...)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 nametrait.pos[]— which traits to scan for each module nameclass.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):
- Read INI
AUTOLOADER[iface.pos]— array of interface names - For each interface name, scan
application/module/[modulename]/interfaces/for matching files require_onceeach found file- Repeat for traits, then modules, then plugins
Module class requirement:
Module main class (e.g., Users) must:
- Implement
IModuleinterface (fromcore/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
- INI parse: Dispatcher reads
[AUTOLOADER]section - Model load:
Auto::loader()->loadModelFiles()requires all model files - Module load:
Auto::loader()->loadModules()scans and requires interfaces, traits, modules, plugins in order - Controller load: If route matches, require and instantiate the controller
- Action call: If
_actionparam 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:
$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:
- The
switch ($key) { case array_key_exists(...) }form is unusual PHP — it works becausecasecompares its expression against$keyandarray_key_exists()returns a boolean. The engine therefore only substitutes keys that the Type class allowlisted in its$_attributesarray — passing an unknown key is silently dropped. strtoupper($key)is plain ASCII upcase, soname → 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 asid="ID". The trait atcore/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 passchecked. Passing'checked' => trueleaves the literalchecked="CHECKED"in place; passingfalse/empty strips it. Real footgun: there's no path that emits a cleanchecked(without the="CHECKED"value) — production code accepts the="CHECKED"rendering.ANY(line 100) — global strip: any leftover bareANYis removed. Used byTypeOpenAny/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> 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, DatetimeaddType*for: FileUpload, Hidden, ImageSubmit, Number, Range, Reset, Search, Telefon, Url, Button, Labeladd*(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:
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)):
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)
- Set timezone from INI
SETTINGS[timezone] - If INI
GENERATOR[database]==true, run autogenerator:new Model(false) - Call
Router::getInstance()->route()— parse URL, extract controller/action - Load all model files via
Auto::loader()->loadModelFiles() - Load all modules via
Auto::loader()->loadModules() - Check if controller file exists at
application/controller/{ControllerName}Controller.php - 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)
- 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
_actionparam 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(), gettergetUserId()user_ID→ setter_setUserID(), gettergetUserID()(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)
- Browser:
GET /users/login - Web server routes to
index.php index.phprequirescore/framework.phpframework.phpat line 119 callsNibiru\Dispatcher::getInstance()->run()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.phpexists - 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()
- Calls
- Controller methods:
navigationAction(): Load nav JSONloginAction(): Validate form, check$_POST['username'],$_POST['password']pageAction(): Load login template
- View renders template via Smarty
Appendix: File Index
Core Framework Files
Configuration & Bootstrapping:
core/framework.php— entry point, includes all core filescore/c/config.php—Config::getInstance(), environment detectioncore/c/settings.php—Settings::getInstance(), INI parsingcore/c/router.php—Router::getInstance(), URL routingcore/c/dispatcher.php—Dispatcher::getInstance()->run(), request dispatch
Database:
core/c/pdo.php—Pdo, database switcher singletoncore/c/mysql.php—Mysql, base connection classcore/c/table.php—Table, autogenerator CLI parsercore/c/model.php—Model, autogenerator executorcore/f/db.php—Db::loadModel()static factorycore/a/mysql.db.php—\Nibiru\Adapter\MySQL\Dbbase classcore/i/IMysql.php,core/i/IOdbc.php,core/i/IDb.php— database interfaces
Views & Forms:
core/c/view.php—View::getInstance(), Smarty wrappercore/c/controller.php—Controller::getInstance(), request accesscore/f/form.php—Form::create()static factorycore/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 autoloadercore/c/autoloader.php— deprecated autoloader (legacy)core/c/module.php—Modulebase classcore/a/module.php—\Nibiru\Adapter\Modulebase adaptercore/i/IModule.php— module interface
Utilities:
core/c/debug.php— debug outputcore/c/display.php— final renderingcore/c/engine.php— template engine setupcore/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 classapplication/module/<name>/interfaces/*.php— module interfacesapplication/module/<name>/traits/*.php— module traitsapplication/module/<name>/plugins/*.php— plugin classes
Configuration & Views:
application/settings/config/settings.[ENV].ini— environment configapplication/settings/db/db.class.mask— model templateapplication/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