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>
1621 lines
68 KiB
Markdown
1621 lines
68 KiB
Markdown
# 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> 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**
|
||
|