On this page
- Module development: the complete working contract
- Semantic contract coverage
- 1. Start with the system boundaries
- 2. Reference module layout
- 3. Route and security contract
- 4. DD and FD are executable contracts
- 5. Keep the router small
- 6. Build forms through dbxForm
- 7. Build lists and nested lists through dbxReport
- 8. Keep templates structural
- 9. Ajax and confirmation reuse the system libraries
- 10. Make multi-table mutations atomic
- 11. Installation and fixtures
- 12. Required acceptance matrix
- 13. Common wrong turns
Module development: the complete working contract
Reference code last changed: August 18, 2026
Revalidated against dbxapp 4.5.3: August 22, 2026
The first date identifies the last domain-level change to the reference module. The second confirms that the unchanged code passed the complete acceptance suite with the current product release.
This manual describes the supported path for a data-backed dbxapp module. The reference module myInvoices combines routing, DD and FD definitions, dbxDB, dbxForm, dbxReport, dbxTPL, Ajax, permissions, transactions, and acceptance tests without creating a parallel framework.
Normative language: must and must not define product requirements. Should identifies the default design choice; deviations need a documented domain reason.
Semantic contract coverage
These stable contract IDs bind the German and English editions together. Publication fails if either language omits a contract.
| Contract | Evidence in this manual |
|---|---|
routing / security | Route matrix, permissions, and action tokens |
dd-fd / form / report | Executable data, form, and list contracts |
ajax / transactions | One PHP path and an atomic multi-table mutation |
installation / acceptance | Protected synchronization and reproducible acceptance |
1. Start with the system boundaries
A module owns a domain task. It does not own database drivers, global routing, template parsing, authentication, or shared browser infrastructure. Use the established system facades and keep domain decisions in a small router plus a service.
| Question | Responsible layer | Do not place it in |
|---|---|---|
| Which route is requested? | Module router | Templates or SQL |
| Which domain operation runs? | Service | Global helpers |
| Where does data come from? | dbxDB through a complete DD | PDO, mysqli, SQLite3, or inline SQL |
| Which fields are displayed? | FD | A second field definition in PHP |
| How are forms and lists processed? | dbxForm and dbxReport | Custom request loops |
| How is output rendered? | dbxTPL | Large PHP strings |
2. Reference module layout
dbx/modules/myInvoices/
├── cfg/config.php
├── dd/invoice.dd.php
├── dd/invoiceItem.dd.php
├── fd/invoice-form.fd.php
├── fd/rpt-invoice-selection.fd.php
├── include/myInvoicesService.class.php
├── tpl/htm/invoice-form.htm
├── tpl/htm/invoice-report.htm
├── tpl/htm/invoice-items-report.htm
├── myInvoices.class.php
└── tests/
The DD files are the authoritative schema and permission definitions. The FD files define the fields and messages used by dbxForm and dbxReport. The router validates the requested route and delegates to the service. Templates contain structure; the service contains domain decisions.
3. Route and security contract
Classify every route before implementing it. Read-only navigation remains a normal GET. Forms use the submit protection provided by dbxForm. A state-changing GET action must use a route- and record-bound action token in addition to module and DD permissions.
| Route | Purpose | Protection |
|---|---|---|
dbx_run1=report | List invoices | Module access and DD read permission |
dbx_run1=positions&invoice_id=17 | Render one embedded line-item list | Module access plus both DD read permissions |
dbx_run1=form&rid=17 | Edit an invoice | dbxForm submit protection and DD write permission |
dbx_run1=delete&rid=17 | Delete an invoice and its items | Permissions plus a record-bound action token |
dbx_run1=install | Synchronize DDs and fixtures | Administrator permission and a protected dbxForm POST |
A confirmation dialog is a usability feature, not an authorization boundary. The server still checks the route, record binding, token, module access, and DD privileges.
4. DD and FD are executable contracts
Define tables, fields, indexes, relationships, owner behavior, and privileges explicitly in each DD. Use the standard export structure with TABLE, FIELDS, and INDEXES. Do not hide field definitions in local closures, and do not set audit fields such as creation date, creator, owner, or update date in domain code; dbxDB maintains them.
An FD selects the visible fields and provides labels, validation messages, and view-specific options. Keep German, English, and Spanish message keys structurally identical. The service loads messages from the FD instead of maintaining a second set of visible strings.
5. Keep the router small
public function run(): string
{
$route = (string) dbx()->get_modul_var('dbx_run1', 'report');
return match ($route) {
'report' => $this->service->report(),
'positions' => $this->service->positions(),
'form' => $this->service->form(),
'delete' => $this->service->delete(),
'install' => $this->service->install(),
default => $this->service->notFound(),
};
}
The same router handles a direct request and a server-side module inclusion. There is no separate internal HTTP API for the embedded position list.
6. Build forms through dbxForm
Initialize dbxForm with the DD, FD, record identifier, and template. Let it resolve submitted values, validate them, retain errors and messages, and persist through save_post(). After an insert, keep the returned RID in the form action so that a second submission updates the same record instead of inserting a duplicate.
Use callbacks only for genuine domain work. The normal callback owner and the {fid}_{event} naming convention already connect the service to the form. An explicit callback registration is needed only when the module intentionally departs from that convention.
7. Build lists and nested lists through dbxReport
The outer report lists invoices. Its record callback formats monetary snapshots, creates the signed row action, accumulates the visible-page total, and sets the marker for the matching line-item report. The inner report calculates quantity × unit price and its complete invoice total.
[modul=myInvoices]dbx_run1=positions&invoice_id=17[/modul]
The interpreter processes this marker on the server. It applies the protected module variables, runs the same module router in the current owner context, checks permissions, and replaces the marker with the resulting HTML. It does not create another browser request.
Use {rpt:col_count} when a row must span every report column and {rpt:colspan} when a footer label spans every column except the final value column. This keeps templates correct when report fields change.
8. Keep templates structural
Templates define the form shell, report rows, empty states, footers, and action controls. They may contain dbxTPL placeholders and inert module markers, but they must not contain domain queries or authorization decisions.
Give each form and report a stable 1 root. Multiple instances on the same page then keep their field state, callbacks, messages, and Ajax replacement targets separate. The root is an instance marker, not a literal value: the runtime replaces 1 with a unique ID for each rendered instance.
9. Ajax and confirmation reuse the system libraries
confirm.jsasks whether the user wants to continue.ajax.jssubmits the existing link or form and replaces the declared HTML target.- The server repeats every permission, token, validation, and transaction check.
- The returned fragment uses the same template as a normal request.
- The runtime initializes required features again after replacement.
A form must still work without JavaScript. Ajax improves the interaction; it does not create a second business path.
10. Make multi-table mutations atomic
Invoice headers and items share one DD server, so deletion runs in a single dbxDB transaction. Start the transaction, delete through both DDs, commit only after every step succeeds, and roll back on any exception. The integration test must force a failure after the first mutation and prove that neither table changed.
11. Installation and fixtures
Do not synchronize schemas on ordinary requests. The protected installation route displays a dbxForm first; only a valid administrator POST starts DD synchronization and idempotent fixtures. Fixtures identify their own demo records, skip existing data, and never overwrite user records.
The same installer may be exposed to automation through a CLI adapter, but both entry points must call the same service. Do not create a second persistence path.
12. Required acceptance matrix
- Direct and embedded reports return the same authorized records.
- A known marker reveals no data when a required DD read permission is missing.
- Count and select use the same filters; sorting uses fixed field and direction allowlists.
- Record and footer calculations are correct, including rounding and dynamic column spans.
- The form works with and without Ajax; insert switches cleanly to update.
- Confirmation “No” causes no request; an invalid token causes no mutation.
- A valid delete removes header and items together; a forced error rolls both back.
- Two report instances keep independent IDs, state, callbacks, and totals.
- DB3 and MySQL require no module-code change.
- Browser console,
files/dbxError.log, Missing counters, PHP logs, and system messages remain clean.
13. Common wrong turns
| Avoid | Use instead |
|---|---|
| PDO, mysqli, SQLite3, or inline SQL | dbxDB with a complete DD |
| Manual audit fields | Automatic dbxDB system fields |
| HTML assembled in callbacks | Values from callbacks, structure in dbxTPL |
| An internal fetch for a nested list | [modul=...]...[/modul] |
| Custom table and pagination loops | dbxReport |
| Totals calculated in JavaScript | Record callback plus add_rep() |
| Hard-coded column spans | {rpt:col_count} or {rpt:colspan} |
window.confirm() and custom fetch handlers | dbxConfirm, dbxAjax, and the system libraries |
| Action tokens on every GET | Tokens only for state-changing GET actions |