| 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.