# Persistence and Scenario Editors (Backend) Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Build the plain-PHP backend that lets an anonymous browser visitor author, validate, save, and start a scenario, with secure image uploads and a CSRF-protected, escape-everything web stack.
**Architecture:** A front controller (`public/index.php`) dispatches every request to a small `Http\Router` that routes by HTTP method + path to a `Http\Handlers\*` class. Handlers parse the request through a `Http\Request` value object, do their work through `Application\*` services (which know the `Domain`), and return a `Http\Response` rendered by `Views\*` PHP templates. The browser owns persistence via `localStorage`; the server only ever sees form POSTs and image uploads, never the assembled `Scenario` JSON.
**Tech Stack:** PHP 8.3, PHPUnit 11, PHPStan level 6, PHP_CodeSniffer PSR-12, plain `php -S` for local dev. The frontend toolchain (ESLint airbnb/base + Prettier) and JS files land in Plan 3b.
## Global Constraints
These apply to every task and are copied verbatim from the design spec.
- Anonymous browser persistence: scenarios and matches live in the browser's `localStorage` (`scenario:{id}` and `match:current`); the server never stores game state.
- No user accounts, no cross-device, no server-side storage of scenarios or matches. The server only keeps per-session CSRF state and uploaded image files.
- Built-in dev server only: `php -S 0.0.0.0:8000 -t public` (documented in README). No Apache-only assumptions; the `var/uploads/.htaccess` and `var/uploads/index.php` sentinels cover both Apache deployments and the built-in server.
- Every state-changing form or request uses and verifies a CSRF token before mutation. Tokens are per-session, signed with the app's secret using `hash_hmac('sha256', …)`. Forms use a hidden `_csrf` field; fetch uses an `X-CSRF-Token` header.
- All rendered output is escaped via `Escape::html`, `Escape::attr`, and `Escape::url` helpers. Code review enforces the pattern; PHPStan has no rule for it.
- All uploaded images are content-validated by reading the first 12 bytes and checking the magic number against the allowlist (PNG, JPEG, WebP, GIF), then `getimagesizefromstring()` confirms dimensions ≤ 512×512, then size is checked at ≤ 2 MB. The original filename and declared MIME are discarded after validation.
- `Content-Security-Policy: default-src 'self'; img-src 'self' data:; style-src 'self'` on every page. `X-Content-Type-Options: nosniff` and `Referrer-Policy: same-origin` on every page.
- Bundled placeholders are committed under `public/assets/placeholders/`; user uploads live in `var/uploads/` (git-ignored).
- PHP follows the repository PHPCS rules and passes PHPStan at level 6.
- The small JS surface passes ESLint `airbnb/base` and Prettier.
- Composer configuration passes `composer validate --strict`.
- Lint, static analysis, unit and integration tests, the end-to-end smoke test, and Composer validation are required CI checks. The MVP is releasable only when all checks pass.
- Domain code has no dependency on HTTP, sessions, storage, or browser code (unchanged from Plans 1 and 2).
## File Structure
```text
src/
├── Application/
│ ├── ScenarioSerializer.php JSON ↔ Scenario (used by both editors and the start-match endpoint)
│ ├── ScenarioDraft.php mutable draft state used to build a Scenario from form input
│ ├── ImageValidator.php content-sniff + size + dimension checks
│ └── ImageUploadService.php validates + stores a user-uploaded image; returns a stable URL
├── Http/
│ ├── Request.php value object wrapping $_GET, $_POST, $_FILES, $_SERVER
│ ├── Response.php value object with status, headers, body
│ ├── Router.php tiny path → handler dispatcher
│ ├── CsrfToken.php per-session token store + verify helper
│ ├── Escape.php html(), attr(), url() helpers
│ └── Handlers/
│ ├── GetHomePage.php
│ ├── GetTeamEditor.php
│ ├── PostTeamEditor.php
│ ├── GetBattlefieldEditor.php
│ ├── PostBattlefieldEditor.php
│ ├── GetAssets.php
│ ├── PostImageUpload.php
│ └── PostStartMatch.php
├── Views/
│ ├── layout.php
│ ├── home.php
│ ├── team-editor.php
│ ├── battlefield-editor.php
│ ├── match-stub.php
│ └── upload-result.php
└── public/
├── index.php front controller
└── assets/
└── styles.css
├── storage.js
└── grid-editor.js
var/ git-ignored, runtime-writable
├── phpunit/
├── phpstan/
└── uploads/
├── .htaccess Deny from all (Apache)
├── index.php Sentinel that returns 403 (built-in dev server)
└── {userToken}/ Per-browser namespace; created on first upload
tests/
├── Unit/
│ ├── Application/
│ │ ├── ScenarioSerializerTest.php
│ │ ├── ScenarioDraftTest.php
│ │ ├── ImageValidatorTest.php
│ │ └── ImageUploadServiceTest.php
│ └── Http/
│ ├── CsrfTokenTest.php
│ ├── EscapeTest.php
│ ├── RequestTest.php
│ └── RouterTest.php
└── Integration/
├── GetHomePageTest.php
├── GetTeamEditorTest.php
├── PostTeamEditorTest.php
├── GetBattlefieldEditorTest.php
├── PostBattlefieldEditorTest.php
├── GetAssetsTest.php
├── PostImageUploadTest.php
└── PostStartMatchTest.php
```
## Delivery Sequence
Plan 3 is the third of four plans. It is split into:
- **Plan 3a (this plan, the PHP backend):** Tasks 1-14. The web app is fully working and `curl`-able; tests cover all backend behavior.
- **Plan 3b (the frontend, separate plan):** the small JS surface (`public/js/storage.js`, `public/js/grid-editor.js`), the placeholder images, and the ESLint/Prettier toolchain. The editor pages render with HTML-only submit and `localStorage` write today; Plan 3b adds the fetch-based battlefield grid and the JS UX.
This plan covers 1-14 below. Plan 3b is drafted and executed after this one lands.
1. Toolchain and infrastructure (Task 1)
2. Application layer (Tasks 2-4)
3. Http layer (Tasks 5-7)
4. Editor pages and the front controller (Tasks 8-12)
5. Integration tests and end-to-end plumbing (Task 13)
6. CI updates (Task 14)
7. Final whole-branch review
## Execution Preflight
Execute this plan in an isolated worktree on `feature/persistence-backend`, branched from `develop`. The implementation branch will be submitted as a pull request into `develop` only after every completion check is green.
### Task 1: Toolchain and infrastructure
**Files:**
- Create: `var/uploads/.htaccess`
- Create: `var/uploads/index.php`
- Create: `public/index.php`
- Create: `public/assets/styles.css`
- Modify: `.gitignore`
- Modify: `composer.json`
- Modify: `phpcs.xml`
- Modify: `phpstan.neon`
- Modify: `phpunit.xml`
**Interfaces:**
- Produces: a public document root that the built-in dev server can serve; an expanded PHPCS/PHPStan/PHPUnit scope that covers the new layers; an updated `.gitignore` that excludes `var/uploads/*` but commits `var/uploads/.htaccess` and `var/uploads/index.php`. The Node toolchain (ESLint + Prettier) and JS files land in Plan 3b.
- [ ] **Step 1: Update `.gitignore`**
Append a force-include rule for the upload sentinels so the `var/uploads/.htaccess` and `var/uploads/index.php` files are committed even though the rest of `var/uploads/` is ignored.
Replace `.gitignore` with:
```
.worktrees/
/vendor/
/var/cache/
/var/phpunit/
/var/phpstan/
/var/logs/
/var/uploads/*
!/var/uploads/.htaccess
!/var/uploads/index.php
/node_modules/
/public/js/*.map
.phpunit.result.cache
.phpunit.cache
```
- [ ] **Step 2: Create `var/uploads/.htaccess`**
Create `var/uploads/.htaccess`:
```
Require all denied
Deny from all
```
- [ ] **Step 3: Create `var/uploads/index.php`**
Create `var/uploads/index.php`:
```php
BattleForge coding standards.srctestspublic/index.php
```
- [ ] **Step 6: Update `phpstan.neon` to include the new layers and exclude templates**
Replace `phpstan.neon` with:
```neon
parameters:
level: 6
paths:
- src
- tests
- public/index.php
excludePaths:
- src/Views/*
tmpDir: var/phpstan
```
- [ ] **Step 7: Update `phpunit.xml` to add an integration test suite**
Replace `phpunit.xml` with:
```xml
tests/Unittests/Integrationsrc
```
- [ ] **Step 8: Create `public/index.php` (front controller stub)**
The full front controller lands in Task 13. This step creates the file with a placeholder so the document root exists and `php -S` can serve it.
Create `public/index.php`:
```php
*/
public static function htmlProvider(): iterable
{
yield 'plain' => ['hello', 'hello'];
yield 'less-than' => ['