# php-qml skeleton The framework's reference application: a minimal Symfony backend plus a Qt/QML host, wired together exactly the way `php-qml/bridge` is intended to be used. Use this as a starting point or copy parts into your own project. ## Prerequisites - Linux (other platforms land in Phase 4) - Qt 6.5+ dev packages (`qt6-base-devel`, `qt6-declarative-devel`, `qt6-quickcontrols2-devel`, `qt6-tools-devel`), CMake, gcc-c++ - PHP 8.3+ - [FrankenPHP](https://frankenphp.dev/) on PATH (or set `FRANKENPHP=/path/to/frankenphp`) - Composer ## First run ```bash make install # composer install in symfony/ make build # cmake + qt host make doctor # bridge:doctor โ€” readiness check make dev # FrankenPHP --watch + Qt host, tears both down on Ctrl-C ``` `make dev` opens a Qt window, status dots flip to green, and the Ping button round-trips through `/api/ping` and back via Mercure SSE. ## Adding a reactive resource The headline workflow โ€” add a Doctrine entity that ends up reactive in QML with **three commands** and zero handwritten glue: ```bash cd symfony # 1) Generate entity + controller + QML snippet bin/console make:bridge:resource Todo # created: src/Entity/Todo.php # #[BridgeResource] + UUIDv7 # created: src/Controller/TodoController.php # created: ../qml/TodoList.qml # 2) Schema migration (Doctrine reads the entity) bin/console make:migration bin/console doctrine:migrations:migrate -n # 3) Use the generated TodoList.qml from your QML window. ``` That's it. The bundle's Doctrine subscriber automatically dual-publishes `postPersist`/`postUpdate`/`postRemove` events to: - `app://model/todo` (collection topic, watched by `ReactiveListModel`) - `app://model/todo/{id}` (entity topic, watched by `ReactiveObject` โ€” Phase 3) The `ReactiveListModel` in `TodoList.qml` does an initial `GET /api/todos`, subscribes to the collection topic, and applies diffs to its rows as they arrive. Adding `--int-id` switches the maker to auto-incrementing integer IDs. ### Verifying end-to-end With `make dev` running, post a todo from another terminal and watch it appear: ```bash curl -X POST http://127.0.0.1:8765/api/todos \ -H 'Authorization: Bearer devtoken' \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: my-key-1' \ -d '{"title":"buy milk","done":false}' ``` The Mercure SSE stream receives a `correlationKey: my-key-1` envelope which the Qt host's `ReactiveListModel` matches against any in-flight optimistic mutation (PLAN.md ยง5). ## Quality checks ```bash make quality # PHPStan + php-cs-fixer (check) + PHPUnit + qmllint ``` The same set CI runs (`.gitea/workflows/ci.yml`). ## Layout ```text skeleton/ Caddyfile # FrankenPHP / Mercure config (dev mode) Makefile # build / dev / doctor / quality scripts/dev.sh # process-group teardown for FrankenPHP + Qt host symfony/ # the Symfony app (composer project) config/ src/Entity/ # filled by `make:bridge:resource` src/Controller/ # filled by `make:bridge:resource` public/index.php bin/console qml/ # the Qt host CMakeLists.txt main.cpp Main.qml List.qml # filled by `make:bridge:resource` (relative to symfony/) ``` ## Where to go next - The framework code lives one level up: [`framework/php`](../php) (the bundle) and [`framework/qml`](../qml) (the Qt module). Both are linked into this skeleton via Composer's path repository / CMake's `add_subdirectory`, so edits there are picked up live. - The full design story is in [`PLAN.md`](../../PLAN.md). - Phase 3 wires this skeleton into a real POC todo application with a multi-window test and packaging-CI dry runs.