toga-ai 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.json +119 -0
- package/.claude-plugin/marketplace.json +87 -0
- package/.claude-plugin/plugin.json +22 -0
- package/CLAUDE.md +161 -0
- package/README.md +72 -0
- package/agents/framework-pattern-checker.md +67 -0
- package/agents/harness-optimizer.md +102 -0
- package/agents/knowledge-writer.md +62 -0
- package/agents/php-build-resolver.md +51 -0
- package/agents/php-reviewer.md +51 -0
- package/agents/planner.md +88 -0
- package/agents/session-capture.md +101 -0
- package/agents/sql-reviewer.md +67 -0
- package/contexts/dev.md +43 -0
- package/contexts/research.md +49 -0
- package/contexts/review.md +37 -0
- package/knowledge/1.0/apps/library/INDEX.md +5 -0
- package/knowledge/1.0/apps/library/architecture.md +105 -0
- package/knowledge/1.0/apps/worker/INDEX.md +5 -0
- package/knowledge/1.0/apps/worker/architecture.md +223 -0
- package/knowledge/1.0/standards/backend-php.md +450 -0
- package/knowledge/2.0/apps/_underscore/INDEX.md +6 -0
- package/knowledge/2.0/apps/_underscore/architecture.md +183 -0
- package/knowledge/2.0/apps/_underscore/features/recursive-item-fulfillments.md +111 -0
- package/knowledge/2.0/apps/api2/INDEX.md +5 -0
- package/knowledge/2.0/apps/api2/architecture.md +162 -0
- package/knowledge/2.0/apps/worker2/INDEX.md +6 -0
- package/knowledge/2.0/apps/worker2/architecture.md +127 -0
- package/knowledge/2.0/apps/worker2/features/creating-worker-actions.md +135 -0
- package/knowledge/2.0/standards/backend-php.md +710 -0
- package/knowledge/CONVENTIONS.md +117 -0
- package/knowledge/INDEX.md +19 -0
- package/knowledge/clients/.gitkeep +0 -0
- package/knowledge/registry.json +7 -0
- package/knowledge.js +384 -0
- package/mcp-configs/README.md +72 -0
- package/mcp-configs/mcp-servers.json +23 -0
- package/package.json +50 -0
- package/rules/README.md +53 -0
- package/rules/common/coding-style.md +123 -0
- package/rules/common/git-workflow.md +72 -0
- package/rules/common/security.md +118 -0
- package/rules/common/testing.md +74 -0
- package/rules/php/app-framework.md +104 -0
- package/rules/php/underscore-framework.md +111 -0
- package/scripts/harness.js +605 -0
- package/scripts/hooks/evaluate-session.js +55 -0
- package/scripts/hooks/post-edit-validate.js +102 -0
- package/scripts/hooks/session-end.js +13 -0
- package/scripts/hooks/session-start.js +57 -0
- package/scripts/install.js +611 -0
- package/scripts/pre-commit +46 -0
- package/skills/capture/SKILL.md +294 -0
- package/skills/code-review/SKILL.md +140 -0
- package/skills/create-elastic-beanstalk/SKILL.md +217 -0
- package/skills/harness-audit/SKILL.md +152 -0
- package/skills/kickoff/SKILL.md +151 -0
- package/skills/php-patterns/SKILL.md +296 -0
- package/skills/session-resume/SKILL.md +156 -0
- package/skills/session-save/SKILL.md +158 -0
- package/skills/sync-team-skills/SKILL.md +87 -0
- package/sync-skills.js +71 -0
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Back-End Coding Standards
|
|
3
|
+
framework: "1.0"
|
|
4
|
+
project: Library
|
|
5
|
+
client: shared
|
|
6
|
+
type: standard
|
|
7
|
+
status: active
|
|
8
|
+
updated: 2026-06-08
|
|
9
|
+
owners: [jcardinal]
|
|
10
|
+
files: []
|
|
11
|
+
related:
|
|
12
|
+
- ../apps/library/architecture.md
|
|
13
|
+
- ../../2.0/standards/backend-php.md
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Back-End Coding Standards (1.0 Legacy — `App_` Framework)
|
|
17
|
+
|
|
18
|
+
> **Scope & posture.** These standards cover server-side PHP for the legacy **1.0**
|
|
19
|
+
> framework — the `App_`/`Browser_` codebase built on the **`library`** repo (the 2.0
|
|
20
|
+
> framework is `_underscore`; see its own backend standard). 1.0 is a mature, maintenance-mode
|
|
21
|
+
> codebase with **far less consistency** than 2.0. This document **describes how the legacy
|
|
22
|
+
> code actually works** so that new and modified code stays consistent with the surrounding
|
|
23
|
+
> code. Where a convention is strong and universal, follow it strictly; where the codebase is
|
|
24
|
+
> historically inconsistent, this is called out — match the local file you're editing and
|
|
25
|
+
> prefer the cleaner pattern for new code. **Do not** retrofit 2.0 conventions (UUIDs,
|
|
26
|
+
> `utf8mb4`, metadata-driven APIs) onto 1.0.
|
|
27
|
+
|
|
28
|
+
## General Principals
|
|
29
|
+
|
|
30
|
+
### Modularity and Reusability
|
|
31
|
+
|
|
32
|
+
* Ensure that the code is modular, making use of functions and classes to promote reusability and separation of concerns.
|
|
33
|
+
|
|
34
|
+
## Framework & Project Structure
|
|
35
|
+
|
|
36
|
+
### The `library` repo
|
|
37
|
+
|
|
38
|
+
* `library` is the shared code for **all 1.0 applications** — the `App_` framework. Target PHP 7.2+, compatible through 8.1.
|
|
39
|
+
* **`library/app/`** — back-end logic, class prefix `App_` (no HTML output).
|
|
40
|
+
* **`library/browser/`** — server-rendered UI, class prefix `Browser_` (emits HTML).
|
|
41
|
+
* **`resources/` repo** — purely front-end assets (JS/CSS). **Do not put front-end assets in `library`.**
|
|
42
|
+
* Consuming apps use **no Composer** — `library` *is* the vendor directory. Add third-party packages directly to `library` as top-level folders.
|
|
43
|
+
|
|
44
|
+
### Bootstrap & autoloader (`_.php`)
|
|
45
|
+
|
|
46
|
+
* Every app starts with `require_once '/path/to/library/_.php';`. It sets the PHP environment (`display_errors`, `error_reporting(E_ALL)`, timezone **`America/Chicago`**), defines `__APPROOT__` (the ancestor folder containing a `_` directory) and `__LIBROOT__` (the `library/` dir), and registers the autoloader.
|
|
47
|
+
* **Class-to-file mapping:** replace every `_` with `/`, **lowercase the entire result**, append `.php`. Both `__APPROOT__` and `__LIBROOT__` are searched, so app classes can **shadow** library classes.
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
App_Model_Client → app/model/client.php
|
|
51
|
+
Browser_Form_Input_Autocomplete → browser/form/input/autocomplete.php
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
* All folder/file names **must be lowercase**. Class names are not case-sensitive (the autoloader lowercases before lookup). Namespace separators (`\`) normalize to `_`.
|
|
55
|
+
|
|
56
|
+
### Conventions you must know
|
|
57
|
+
|
|
58
|
+
* **`debugVar($input, $stopExecution, $withHrLineBreaker)`** is the global debug dump helper. **Remove all `debugVar()` calls before merging.**
|
|
59
|
+
* **`DOA_` prefix** (on files, folders, classes, and methods) marks **deprecated/legacy code that is still in active use** ("Dead On Arrival"). Do not build new functionality on `DOA_`-prefixed code; do not assume it is safe to delete — it often still has live dependencies. Check for an explicit removal note before touching it.
|
|
60
|
+
|
|
61
|
+
## Database/SQL
|
|
62
|
+
|
|
63
|
+
> **Legacy DB reality (verified against production):** legacy schemas (`Core`, `TOGA_<client>`,
|
|
64
|
+
> `Vision`, …) are **`InnoDB`** but use the **`latin1_swedish_ci`** collation throughout — not
|
|
65
|
+
> `utf8mb4`. Tables have an **`id`** auto-increment primary key and **no `uuid` column**, **no
|
|
66
|
+
> `c_` custom-field prefix**, and **almost no column/table comments**. This is the opposite of
|
|
67
|
+
> 2.0 on those points — do not import the 2.0 rules. The naming conventions, however, are
|
|
68
|
+
> shared (PascalCase tables, camelCase fields).
|
|
69
|
+
|
|
70
|
+
### Table Design — Order of Fields
|
|
71
|
+
|
|
72
|
+
1. `id` (mandatory — auto-increment unsigned primary key)
|
|
73
|
+
2. Foreign Keys
|
|
74
|
+
3. Record Identifiers (references like order number)
|
|
75
|
+
4. Date/Time fields (legacy uses a `dt` prefix, e.g. `dtCreated`, `dtUpdated`)
|
|
76
|
+
5. Date fields
|
|
77
|
+
6. Flag/Boolean fields (`is`-prefixed)
|
|
78
|
+
7. ENUM-type fields (lists)
|
|
79
|
+
8. Char/Varchar/Integer/Decimal fields
|
|
80
|
+
9. Text/Blob fields
|
|
81
|
+
|
|
82
|
+
> Note vs 2.0: legacy has **no `uuid` second column** and **no trailing `c_` custom-field
|
|
83
|
+
> block**. The primary key alone (`id`) identifies a record.
|
|
84
|
+
|
|
85
|
+
### Primary Keys & IDs
|
|
86
|
+
|
|
87
|
+
* Every table has an `id` column: `INT UNSIGNED`, `AUTO_INCREMENT`, primary key, as the **first** column.
|
|
88
|
+
* There is no external/UUID identifier in legacy. Do not add a `uuid` column to a legacy table unless a feature explicitly requires it.
|
|
89
|
+
|
|
90
|
+
### Engine, Character Set & Collation
|
|
91
|
+
|
|
92
|
+
* All tables use the **`InnoDB`** engine.
|
|
93
|
+
* Legacy tables use **`latin1_swedish_ci`**. New tables in a legacy database should **match the collation of the database/tables they join against** — mixing `latin1` and `utf8mb4` columns in a `JOIN`/`WHERE` causes "Illegal mix of collations" errors. Do not introduce `utf8mb4_0900_ai_ci` (the 2.0 standard) into a legacy schema without confirming every joined column is migrated.
|
|
94
|
+
|
|
95
|
+
### Table & Field Comments
|
|
96
|
+
|
|
97
|
+
* Legacy tables and columns are almost never commented. Comments are **encouraged for new columns** but are not historically enforced here. Where you do comment, keep it short and descriptive and do not begin with "The".
|
|
98
|
+
|
|
99
|
+
### Queries
|
|
100
|
+
|
|
101
|
+
* `SELECT *` is fine in your MySQL client but should be avoided in code (it stresses the server and breaks when columns change). Note the framework utility `App_Database::lookupRecord()` uses `SELECT *` internally by design — that is acceptable for that generic helper, not a license to use it in feature code.
|
|
102
|
+
|
|
103
|
+
#### Capitalization
|
|
104
|
+
|
|
105
|
+
* Capitalize all SQL keywords (`SELECT`, `FROM`, `INNER JOIN`, `ON`, `WHERE`, `AND`, `OR`, `LIKE`, `AS`, `ORDER BY`, `DESC`, `LIMIT`, `GROUP BY`, `HAVING`, etc.).
|
|
106
|
+
|
|
107
|
+
```sql
|
|
108
|
+
# Bad
|
|
109
|
+
select column1 from table1
|
|
110
|
+
|
|
111
|
+
# Good
|
|
112
|
+
SELECT column1 FROM table1
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
#### Spacing & SQL within PHP
|
|
116
|
+
|
|
117
|
+
> Legacy application code is **inconsistent** about SQL formatting. Framework code mostly
|
|
118
|
+
> follows the rules below; match this style for any new or modified SQL.
|
|
119
|
+
|
|
120
|
+
1. Use tabs for indentation.
|
|
121
|
+
2. Every clause starts on a new line; indent the column list and conditions.
|
|
122
|
+
3. A line break after each comma in the `SELECT` column list.
|
|
123
|
+
4. `SELECT`, `WHERE`, `GROUP BY`, `HAVING`, `ORDER BY` on their own lines.
|
|
124
|
+
5. When writing SQL in PHP, put the opening/closing quotes on their own lines and indent the query.
|
|
125
|
+
|
|
126
|
+
```php
|
|
127
|
+
$sql = "
|
|
128
|
+
SELECT
|
|
129
|
+
column1,
|
|
130
|
+
column2
|
|
131
|
+
FROM TableName
|
|
132
|
+
WHERE
|
|
133
|
+
val > 0
|
|
134
|
+
ORDER BY
|
|
135
|
+
column1 DESC
|
|
136
|
+
LIMIT 10
|
|
137
|
+
";
|
|
138
|
+
$res = App_Database::query($sql, 'db_toga');
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
#### Aliasing, Joining, Subqueries, Conditions
|
|
142
|
+
|
|
143
|
+
* Alias tables only when necessary (e.g. self-joins or duplicate table names); use `AS` for derived columns.
|
|
144
|
+
* Each `JOIN` on its own line; put `INNER JOIN`s before outer joins; prefer `LEFT OUTER JOIN` over `RIGHT OUTER JOIN`.
|
|
145
|
+
* Enclose subqueries in parentheses, start them on a new line, and indent them.
|
|
146
|
+
* Put multiple `WHERE` conditions on separate lines; start `AND`/`OR` on a new line; **use parentheses whenever mixing `AND` and `OR`**; surround operators (`>`, `<`, `=`) with spaces.
|
|
147
|
+
|
|
148
|
+
#### SQL Comments
|
|
149
|
+
|
|
150
|
+
* Prefer `#` over `--` to begin a SQL comment; keep comments concise and place them above the code they describe.
|
|
151
|
+
|
|
152
|
+
### Encoding
|
|
153
|
+
|
|
154
|
+
* Data inserted into the database must already be decoded (not HTML- or URL-encoded) unless a column is explicitly intended to store encoded content — store the real `<` character, not `<`.
|
|
155
|
+
|
|
156
|
+
### SQL Naming Conventions
|
|
157
|
+
|
|
158
|
+
These conventions are shared with 2.0 and **do hold** across legacy schemas.
|
|
159
|
+
|
|
160
|
+
* **Case:** database names PascalCase or their formal name (e.g. `TOGA`), table names **PascalCase**, field names **camelCase**.
|
|
161
|
+
* **Foreign keys:** take the referenced table, singularize it, append `Id`, camelCase it — `Clients` → `clientId`, `PurchaseOrders` → `purchaseOrderId`. (Legacy FKs are `INT UNSIGNED`.)
|
|
162
|
+
* **Human-readable field:** call it `name` — not `label`, `type`, `displayName`, or `companyName`.
|
|
163
|
+
* **Bridge tables:** join the two table names with an underscore, parent first, keeping plurals aligned with the real table names (e.g. `Locations_LocationAttributes`).
|
|
164
|
+
|
|
165
|
+
### SQL Data Types via `App_Model`
|
|
166
|
+
|
|
167
|
+
In code you rarely write raw column types — an `App_Model` declares each column as a `FIELDTYPE_*` constant that maps to the underlying type. The 1.0 constants (note the `FIELDTYPE_` prefix; 2.0 uses `FIELD_`):
|
|
168
|
+
|
|
169
|
+
| Constant | Purpose |
|
|
170
|
+
|---|---|
|
|
171
|
+
| `FIELDTYPE_PRIMARYKEY` / `FIELDTYPE_FOREIGNKEY` | unsigned int key columns |
|
|
172
|
+
| `FIELDTYPE_INT` / `FIELDTYPE_INT_AUTONUMBER` | integers |
|
|
173
|
+
| `FIELDTYPE_BOOLEAN` | `is`-flag tinyint |
|
|
174
|
+
| `FIELDTYPE_DECIMAL` | decimals |
|
|
175
|
+
| `FIELDTYPE_CHAR` / `FIELDTYPE_CHAR_CAPS` / `FIELDTYPE_CHAR_UCWORDS` / `FIELDTYPE_CHAR_HTMLENTITY` / `FIELDTYPE_CHAR_PASSWORD` / `FIELDTYPE_PHONE` | string variants |
|
|
176
|
+
| `FIELDTYPE_LIST` | ENUM/list |
|
|
177
|
+
| `FIELDTYPE_DATE` / `FIELDTYPE_DATETIME` / `FIELDTYPE_DATETIME_CREATED` / `FIELDTYPE_DATETIME_UPDATED` / `FIELDTYPE_TIME` / `FIELDTYPE_YEAR` | date/time (CREATED/UPDATED auto-managed) |
|
|
178
|
+
| `FIELDTYPE_BLOB` / `FIELDTYPE_BLOBSTORAGE` | binary / external blob storage |
|
|
179
|
+
| `FIELDTYPE_SERIALIZED` | PHP-serialized payload |
|
|
180
|
+
|
|
181
|
+
#### Flags/Booleans
|
|
182
|
+
|
|
183
|
+
* Flag fields begin with `is` (e.g. `isVisible`) and use `FIELDTYPE_BOOLEAN` (`UNSIGNED TINYINT`, `0` = false, `1` = true).
|
|
184
|
+
|
|
185
|
+
#### Integers
|
|
186
|
+
|
|
187
|
+
* Primary and foreign keys must be **unsigned** integers. Choose the smallest integer type that fits the range; use unsigned unless negative values are genuinely required.
|
|
188
|
+
|
|
189
|
+
### SQL Injection Prevention
|
|
190
|
+
|
|
191
|
+
* Never interpolate raw user input into a query string. In order of preference:
|
|
192
|
+
1. **Use the `App_Model` layer** — it builds and escapes queries for you.
|
|
193
|
+
2. **When writing SQL by hand, escape every interpolated value with `App_Database::sqlEscape()`** before placing it in the query string. In `browser/` action files the helper `getVarEscaped('field')` wraps `sqlEscape()` for request values.
|
|
194
|
+
* `App_Database::sqlEscape()` escapes a value for safe interpolation (and handles arrays/objects recursively). `App_Database::sqlProtect()` is a **stricter, lossy filter** that *strips* `% \ / * " '` and the literal ` or` — use it only to sanitize free-form search input, never for values you intend to store intact.
|
|
195
|
+
* Legacy uses **mysqli** (via `App_Database`), not PDO/prepared statements; escaping discipline at the call site is the control.
|
|
196
|
+
|
|
197
|
+
```php
|
|
198
|
+
// Bad — raw interpolation
|
|
199
|
+
$sql = "SELECT id FROM Users WHERE email = '$email'";
|
|
200
|
+
|
|
201
|
+
// Good — escape before interpolating
|
|
202
|
+
$email = App_Database::sqlEscape($email);
|
|
203
|
+
$sql = "
|
|
204
|
+
SELECT
|
|
205
|
+
id
|
|
206
|
+
FROM Users
|
|
207
|
+
WHERE
|
|
208
|
+
email = '$email'
|
|
209
|
+
";
|
|
210
|
+
|
|
211
|
+
// Better — let the model layer handle it
|
|
212
|
+
$user = new App_Model_User();
|
|
213
|
+
// ...set lookup fields and load...
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## PHP
|
|
217
|
+
|
|
218
|
+
### PHP Tags
|
|
219
|
+
|
|
220
|
+
* Short tags (`<?`) are **not** permitted — always `<?php`.
|
|
221
|
+
* PHP files **MUST NOT** end with a closing `?>` tag (prevents accidental output of trailing whitespace).
|
|
222
|
+
|
|
223
|
+
### File & Class Naming
|
|
224
|
+
|
|
225
|
+
* Files and folders are **all lowercase**; the path mirrors the class name with `_` → `/` (see autoloader above).
|
|
226
|
+
* Back-end logic classes are prefixed `App_`; server-rendered UI classes are prefixed `Browser_`.
|
|
227
|
+
|
|
228
|
+
### Code Formatting
|
|
229
|
+
|
|
230
|
+
#### Indentation
|
|
231
|
+
|
|
232
|
+
* Use **tabs** for indentation, not spaces. (Universal in this codebase.)
|
|
233
|
+
|
|
234
|
+
#### Braces
|
|
235
|
+
|
|
236
|
+
* Opening braces for classes, methods, and control structures go on the **same line**.
|
|
237
|
+
|
|
238
|
+
```php
|
|
239
|
+
if ($condition) {
|
|
240
|
+
// code
|
|
241
|
+
} else {
|
|
242
|
+
// code
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Naming Conventions
|
|
247
|
+
|
|
248
|
+
* **Variables:** camelCase, descriptive (`$primaryKeyId`, `$fieldChanges`).
|
|
249
|
+
* **Methods/functions:** camelCase (`buildModel()`, `getChangedFields()`).
|
|
250
|
+
* **Constants:** `UPPERCASE_SNAKE_CASE` (`FIELDTYPE_PRIMARYKEY`, `TABLENAME`, `MODE_EDIT`).
|
|
251
|
+
* **Classes:** PascalCase with the `App_`/`Browser_` prefix.
|
|
252
|
+
|
|
253
|
+
### Positional Arguments (not named)
|
|
254
|
+
|
|
255
|
+
* 1.0 targets PHP 7.2 and the codebase uses **positional arguments only** — do **not** use PHP 8 named arguments (`substr(string: $x)`) here. (This is the opposite of the 2.0 standard.)
|
|
256
|
+
|
|
257
|
+
```php
|
|
258
|
+
// Correct for 1.0
|
|
259
|
+
$piece = substr($value, 0, 8);
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Documentation
|
|
263
|
+
|
|
264
|
+
* **DocBlocks:** framework (`library`) classes/methods are well documented with a description and `@param`/`@return`/`@throws`. Application code is less consistent — document new and modified methods to the framework standard.
|
|
265
|
+
* **Comments:** explain the "why", not the "how"; use inline comments sparingly.
|
|
266
|
+
|
|
267
|
+
### Performance
|
|
268
|
+
|
|
269
|
+
* Use single quotes for strings unless interpolation is needed (`'Hello'`, `"Hello $world"`, `'Hello ' . $world`). Note: legacy code is pragmatic and mixed here; prefer the rule for new code.
|
|
270
|
+
|
|
271
|
+
### Miscellaneous
|
|
272
|
+
|
|
273
|
+
* Use `require_once` for include-once files. Avoid global variables and global state.
|
|
274
|
+
|
|
275
|
+
## App_Model layer (the 1.0 ORM)
|
|
276
|
+
|
|
277
|
+
Each DB table has a model under `app/model/` extending `App_Model`.
|
|
278
|
+
|
|
279
|
+
```php
|
|
280
|
+
class App_Model_Client extends App_Model {
|
|
281
|
+
const TABLENAME = 'Clients';
|
|
282
|
+
const DATABASE = 'db_toga';
|
|
283
|
+
|
|
284
|
+
public $id = self::FIELDTYPE_PRIMARYKEY;
|
|
285
|
+
public $clientName = self::FIELDTYPE_CHAR;
|
|
286
|
+
public $companyId = array(self::FIELDTYPE_FOREIGNKEY, 'App_Model_Company'); // FK → parent model
|
|
287
|
+
public $active = array(self::FIELDTYPE_BOOLEAN, 1); // [type, default]
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
* Declare the table via `TABLENAME` and the connection via `DATABASE`.
|
|
292
|
+
* Declare each column as a public property set to a `FIELDTYPE_*` constant, **or** an array `[FIELDTYPE_*, default]`; foreign keys may carry the parent model name as a second element.
|
|
293
|
+
* CRUD / instance methods: `save($okayToLog = true, ...)` (insert or update by PK), `delete()`, `buildModel()` (load by PK — the constructor calls it when given an id), `getChangedFields()`.
|
|
294
|
+
* Static helpers: `lookupLabel($id)`, `lookupValue($id, $field)`, `exists($id)`, `fetchCount($conditions)`, `deleteWhere($include, $exclude)`.
|
|
295
|
+
* Models may also define UI-binding constants (`ACTION`, `VIEW`, `LISTING`, `TABS`) tying the model to its `browser/` screens.
|
|
296
|
+
* **Client-specific behavior** lives in `app/client/` overrides — keep tenant logic there, not in the base model.
|
|
297
|
+
|
|
298
|
+
## App_Database layer
|
|
299
|
+
|
|
300
|
+
`App_Database` is a static wrapper over **mysqli** with result caching and query helpers. Connections are addressed by a link-registry string (e.g. `'db_toga'`, `'db_catalog'`).
|
|
301
|
+
|
|
302
|
+
* **Run a query:** `App_Database::query($sql, $linkRegistryString = '')`.
|
|
303
|
+
* **Read results:** `fetchRow(&$res)`, `fetchOne(&$res, $rowIndex, $colIndexOrField)`, `numRows(&$res)`.
|
|
304
|
+
* **Writes:** `affectedRows($link)`, `getInsertId($link)`.
|
|
305
|
+
* **Cached lookups:** `lookupField($table, $aryLookup, $returnField, $link, $throwIfNotFound)`, `lookupRecord($table, $idValue, $link)`.
|
|
306
|
+
* **Escaping:** `sqlEscape()` (safe interpolation) and `sqlProtect()` (strict, lossy filter) — see SQL Injection Prevention.
|
|
307
|
+
|
|
308
|
+
## browser/ — Server-Rendered UI
|
|
309
|
+
|
|
310
|
+
The `browser/` tree renders HTML server-side. Components are `Browser_*` classes in lowercase files (`Browser_Datagrid` → `browser/datagrid.php`, `Browser_Form_Input_Text` → `browser/form/input/text.php`).
|
|
311
|
+
|
|
312
|
+
### Core components
|
|
313
|
+
|
|
314
|
+
| Component | Purpose |
|
|
315
|
+
|---|---|
|
|
316
|
+
| `Browser_Datagrid` | Table/list view with filtering, sorting, pagination, bulk edit (140+ subclasses in `datagrid/`) |
|
|
317
|
+
| `Browser_Form` + `Browser_Form_Input_*` | Form builder and field elements (Text, Textarea, Select, Checkbox, Datepicker, Autocomplete, Button, …) |
|
|
318
|
+
| `Browser_Dialog` | Modal dialogs |
|
|
319
|
+
| `Browser_ButtonBar` + `Browser_ButtonBar_Button_*` | Button toolbars (Save, Cancel, Edit, Delete, Import, …) |
|
|
320
|
+
| `Browser_Breadcrumb` | Navigation trail |
|
|
321
|
+
| `Browser_Sidebar` | Sidebar modules |
|
|
322
|
+
| `Browser_Widget` | Dashboard widgets |
|
|
323
|
+
| `Browser_Status` | Status badges (40+ subclasses in `status/`) |
|
|
324
|
+
| `Browser_Message` | Alert/message boxes |
|
|
325
|
+
|
|
326
|
+
### Page pattern — action.php + view.php
|
|
327
|
+
|
|
328
|
+
A screen is a folder under an app's `app/` tree with paired files:
|
|
329
|
+
|
|
330
|
+
* **`action.php`** — reads `$_REQUEST`/mode (`MODE_NEW`, `MODE_EDIT`), validates (`Browser_Form_Validator::isValid()`), and writes to the DB, then redirects/sets state.
|
|
331
|
+
* **`view.php`** — instantiates `Browser_*` components and renders them. Optional `tabs.php`, `summary.php`, `listing.php`.
|
|
332
|
+
|
|
333
|
+
```php
|
|
334
|
+
// view.php (abridged)
|
|
335
|
+
$buttonBar = new Browser_ButtonBar();
|
|
336
|
+
$buttonBar->addButton(new Browser_ButtonBar_Button_Save());
|
|
337
|
+
|
|
338
|
+
$form = new Browser_Form();
|
|
339
|
+
$form->setAction(linkProductBundle($id, array('mode' => $mode), '..._action'));
|
|
340
|
+
$form->registerButtonBar($buttonBar);
|
|
341
|
+
$form->renderFormHeader();
|
|
342
|
+
$form->groupHeader('Product Bundle Information');
|
|
343
|
+
|
|
344
|
+
$element = new Browser_Form_Input_Text('name');
|
|
345
|
+
$element->setLabel('Name');
|
|
346
|
+
$element->setRequired(true);
|
|
347
|
+
echo $element->renderField();
|
|
348
|
+
|
|
349
|
+
$form->groupFooter();
|
|
350
|
+
$form->renderFormFooter();
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### Datagrid pattern
|
|
354
|
+
|
|
355
|
+
Subclass `Browser_Datagrid` and configure it in the constructor — columns are added via `addField()`, not declared as properties:
|
|
356
|
+
|
|
357
|
+
```php
|
|
358
|
+
class Browser_Datagrid_Accounts extends Browser_Datagrid {
|
|
359
|
+
public function __construct() {
|
|
360
|
+
parent::__construct('accounts');
|
|
361
|
+
$this->setTitle('Accounts');
|
|
362
|
+
$this->setTableJoins("
|
|
363
|
+
FROM Companies
|
|
364
|
+
LEFT OUTER JOIN Users AS AccountManagers ON ...
|
|
365
|
+
");
|
|
366
|
+
$this->setViewLink(linkCompany('!Companies.id!')); // !Field! = row-value placeholder
|
|
367
|
+
$this->addField('active', 'Active', 'Companies.active', self::FILTERABLE,
|
|
368
|
+
array('style' => self::STYLE_BOOLEAN));
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
* Column display uses `STYLE_*` constants (`STYLE_DATE`, `STYLE_CURRENCY`, `STYLE_BOOLEAN`, `STYLE_STOPLIGHT`, …).
|
|
374
|
+
* Filterability via `self::FILTERABLE` / `self::SEARCHABLE`.
|
|
375
|
+
* `!TableAlias.column!` placeholders are replaced per-row when building links.
|
|
376
|
+
|
|
377
|
+
### Rendering & output
|
|
378
|
+
|
|
379
|
+
* Components render by **echoing HTML directly** (`render()` / `renderField()`), mixing PHP and HTML via inline `?> … <?php` fragments and heredocs. There is no separate template engine.
|
|
380
|
+
* For a string instead of direct output, components use `App_Page::startCapture()` / `App_Page::stopCapture()` (output buffering); many `render*` methods accept a `$renderAsString` flag.
|
|
381
|
+
|
|
382
|
+
### HTML escaping (a known weak spot — do better in new code)
|
|
383
|
+
|
|
384
|
+
* Input values are escaped with `App_String::htmlEscapeQuotes()` / `htmlentities()` in some elements, **but escaping is inconsistent** across the layer (textarea, dialog, and datagrid-injected JS frequently emit unescaped strings).
|
|
385
|
+
* **For new/modified UI code, escape all output that contains user/DB data** (`App_String::htmlEntity()` / `htmlEscapeQuotes()`), especially anything interpolated into HTML attributes or inline `<script>`.
|
|
386
|
+
|
|
387
|
+
### browser/ structure
|
|
388
|
+
|
|
389
|
+
* Lowercase files organized by component type: `datagrid/`, `form/`, `form/input/`, `dialog/`, `status/`, `sidebar/`, `buttonbar/`, `buttonbar/button/`, `widget/`.
|
|
390
|
+
|
|
391
|
+
## Error Handling
|
|
392
|
+
|
|
393
|
+
* Throw exceptions for unexpected conditions: `throw new Exception('message')` is the prevailing pattern (an `App_Exception` class exists but is rarely used for throwing — match the surrounding file).
|
|
394
|
+
* Catch and handle exceptions where you can add value; provide meaningful messages.
|
|
395
|
+
* The `@` error-suppression operator is acceptable only for defensive file I/O (e.g. reading an optional cache file); do not use it to hide real errors.
|
|
396
|
+
* Use structured logging to capture application behavior and errors.
|
|
397
|
+
|
|
398
|
+
## Security Best Practices
|
|
399
|
+
|
|
400
|
+
### Input Validation and Sanitization
|
|
401
|
+
|
|
402
|
+
* Validate and sanitize all user input (see SQL Injection Prevention and HTML escaping above).
|
|
403
|
+
|
|
404
|
+
### Secure Communication
|
|
405
|
+
|
|
406
|
+
* Always use HTTPS to encrypt data in transit.
|
|
407
|
+
|
|
408
|
+
### Authentication and Authorization
|
|
409
|
+
|
|
410
|
+
* Use the framework's `App_Auth` / `App_Acl` for authentication and role-based access control; do not roll your own session/permission checks.
|
|
411
|
+
|
|
412
|
+
## Performance Optimization
|
|
413
|
+
|
|
414
|
+
### Caching
|
|
415
|
+
|
|
416
|
+
* `App_Database` provides built-in result caching for repeated lookups (`lookupField`/`lookupRecord`). Reuse it rather than re-running identical reads. Store per-request reusable data in variables; use sessions/cookies for cross-request client state.
|
|
417
|
+
|
|
418
|
+
### Asynchronous / Background Processing
|
|
419
|
+
|
|
420
|
+
* For long-running or background work, hand off to the legacy worker tier (the `worker` / `worker1.5` apps) rather than running long tasks inline in a web request. Use AJAX for front-end interactions.
|
|
421
|
+
|
|
422
|
+
## API Design
|
|
423
|
+
|
|
424
|
+
### Outbound / integration APIs (`App_Api`)
|
|
425
|
+
|
|
426
|
+
* Third-party integrations live under `app/api/` as `abstract class App_Api_<Vendor> extends App_Api`, with static methods and an `initialize()` for setup; results are returned via `App_ApiResult`.
|
|
427
|
+
* RESTful principles apply when *we* expose endpoints, but many legacy partner/3rd-party APIs are not REST — follow the partner's contract.
|
|
428
|
+
|
|
429
|
+
> Note: the 2.0 metadata-driven API features (Record Scripts, API Payload Interceptors,
|
|
430
|
+
> `/v2` engine) **do not exist in 1.0**. Do not reference them in legacy code.
|
|
431
|
+
|
|
432
|
+
## Logging and Monitoring
|
|
433
|
+
|
|
434
|
+
* Use structured logging with relevant context. Field/record change logging is available via the model layer (`App_FieldLog`) — keep `save()` logging enabled unless there's a reason not to.
|
|
435
|
+
* Implement monitoring/alerting to catch issues early (the `systemmonitor/` helpers support this).
|
|
436
|
+
|
|
437
|
+
## Configuration Management
|
|
438
|
+
|
|
439
|
+
* Manage configuration through `App_Config` and per-environment config files. Keep secrets out of source where possible.
|
|
440
|
+
|
|
441
|
+
## Code Quality and Maintainability
|
|
442
|
+
|
|
443
|
+
* Establish a thorough code review process.
|
|
444
|
+
* **Remove all `debugVar()` calls before merging.**
|
|
445
|
+
* Don't extend `DOA_`-prefixed code; plan its removal when you touch the area and dependencies allow.
|
|
446
|
+
|
|
447
|
+
## Documentation
|
|
448
|
+
|
|
449
|
+
* Keep code well-documented with DocBlocks and usage examples.
|
|
450
|
+
* Maintain clear API documentation for endpoints we expose; use Postman for API testing/storage where applicable.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# _underscore (_Underscore) — 2.0 knowledge
|
|
2
|
+
|
|
3
|
+
| Doc | Summary | Files |
|
|
4
|
+
|-----|---------|-------|
|
|
5
|
+
| [_underscore Framework Architecture](architecture.md) | `_underscore` is the shared PHP backend framework for **all 2.0 applications**. | _underscore/_underscore.php, _underscore/Loader.php, _underscore/Framework.php, _underscore/Model.php, _underscore/Database.php, _underscore/Query.php, _underscore/Route.php, _underscore/Component.php |
|
|
6
|
+
| [Recursive Item Fulfillments (upstream mirroring)](features/recursive-item-fulfillments.md) | In a multi-tier supply chain a sales order (SO) spawns a purchase order (PO) that becomes another SO downstream, and so on. | _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/ItemFulfillmentItem.php, _underscore/Model/Client/ItemFulfillmentItemUnit.php, _underscore/Model/Client/ItemFulfillmentPackage.php, _underscore/Model/Compass/AdvanceShippingNotice.php, dbchanges2/Core/2026-02-13 - 75601 - RecursiveItemFulfillmentCreation.sql, dbchanges2/Core/2026-06-04 - RecursiveItemFulfillmentPut.sql |
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: _underscore Framework Architecture
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: _underscore
|
|
5
|
+
project: _Underscore
|
|
6
|
+
client: shared
|
|
7
|
+
type: architecture
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-06-08
|
|
10
|
+
owners: [jcardinal]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/_underscore.php
|
|
13
|
+
- _underscore/Loader.php
|
|
14
|
+
- _underscore/Framework.php
|
|
15
|
+
- _underscore/Model.php
|
|
16
|
+
- _underscore/Database.php
|
|
17
|
+
- _underscore/Query.php
|
|
18
|
+
- _underscore/Route.php
|
|
19
|
+
- _underscore/Component.php
|
|
20
|
+
related: []
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Summary
|
|
24
|
+
|
|
25
|
+
`_underscore` is the shared PHP backend framework for **all 2.0 applications**. It
|
|
26
|
+
provides database abstraction, ORM-style models, routing, component rendering,
|
|
27
|
+
configuration, ACL, and third-party integrations. Every 2.0 project depends on it.
|
|
28
|
+
Minimum PHP 8.1 (enforced in `_underscore.php`). The current architecture is
|
|
29
|
+
**decoupled**: React handles the frontend; `_underscore` serves as the backend API layer.
|
|
30
|
+
|
|
31
|
+
## Entry point & boot sequence
|
|
32
|
+
|
|
33
|
+
Every 2.0 project's `index.php` is just `<?php require '_underscore.php';`.
|
|
34
|
+
|
|
35
|
+
1. `_underscore.php` — PHP version check, require `Initialize.php`
|
|
36
|
+
2. `Initialize.php` — sets `_START_TIME`, loads interfaces, `Time.php`, `Loader.php`
|
|
37
|
+
3. `Loader.php::initialize()` — registers the SPL autoloader, then in order:
|
|
38
|
+
`_Error::initialize()` → `_Environment::initialize()` → `_Config::initialize()`
|
|
39
|
+
(parses INI) → `_Time::initialize()` → **`_underscore::initialize()`** (project hook,
|
|
40
|
+
required) → `_Session::initialize()` → `_Route::initialize()` (web only).
|
|
41
|
+
|
|
42
|
+
## Naming convention & autoloader
|
|
43
|
+
|
|
44
|
+
Class names map **directly** to file paths: replace each `_` with `/`; the last segment
|
|
45
|
+
is the `.php` filename. **Case-sensitive.**
|
|
46
|
+
|
|
47
|
+
| Class | File |
|
|
48
|
+
|---|---|
|
|
49
|
+
| `_Model_Client_User` | `Model/Client/User.php` |
|
|
50
|
+
| `_Model_Compass_SalesOrder` | `Model/Compass/SalesOrder.php` |
|
|
51
|
+
| `_Component_Ui_Button` | `Component/Ui/Button.php` |
|
|
52
|
+
|
|
53
|
+
The autoloader resolves framework root first, then the project namespace. External
|
|
54
|
+
libraries register via `_Framework::registerLibrary()`. There is **no Composer** for
|
|
55
|
+
core PHP deps; frontend libs (Bootstrap 5.1.3, Font Awesome 6) are vendored in
|
|
56
|
+
`Component/Library/`.
|
|
57
|
+
|
|
58
|
+
## Database architecture
|
|
59
|
+
|
|
60
|
+
Built for multi-tenant, multi-database apps. Connections register via
|
|
61
|
+
`_Database::register()`; well-known categories:
|
|
62
|
+
|
|
63
|
+
| Constant | Description |
|
|
64
|
+
|---|---|
|
|
65
|
+
| `_underscore::DB_CORE` | Shared infra DB — clients, domains, environments, ACL, parameters. Models in `Model/Core/`. |
|
|
66
|
+
| `_underscore::DB_CLIENT` | Per-client business data; each client has an isolated DB. Models in `Model/Client/`. |
|
|
67
|
+
| `_underscore::DB_LOGS` | Core-level logging (non-client). |
|
|
68
|
+
| `_underscore::DB_CLIENT_LOGS` | Per-client logging / audit trails. |
|
|
69
|
+
|
|
70
|
+
`Database.php` handles named connections with read/write replica separation, **lazy
|
|
71
|
+
transactions** (a transaction starts only on first write), result caching, and
|
|
72
|
+
multi-tenant client-DB lookup from Core. `Query.php` wraps MySQLi with read/write
|
|
73
|
+
auto-routing and result helpers: `fetchRows()`, `fetchRow()`, `fetchOne()`,
|
|
74
|
+
`fetchRowsAssocArray()`, `fetchKeyValuePairs()`, `fetchValues()`.
|
|
75
|
+
|
|
76
|
+
## Model layer
|
|
77
|
+
|
|
78
|
+
Each DB table has a model. Every model must define:
|
|
79
|
+
|
|
80
|
+
```php
|
|
81
|
+
const DATABASE = _underscore::DB_CLIENT; // or DB_CORE
|
|
82
|
+
const TABLE = 'TableName';
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Columns are declared as `public` properties set to field-type constants. Numeric
|
|
86
|
+
(`FIELD_PRIMARYKEY`, `FIELD_FOREIGNKEY`, `FIELD_INTEGER`, `FIELD_BOOLEAN`,
|
|
87
|
+
`FIELD_DECIMAL`…), string (`FIELD_CHAR`, `FIELD_CHAR_CAPS`, `FIELD_CHAR_UUID`,
|
|
88
|
+
`FIELD_LIST`…), date/time (`FIELD_DATE`, `FIELD_DATETIME`, `FIELD_DATETIME_CREATED`
|
|
89
|
+
auto-set on insert, `FIELD_DATETIME_UPDATED` auto-set on save), binary
|
|
90
|
+
(`FIELD_BLOB`, `FIELD_STORAGE` for S3/filesystem), and calculated (`FIELD_SQL`).
|
|
91
|
+
|
|
92
|
+
**Foreign keys** are arrays with the target model:
|
|
93
|
+
|
|
94
|
+
```php
|
|
95
|
+
public $salesOrderId = [
|
|
96
|
+
self::FIELD_FOREIGNKEY,
|
|
97
|
+
self::FIELDOPT_FOREIGNKEY_MODEL => '\_Model_Client_SalesOrder'
|
|
98
|
+
];
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Calculated (SQL) fields
|
|
102
|
+
|
|
103
|
+
`FIELD_SQL` declares a computed field (always prefixed `_`). A static method of the
|
|
104
|
+
same name returns a raw SQL expression used inline in queries:
|
|
105
|
+
|
|
106
|
+
```php
|
|
107
|
+
public $_extPrice = [self::FIELD_SQL, self::FIELDOPT_SQL_TYPE => self::FIELD_DECIMAL];
|
|
108
|
+
public static function _extPrice($table = self::TABLE) {
|
|
109
|
+
return "($table.quantity * $table.price)";
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`FIELDOPT_SQL_STORED => true` persists the result to a real column on save (must be
|
|
114
|
+
refreshed after bulk migrations); default `false` recalculates on every query.
|
|
115
|
+
|
|
116
|
+
### CRUD
|
|
117
|
+
|
|
118
|
+
```php
|
|
119
|
+
$po = new _Model_Team_PullRequest(); // create
|
|
120
|
+
$po->title = $title; $po->save();
|
|
121
|
+
|
|
122
|
+
$po = new _Model_Team_PullRequest(123); // update by PK
|
|
123
|
+
$po->title = $new; $po->save();
|
|
124
|
+
|
|
125
|
+
$v = new _Model_Client_TableView(); // load single
|
|
126
|
+
$v->slug = $slug; $v->isActive = true;
|
|
127
|
+
if ($v->load()) { /* found */ }
|
|
128
|
+
|
|
129
|
+
$s = new _Model_Client_State(); // search many
|
|
130
|
+
$s->name = $name; $states = $s->search();
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Client-specific model extensions
|
|
134
|
+
|
|
135
|
+
`Model/Client/` holds base models for all clients. A client needing custom logic gets a
|
|
136
|
+
directory (`Model/Compass/`, `Model/Aig/`, `Model/Canon/`) with classes extending the
|
|
137
|
+
base — e.g. `_Model_Compass_SalesOrder extends _Model_Client_SalesOrder`. Supports
|
|
138
|
+
deeply nested sub-clients (`Model/Compass/Canada/`). **This is the multi-tenant
|
|
139
|
+
customization hook — client behavior lives here, not in the API engine.**
|
|
140
|
+
|
|
141
|
+
## Interceptors
|
|
142
|
+
|
|
143
|
+
Static methods on a model that intercept API calls before/after the HTTP method:
|
|
144
|
+
`[pre|post][HttpMethod](&$api, &$payload)` — e.g. `postPost`, `prePut`, `preDelete`.
|
|
145
|
+
Both args by reference; interceptors **do not return** — they mutate `$payload` and/or
|
|
146
|
+
perform side effects. `$api` carries user identity, JWT, client info, headers, route.
|
|
147
|
+
|
|
148
|
+
## Scripted APIs & internal requests
|
|
149
|
+
|
|
150
|
+
A **Scripted API** is a custom static method acting as a full endpoint; first arg is
|
|
151
|
+
`&$api`, subsequent args come from the query string, and it **must return** a value
|
|
152
|
+
(JSON-encoded as the response). `$api->internalApiRequest($method, $route, $payload,
|
|
153
|
+
$options)` invokes another endpoint in-process (no HTTP round trip), reusing auth/ACL —
|
|
154
|
+
heavily used by interceptors and NetSuite traits.
|
|
155
|
+
|
|
156
|
+
## Traits
|
|
157
|
+
|
|
158
|
+
`Trait/` composes shared model functionality: `Trait/Netsuite/` (22 ERP-sync traits:
|
|
159
|
+
SalesOrder, Invoice, Item, Customer, …), `Trait/Startech/`, `Trait/Ai/Bdr/`,
|
|
160
|
+
`Trait/NonStatic/` (`PrePost`, `Href`, `Toggle`, `ViewDataGetSet`), `Trait/Static/`.
|
|
161
|
+
|
|
162
|
+
## Routing & components
|
|
163
|
+
|
|
164
|
+
`Route.php` parses the URI to a controller method via reflection: URI segments
|
|
165
|
+
(kebab-case → camelCase) become candidate method names; parameter names use the `inp`
|
|
166
|
+
prefix by default (`sales-orders` → `$inpSalesOrders`). `Component.php` renders
|
|
167
|
+
multi-file UI components (`.php`/`.html`/`.css`/`.js`) invoked as `<_ComponentName>` tags.
|
|
168
|
+
|
|
169
|
+
## Configuration
|
|
170
|
+
|
|
171
|
+
`Config/[environment].ini`, accessed via `_Config::database('username')`,
|
|
172
|
+
`_Config::debug('show_queries')`. Groups: `[_underscore]`, `[database]`, `[debug]`,
|
|
173
|
+
`[api]` (per-hostname).
|
|
174
|
+
|
|
175
|
+
## Key abstractions
|
|
176
|
+
|
|
177
|
+
| Class | Purpose |
|
|
178
|
+
|---|---|
|
|
179
|
+
| `_Framework` | Abstract base all projects extend; `initialize()`, `pre()`, `post()`, DB/folder constants |
|
|
180
|
+
| `_Model` / `_Model_True` | Base ORM (field types, CRUD, FK traversal, SQL fields) |
|
|
181
|
+
| `_Database` / `_Query` | Connection pool + read/write routing / MySQLi wrapper with caching |
|
|
182
|
+
| `_Route` / `_Component` | URI router / HTML component renderer |
|
|
183
|
+
| `_Config` / `_Environment` / `_Loader` / `_Http` / `_Error` | config, env/debug, autoload, JWT/HTTP, errors |
|