# 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`](../framework/skeleton/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. ```bash 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`](https://doc.qt.io/qt-6/qtqml-tooling-qmlls.html) 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](../PLAN.md#6-development-mode-toolchain) for the long-term plan. ## Dev console (`Ctrl+\``) The skeleton's `Main.qml` ships a hidden `DevConsole` component bound to `Ctrl+\``: ```qml 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: ```bash 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`: ```bash 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: ```bash 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.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` (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: ```bash cd framework/php vendor/bin/phpunit tests/Maker/ # diff against tests/snapshot/ ``` If you intentionally changed the template, regenerate the snapshot and commit it: ```bash ./tests/snapshot/run.sh git add tests/snapshot/ ``` ## QML unit tests (`make qmltest`) `framework/qml/tests/` ships a [Qt Quick Test](https://doc.qt.io/qt-6/qtquicktest-index.html) 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: ```bash 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_.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`: ```bash cd examples/todo make integration ``` It takes ~15 seconds and runs in CI on every push to `main`. ## Performance smoke After packaging: ```bash 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](packaging-linux.md#performance-smoke). ## See also - [Getting started](getting-started.md) — first-time setup including PHP/Qt by distro. - [Bundled mode](bundled-mode.md) — what `make dev` *doesn't* exercise (token rotation, supervisor). - [Linux packaging](packaging-linux.md) — when to switch from `make dev` to `make appimage`.