The README still framed the project as "Phase 5 / pre-v0.1.0" and the docs predated the v0.2.0 surface (typed BridgeOp, public service interfaces, port negotiation, pre-migration auto-backup, bridge:export, periodic auto-update, two new makers, qmltestrunner). Bring them in line with what's actually shipped, and add badges (release, license, PHP, Symfony, Qt, FrankenPHP, CI, platform) to the README so the status is legible at a glance. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8.7 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/
QML unit tests (make qmltest)
framework/qml/tests/ ships a Qt Quick Test executable target (qml_unit_tests) discovered by CTest. Built only when CMake is configured with -DBUILD_TESTING=ON, so production AppImages don't carry it.
Locally:
cd framework/skeleton # or examples/todo / a php-qml-init'd project
make qmltest
# → cmake -DBUILD_TESTING=ON -S qml -B build/qml
# → cmake --build build/qml --target qml_unit_tests
# → ctest --test-dir build/qml --output-on-failure
# ✓ tst_smoke.qml passed
Add per-feature tests next to tst_smoke.qml as tst_<feature>.qml — Quick Test auto-discovers them. Tests run under the offscreen Qt platform plugin so CI doesn't need xvfb.
This is wired into make quality (skeleton + todo example) and into the Gitea Actions Quality job after qmllint, so QML regressions fail the build alongside PHP regressions.
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.