Files
php-qml/docs/dev-workflow.md
magdev beb4e3ab9d docs: refresh README + docs/ for v0.2.0
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>
2026-05-03 22:27:52 +02:00

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:

  1. Runs make build (cmake + make) for the Qt host, since QML files are baked into the binary's resource bundle.
  2. Invokes scripts/dev.sh:
    • Starts FrankenPHP under --watch in its own process group.
    • Starts the Qt host with BRIDGE_URL=http://127.0.0.1:8765 and BRIDGE_TOKEN=devtoken.
    • Traps SIGTERM / SIGINT and tears down both processes (per-PID, not via kill 0 — that broke under tmux).

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=debug for 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 dev started in another terminal).
  • Compound: Dev: Xdebug + Qt host — runs both at once.

.vscode/tasks.jsonmake 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 (and make appimage in 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 dev doesn't exercise (token rotation, supervisor).
  • Linux packaging — when to switch from make dev to make appimage.