Files
php-qml/docs/dev-workflow.md
magdev da048434b8 docs: rewrite README + add comprehensive docs/
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>
2026-05-02 22:18:37 +02:00

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:

  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/

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.