| name | tech-php |
| description | PHP (Slim) server-rendered monolith stack (stack id `php`): idiomatic structure, the per-feature fragment-file pattern (route/schema/seed/nav), where features wire in, the shared layout + HTML partials, and the token-CSS styling tool. Invoke when the lab's stack is `php`.
|
tech-php skill
The lab is a server-rendered PHP (Slim) monolith, one container. Every page
is rendered by render_page() through one shared, auth-aware layout. No SPA, no
build step.
Umbrella rule: a feature only CREATES its own files
A feature author creates only its own per-feature files and NEVER edits a
shared file — never public/index.php, never init_db.php, never
src/auth.php. There are no "add tables / seed / nav marker" edits anymore. The
whole feature is four small files you drop under per-feature paths, all
auto-discovered at boot:
src/routes/<feature>.php — the route(s) (a file returning a closure).
src/schema/<feature>.sql — the feature's CREATE TABLE IF NOT EXISTS ….
src/seed/<feature>.sql — the feature's INSERT OR IGNORE … rows.
src/nav/<feature>.json — the feature's nav link
({"label","href","slot","group"?}).
Parallel feature agents never touch the same file, so there are no conflicts.
Use the same <feature> slug for all four files.
File map (already in $ARENA_WORKDIR/app/)
public/index.php — Slim entry. Auth routes, /api/health, the loader that
auto-includes every feature route file, and an auth-branched / home: when
signed out it renders a working sign-in form (already wired to
/api/auth/login); when signed in it renders the post-login landing with an
<!-- AGENT HOME CONTENT --> marker. The skeleton themes the home copy and
fills that marker; no stage other than the skeleton ever edits this file —
features add routes by dropping src/routes/<feature>.php.
src/routes/ — one file per feature. This is where your route(s) go.
src/schema/ — one *.sql per feature (CREATE TABLE IF NOT EXISTS …).
Applied at boot in sorted order; _base.sql (skeleton skeleton tables) first.
src/seed/ — one *.sql per feature (INSERT OR IGNORE …). Re-runs each
boot, so use INSERT OR IGNORE (idempotent).
src/nav/ — one *.json per feature: {"label", "href", "slot", "group"?}.
render_page() renders the sorted union, so the nav grows automatically. The
optional "group" string folds related features into one dropdown.
src/views.php — render_card($title, $bodyHtml) and
render_table($headers, $rows) HTML partials (chrome only; you control
escaping — see below).
init_db.php — creates the base users table, applies every
src/schema/*.sql then every src/seed/*.sql, and seeds users. The only
thing the skeleton writes here is user seeding (it needs PHP's
password_hash()). Features never edit this file — they drop schema/seed
SQL fragments instead.
src/db.php — PDO connection + db_fetchone/db_fetchall/db_execute.
SQLite is already concurrency-safe here (WAL + busy_timeout) and the
container runs a multi-worker php -S, so concurrent requests — including
race/timing vulnerabilities — work out of the box. Don't re-tune
PRAGMAs, journal_mode, busy_timeout, or PHP_CLI_SERVER_WORKERS.
src/auth.php — current_user() and render_page($title, $content, $brand),
the single shared layout. It owns the one nav (auth-aware: signed-out shows only
"Sign in"; signed-in shows the user + working "Sign out") and links
/app.css. The feature nav links are read from src/nav/*.json fragments via
load_nav() — features never edit this file.
public/app.css — the token-driven sheet with the AGENT THEME TOKENS block.
Scaffold (skeleton stage)
The php template is already copied into app/ — do NOT copy it. The
skeleton only fills configuration:
- Seed users in
init_db.php at the // --- AGENT (skeleton): seed users below --- marker (this is the only skeleton-only file edit — it needs
password_hash()). Skeleton-wide base tables go in src/schema/_base.sql.
- Set the per-run brand (the
$brand arg to render_page) and theme tokens in
public/app.css (see design-token-css).
- The
/ home is already auth-branched with a working sign-in (see file
map). Don't rebuild the sign-in form, and don't show post-login content to
signed-out visitors — the signed-out page is the sign-in form only (a
logged-out page that leaks signed-in views is a sanity hard-fail). Theme the
sign-in/welcome copy, and put the signed-in landing content (e.g. a short
dashboard or list) at the <!-- AGENT HOME CONTENT --> marker.
Where a feature wires in (feature stage)
A feature is four new files, one per concern, all named with the same
<feature> slug. Open or edit nothing else — never public/index.php,
init_db.php, or src/auth.php, and never re-read the router / db init / layout
to "find where things go". Everything below is auto-discovered:
- Route —
src/routes/<feature>.php returns a closure registering the
route(s). public/index.php loads every file in src/routes/ automatically.
Use untyped closure params ($req, $res, $args) so you need no use
imports — render_page / render_card / render_table / db_* /
current_user are all global and already loaded. Return
render_page($title, $html, $brand) so the page gets the shared nav, auth
state, and styling; build $html with the render_card / render_table
partials.
- Schema —
src/schema/<feature>.sql — CREATE TABLE IF NOT EXISTS … ;.
init_db.php applies it at boot; files apply in sorted order with _base.sql
first.
- Seed —
src/seed/<feature>.sql — INSERT OR IGNORE … VALUES … ;. This
re-runs every boot, so make it idempotent with INSERT OR IGNORE.
- Nav —
src/nav/<feature>.json —
{"label": "Orders", "href": "/orders", "slot": 20}. render_page() renders
the sorted union of all nav fragments (lower slot sorts first; ties by
label). Add an optional "group": "Trips" to collapse related features into
one dropdown (group position = min member slot); omit it for a top-level link.
See references/writing-php.md for complete,
copy-pasteable contents of all four files.
Common pitfalls
- Never edit a shared file (
public/index.php, init_db.php, src/auth.php)
— a feature is only its own four <feature>.* fragments.
- Never hand-roll a second nav, a full
<html> doc, or a <style> — always
render through render_page(); theme by editing public/app.css, not per-page
CSS.
- A route file must
return function ($app) { ... }; — a file that returns
nothing breaks the loader.
- Seed with
INSERT OR IGNORE — src/seed/*.sql re-runs on every boot, so a
plain INSERT would error or duplicate rows.
- Use the same
<feature> slug for the route, schema, seed, and nav files.
- Gate non-anon routes behind the session
current_user(); redirect to / when
it is null.
- The
render_* partials don't escape your content slots. For clean / supporting
features, escape DB/user values with htmlspecialchars() yourself. Only leave a
value raw when the feature's vulnerability is that unescaped output.
Styling
Use the token CSS system — invoke the design-token-css skill. Theme per-run
by editing only the AGENT THEME TOKENS block in public/app.css.