README is now tight and link-heavy: 60-second tour, then deep links into docs/. The wall of detail moved out. docs/ covers the framework end-to-end: - getting-started.md — prerequisites by distro (Tumbleweed, Fedora, Debian/Ubuntu, Arch), full first-run walkthrough, troubleshooting. - architecture.md — process pair, transport, dev/bundled mode. - update-semantics.md — state machine + optimistic mutations + key round-tripping. - reactive-models.md — ReactiveListModel, ReactiveObject, Mercure dual-publish. - makers.md — make:bridge:resource/command/window. - dev-workflow.md — hot reload (PHP + QML), dev console, editor configs, bridge:doctor, snapshot/integration test loops, perfsmoke. - bundled-mode.md — supervisor, per-session secret rotation, first-launch migrations, auto-update wiring. - packaging-linux.md — make appimage, build-appimage.sh CLI, AppImageUpdate sidecar, perfsmoke budgets, release CI, bundle-size breakdown. - qml-api.md / php-api.md — exhaustive symbol reference with all Q_PROPERTY/Q_INVOKABLE/signals + every public PHP service / attribute / command. - configuration.md — every env var (host, Symfony, dev script, packaging script, perfsmoke), every CLI flag (php-qml-init, build-appimage.sh), make targets, default ports/paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7.8 KiB
Dev workflow
Day-to-day: hot reload, dev console, editor configs, bridge:doctor. The skeleton README has a short version of this; here is the long version with the why.
make dev
make dev is the canonical entry point. It:
- Runs
make build(cmake + make) for the Qt host, since QML files are baked into the binary's resource bundle. - Invokes
scripts/dev.sh:- Starts FrankenPHP under
--watchin its own process group. - Starts the Qt host with
BRIDGE_URL=http://127.0.0.1:8765andBRIDGE_TOKEN=devtoken. - Traps
SIGTERM/SIGINTand tears down both processes (per-PID, not viakill 0— that broke under tmux).
- Starts FrankenPHP under
Stop with Ctrl-C; you'll see two cleanup messages.
Hot reload — PHP side
FrankenPHP --watch does the work. Save any file under symfony/ and the next request through the host hits the new code. There's no opcache to clear and no service to restart.
Things that don't require even a save-reload:
- Editing a controller method body.
- Editing a service's body.
- Editing a Twig template (if you have any).
Things that need a make:migration + migrate:
- Adding/removing/renaming an entity field.
- Adding/removing an entity.
- Changing column types.
cd symfony
bin/console make:migration
bin/console doctrine:migrations:migrate -n
The Qt host stays up across all of this. The next time you ask Doctrine for the affected entity, the new schema is in effect.
Things that need a host restart:
- Changes to
framework/qml/src/*.cpp(C++ — needs rebuild + relink). - Changes to QML files baked into the QML cache (see below).
Hot reload — QML side
QML resides in a Qt resource bundle baked into the host binary. Saving Main.qml does not automatically flip the running window. Three workflows that do:
Qt Creator → File → Reload (Ctrl+R)
The lowest-friction option. Works on edits to existing QML files; new files need a rebuild because they have to be added to QML_FILES in CMake.
qmlls live preview
qmlls is the QML language server bundled with Qt 6.5+. With the Qt for VSCode extension or any other LSP-capable editor, you get completion, navigation, and a live preview window driven by qmlls. Edits show up instantly in the preview, no rebuild.
qmlls writes a .qmlls.ini next to your project; that file is git-ignored at the repo root.
Run from source
Set QML_IMPORT_TRACE=1 and pass -DQT_QML_DEBUG to the host build. Launch the host with Qt Creator's QML/JS Debugger attached. Save QML, the engine reloads in place.
This is more involved than the first two and we don't yet have it gated behind BRIDGE_DEV=1 — see PLAN.md §6 for the long-term plan.
Dev console (`Ctrl+``)
The skeleton's Main.qml ships a hidden DevConsole component bound to `Ctrl+``:
DevConsole {
id: devConsole
visible: false
Layout.fillWidth: true
Layout.preferredHeight: 220
}
Shortcut {
sequences: ["Ctrl+`", "Ctrl+~"]
onActivated: devConsole.visible = !devConsole.visible
}
Ctrl+\`` toggles a 220px-tall panel showing the bundled FrankenPHP child's merged stdout+stderr, scrolling live. The panel reads from a 500-line ring buffer that BackendConnection` keeps regardless of whether the panel is open, so opening it is free — no IPC ramp-up.
The console only has content in bundled mode. Dev mode (BRIDGE_URL set) shows an "observe your terminal instead" hint, since the FrankenPHP child is owned by make dev and writes to its own controlling terminal.
Ctrl+~ is provided as an alias because some keyboard layouts report the backtick as a shifted tilde and the original binding doesn't fire.
The other thing the console is good for: triggering it on a deployed AppImage to inspect why something didn't boot. The 500-line ring buffer typically catches the entire stack trace if the child died at startup.
bridge:doctor
A Symfony console command that checks the dev environment is wired up correctly:
make doctor
# → bridge:doctor
# ✓ Symfony bundle present and registered
# ✓ Mercure URL reachable
# ✓ BRIDGE_TOKEN configured
# ✓ JWT secret length (≥256 bits)
# ✓ SQLite database created
# ✓ Doctrine connection works
# ✓ No pending migrations
Add --connect to also probe the Qt host's expected BRIDGE_URL:
make doctor-connect
# additionally:
# ✓ BackendConnection probe succeeded
# ✓ Mercure subscribe + publish round-trip
Run this after every php-qml-init or whenever something looks off. It catches ~80% of the "why doesn't dev mode work?" failures before they hit the terminal.
Editor configs
Both the skeleton and examples/todo ship .vscode/ and .idea/runConfigurations/.
VSCode
.vscode/launch.json:
- Listen for Xdebug (Symfony / FrankenPHP) — attaches the debugger on port 9003. Set
XDEBUG_MODE=debugfor the FrankenPHP child:XDEBUG_MODE=debug make dev - Run skeleton (Qt host) — gdb-launches the host with dev-mode env vars. Use this when FrankenPHP is already running (e.g.
make devstarted in another terminal). - Compound: Dev: Xdebug + Qt host — runs both at once.
.vscode/tasks.json — make build, make dev, make doctor, make quality (the todo example also has make integration, make appimage).
.vscode/settings.json — file-explorer excludes for build/, vendor/, .qt/, .rcc/, etc. and per-language tab sizes. The skeleton sets intelephense.environment.phpVersion: "8.4.0" so PHP IntelliSense doesn't flag 8.4-only syntax.
PhpStorm / IntelliJ
.idea/runConfigurations/:
make dev,make doctor,make quality(andmake appimagein the todo example) as Shell run configs that execute in PhpStorm's terminal.
PhpStorm's Xdebug listener is global, not per-project. Toggle it via the toolbar's Start Listening for PHP Debug Connections button.
.idea/.gitignore is set up so per-IDE state (workspace.xml, etc.) is ignored while shared run configs are tracked.
Why both VSCode and PhpStorm
A php-qml app is multi-language: Symfony controllers (PHP), QML, C++ host code. Different editors win at different parts:
- VSCode with Qt+PHP extensions: best PHP↔QML cross-edit experience.
- PhpStorm: superior PHP refactoring, debugger, Symfony plugin.
Ship both so people start with a working setup either way; let them remove the one they don't use.
Snapshot test loop
Tweaking a maker template? The snapshot test in CI catches accidental drift. Locally:
cd framework/php
vendor/bin/phpunit tests/Maker/
# diff against tests/snapshot/
If you intentionally changed the template, regenerate the snapshot and commit it:
./tests/snapshot/run.sh
git add tests/snapshot/
Integration test loop
examples/todo/tests/integration.sh boots the example app in dev mode, fires a real HTTP+SSE round-trip plus a crash-recover, and asserts the output. Run it after touching anything in BackendConnection, MercureClient, or ReactiveListModel:
cd examples/todo
make integration
It takes ~15 seconds and runs in CI on every push to main.
Performance smoke
After packaging:
cd examples/todo
make appimage
make perf # asserts §11 budgets against build/Todo-x86_64.AppImage
Budgets:
- Bundle ≤ 200 MB
- Cold start ≤ 2 s (4 s on shared CI runners)
- Idle RSS ≤ 200 MB
Fail-loud: any breach exits non-zero and CI flags it. See Linux packaging.
See also
- Getting started — first-time setup including PHP/Qt by distro.
- Bundled mode — what
make devdoesn't exercise (token rotation, supervisor). - Linux packaging — when to switch from
make devtomake appimage.