Phase 3 sub-commit 3: examples/todo POC app, built via the makers
Standalone Composer/CMake project under examples/todo/ derived from the
skeleton, demonstrating every Phase 3 architectural primitive in a
non-trivial app. All cross-side wiring is maker-generated; no
handwritten bridge glue.
Generated and customised:
- src/Entity/Todo.php — make:bridge:resource Todo (UUIDv7 id)
- src/Controller/TodoController.php — make:bridge:resource Todo (CRUD)
- src/Controller/MarkAllDoneController.php — make:bridge:command
MarkAllDone, body filled in to flip done=true on every row
- qml/TodoList.qml — make:bridge:resource Todo (starter ListView)
- qml/TodoWindow.qml — make:bridge:window Todo, body customised to
embed a read-only mirror of the same ReactiveListModel
The Phase 1 ping demo is dropped from this app — it doesn't fit the
todo flow and nothing in Main.qml references it.
Main.qml is the real list UI:
- Add input + button (POST /api/todos with optimistic-friendly key).
- Per-row CheckBox + delete button (PATCH/DELETE via
todoModel.invoke() with `pending` role driving opacity).
- "Mark all done" button (POST /api/mark-all-done).
- "Open second window" button (Component { TodoWindow {} } pattern).
Build / run delegated to the same Makefile shape as the skeleton, with
SCRIPT_DIR/QT_BIN updated for the renamed binary (build/qml/todo).
composer.json's path repo points at ../../../framework/php (one level
deeper than the skeleton's path repo).
Verified end-to-end with offscreen QPA: POST/PATCH/DELETE on /api/todos
all round-trip, /api/mark-all-done flips every row, Mercure dual-
publishes on every change. Clean shutdown.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
81
examples/todo/README.md
Normal file
81
examples/todo/README.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# examples/todo
|
||||
|
||||
The framework's POC application: a real Qt/QML todo app whose backend is a Symfony service generated entirely by the `make:bridge:*` makers. Demonstrates every architectural primitive in PLAN.md §13 Phase 3.
|
||||
|
||||
## What's here that wasn't in the skeleton
|
||||
|
||||
- `Todo` resource (entity + controller + QML snippet) generated via `make:bridge:resource Todo`.
|
||||
- `MarkAllDone` command generated via `make:bridge:command MarkAllDone`, with a body that flips `done = true` on every todo.
|
||||
- `TodoWindow.qml` second-window scaffold generated via `make:bridge:window Todo`, customised to embed a read-only mirror of the same `ReactiveListModel`.
|
||||
- `Main.qml` rewritten as a real list UI with add input, per-row toggle/delete, "mark all done", and "open second window".
|
||||
|
||||
Everything cross-side (PHP ↔ QML) is maker-generated. There is no handwritten bridge glue in this example.
|
||||
|
||||
## Run it
|
||||
|
||||
```bash
|
||||
make install # composer install
|
||||
make build # cmake + qt host
|
||||
make doctor # bridge:doctor — readiness check
|
||||
make dev # FrankenPHP --watch + Qt host
|
||||
```
|
||||
|
||||
Add a few todos, toggle them, click "Mark all done", click "Open second window" — both windows stay in sync.
|
||||
|
||||
## Multi-window test
|
||||
|
||||
1. `make dev` starts the app.
|
||||
2. Add three todos via the input field.
|
||||
3. Click **Open second window**. A `Todos (mirror)` window opens.
|
||||
4. In the main window, toggle one of the todos. The mirror flips its row within ~50 ms (Mercure SSE).
|
||||
5. Add a new todo in the main window. It appears in the mirror within ~50 ms.
|
||||
6. Click **Mark all done** in the main window. Both windows show all rows ticked simultaneously.
|
||||
|
||||
Each window has its own `ReactiveListModel` instance subscribed to the same `app://model/todo` topic; the framework keeps them coherent without per-window glue.
|
||||
|
||||
## Crash-and-recover test
|
||||
|
||||
1. `make dev` is running.
|
||||
2. Add a todo so the list is non-empty.
|
||||
3. From another terminal: `pkill -f 'examples/todo/symfony.*frankenphp'`.
|
||||
4. The app's `AppShell` shows the **Reconnecting** banner (visible on the second window; the main window keeps its own status display).
|
||||
5. Restart the FrankenPHP child: from the example dir, `cd symfony && frankenphp run --watch --config ../Caddyfile` — or just re-run `make dev`.
|
||||
6. The app flips back to **Online** and re-fetches the list. No row corruption.
|
||||
|
||||
## Layout
|
||||
|
||||
```text
|
||||
todo/
|
||||
Caddyfile # FrankenPHP / Mercure config (port 8765, anonymous SSE)
|
||||
Makefile # build / dev / doctor / quality
|
||||
scripts/dev.sh
|
||||
symfony/
|
||||
composer.json # path repo points one level deeper at framework/php
|
||||
config/
|
||||
src/Entity/Todo.php # generated by make:bridge:resource
|
||||
src/Controller/TodoController.php # generated, CRUD on /api/todos
|
||||
src/Controller/MarkAllDoneController.php # generated, body filled in
|
||||
public/index.php
|
||||
bin/console
|
||||
.env
|
||||
migrations/
|
||||
qml/
|
||||
CMakeLists.txt
|
||||
main.cpp
|
||||
Main.qml # real todo UI (add / toggle / delete / mark-all / 2nd-window)
|
||||
TodoList.qml # generated; also reused by the second window
|
||||
TodoWindow.qml # generated, customised to embed mirror list
|
||||
```
|
||||
|
||||
## What this example proves
|
||||
|
||||
- The headline workflow scales: a non-trivial app's PHP/QML wiring is **all generated**.
|
||||
- `ReactiveListModel` handles in-flight optimistic mutations (`pending` role drops opacity on toggled rows until the Mercure echo lands).
|
||||
- Two windows of the same QML app stay coherent under mutations from either side.
|
||||
- Stopping FrankenPHP mid-session triggers `Reconnecting` → `Offline` UI; restart restores `Online` and re-fetches without dupes.
|
||||
- `Idempotency-Key` round-trips through to Mercure as `correlationKey` — visible in `dev.log` if you `grep correlationKey`.
|
||||
|
||||
## Out of scope here
|
||||
|
||||
- A CI-driven version of the multi-window / crash-recover tests lives in Phase 3 sub-commit 4 as a bridge-integration suite.
|
||||
- No persistence options (export, backup) — same SQLite `var/data.sqlite` as the skeleton. Apps move to Postgres by overriding `DATABASE_URL`.
|
||||
Reference in New Issue
Block a user