# Getting started End-to-end walkthrough: install prerequisites, scaffold a project, run it, add a reactive resource, package it. Plan on ~15 minutes if FrankenPHP and Qt are already installed; ~45 minutes from a clean machine. If something doesn't work the way this page says, the [Troubleshooting](#troubleshooting) section at the bottom covers the cases we've actually hit. ## 1. Prerequisites | Tool | Minimum | Notes | | ---------- | --------- | ---------------------------------------------------------------------- | | PHP | **8.4** | Symfony 8 enforces this. Earlier PHP will fail `composer install`. | | Composer | 2.x | | | FrankenPHP | **1.12.2**| Single static binary. Either on `PATH` or pointed at via `FRANKENPHP`. | | Qt | **6.5** | LTS. We test on 6.5–6.11. | | CMake | 3.21+ | | | GCC/Clang | C++20 | `g++ ≥ 11` or `clang++ ≥ 14`. | | make | any | Plain GNU make is fine. | | git, curl, jq, rsync | any | Used by the dev / packaging scripts. | ### Install by distribution #### openSUSE Tumbleweed ```bash sudo zypper install -t pattern devel_basis devel_C_C++ sudo zypper install \ qt6-base-devel qt6-declarative-devel \ qt6-quickcontrols2-devel qt6-tools-devel qt6-quickcontrols2-imports \ cmake gcc-c++ git rsync curl jq \ php8 php8-cli php8-pdo php8-sqlite php8-mbstring php8-zip \ composer ``` #### Fedora 40+ ```bash sudo dnf install \ qt6-qtbase-devel qt6-qtdeclarative-devel \ qt6-qtquickcontrols2 qt6-qttools-devel \ cmake gcc-c++ git rsync curl jq \ php-cli php-pdo php-sqlite php-mbstring php-zip \ composer ``` #### Debian / Ubuntu (24.04+) PHP 8.4 isn't in vanilla Ubuntu yet — use [Ondřej Surý's PPA](https://launchpad.net/~ondrej/+archive/ubuntu/php). ```bash sudo add-apt-repository ppa:ondrej/php sudo apt update sudo apt install \ qt6-base-dev qt6-declarative-dev qt6-quickcontrols2-dev qt6-tools-dev \ libqt6opengl6-dev libqt6quick6 libqt6quickcontrols2-6 \ cmake g++ git rsync curl jq \ php8.4-cli php8.4-pdo php8.4-sqlite3 php8.4-mbstring php8.4-zip php8.4-curl \ composer ``` #### Arch Linux ```bash sudo pacman -S \ qt6-base qt6-declarative qt6-quickcontrols2 qt6-tools \ cmake gcc git rsync curl jq \ php php-sqlite \ composer ``` ### FrankenPHP Grab the static linux/amd64 binary from : ```bash curl -fsSL -o /tmp/frankenphp \ https://github.com/php/frankenphp/releases/download/v1.12.2/frankenphp-linux-x86_64 sudo install -m 0755 /tmp/frankenphp /usr/local/bin/frankenphp frankenphp version # expect: FrankenPHP v1.12.2 … ``` Or build it from source if you need a custom PHP/extensions set; the FrankenPHP project has good build instructions. ### Verify ```bash php --version # 8.4.x composer --version frankenphp version qmake6 --version # Qt 6.5.x or newer cmake --version ``` If `qmake6` isn't on `PATH`, the dev packages are installed but `update-alternatives` (Debian) hasn't symlinked it. Use `qmake-qt6` or `/usr/lib/qt6/bin/qmake` directly when configuring CMake. ## 2. Get the framework ```bash git clone https://src.bundespruefstelle.ch/magdev/php-qml cd php-qml ``` Optional: run the framework's own quality gate to confirm your PHP toolchain is wired up correctly: ```bash cd framework/php && composer install && composer quality cd ../.. ``` ## 3. Scaffold a project [`bin/php-qml-init`](../bin/php-qml-init) is a single bash script that copies the skeleton into a new directory and rewrites every identifier (CMake project name, Qt target, QML module URI, app title, single-instance lock id, composer path-repo, CMake `add_subdirectory`). ```bash ./bin/php-qml-init my-app ``` Output you can expect: ``` → copying skeleton → /…/my-app → rewriting identifiers (snake=my_app, pascal=MyApp) → path-repo → /…/php-qml/framework/php → qml framework path → /…/php-qml/framework/qml → composer install … → first-run migrations … ✓ Scaffolded 'my-app' at /…/my-app ``` By default the new project references this framework checkout via absolute paths — edits to `framework/php` or `framework/qml` are picked up live. Pass `--vendor` to copy them into `my-app/.bridge/` and `my-app/.bridge-qml/` so the project is portable away from the checkout. Pass `--git` to `git init` and create an initial commit. See the [`php-qml-init` reference](configuration.md#php-qml-init) for the full flag list. ## 4. First run ```bash cd my-app make doctor # bridge:doctor — readiness check make dev # FrankenPHP --watch + Qt host ``` `make doctor` should print a green checklist (Doctrine connection, Mercure URL, bridge token, JWT secret, etc.). `make dev` opens the Qt window and runs FrankenPHP in worker mode in another process group. Watch for: 1. The status dot in the toolbar flips to **green** within ~1 s — connection state is `Online`. 2. The **Mercure** dot turns green — SSE subscription is live. 3. Click **Ping** — round-trip via `/api/ping` returns; the next event from Mercure shows up in the log pane within ~50 ms. `Ctrl-C` in the terminal stops both processes cleanly (`scripts/dev.sh` traps SIGTERM and tears down the process group). ## 5. Add a reactive resource The headline workflow — entity + REST controller + QML snippet — is one maker invocation: ```bash cd symfony bin/console make:bridge:resource Todo # created: src/Entity/Todo.php # #[BridgeResource] + UUIDv7 id # created: src/Controller/TodoController.php # created: ../qml/TodoList.qml bin/console make:migration bin/console doctrine:migrations:migrate -n ``` The maker's defaults: - **UUIDv7** ID. Use `--int-id` if you prefer auto-incrementing integers. - **`#[BridgeResource]` attribute** on the entity. The bundle's Doctrine subscriber sees this and auto-publishes `postPersist` / `postUpdate` / `postRemove` to two Mercure topics: - `app://model/todo` — collection topic, watched by `ReactiveListModel`. - `app://model/todo/` — entity topic, watched by `ReactiveObject`. - **`/api/` CRUD controller** — `GET /api/todos`, `POST /api/todos`, `GET/PATCH/DELETE /api/todos/`. - **`List.qml`** — a starter `ListView` bound to a `ReactiveListModel` doing the initial GET and subscribing to the collection topic. Use the generated QML from your `Main.qml`: ```qml import Todo // local module — exposes TodoList.qml TodoList { anchors.fill: parent } ``` Run `make dev` again and post a todo from another terminal: ```bash curl -X POST http://127.0.0.1:8765/api/todos \ -H 'Authorization: Bearer devtoken' \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: my-key-1' \ -d '{"title":"buy milk","done":false}' ``` It appears in the Qt window within ~50 ms via Mercure SSE. The `Idempotency-Key` round-trips back as `correlationKey` so any in-flight optimistic mutation can match the echo (see [Update semantics](update-semantics.md)). ## 6. Package as an AppImage (optional) ```bash FRANKENPHP=/usr/local/bin/frankenphp make appimage ./build/MyApp-x86_64.AppImage ``` `make appimage` produces a single ~150 MB file containing Qt, the Symfony app, the FrankenPHP binary, and an AppImageUpdate sidecar. When the AppImage runs without `BRIDGE_URL` set, `BackendConnection` switches to **bundled mode** — see [Bundled mode](bundled-mode.md) and [Linux packaging](packaging-linux.md). ## 7. What to read next - **[Architecture](architecture.md)** — what's actually happening when `make dev` runs. - **[Reactive models](reactive-models.md)** — how `ReactiveListModel` decides to upsert / delete / re-fetch. - **[Update semantics](update-semantics.md)** — when optimistic mutations roll back; what `pending` means. - **[Dev workflow](dev-workflow.md)** — hot-reload, dev console (`Ctrl+\``), editor configs. - **[Makers](makers.md)** — the rest of the maker family (`command`, `window`). ## Troubleshooting ### `make dev` exits with "frankenphp: command not found" `scripts/dev.sh` looks for `frankenphp` on `PATH` or via `FRANKENPHP=/path/to/frankenphp`. Fix one of those: ```bash which frankenphp # or FRANKENPHP=/path/to/frankenphp make dev ``` ### Qt window opens but the status dot stays red / "Reconnecting" FrankenPHP didn't bind. From another terminal: ```bash curl -i http://127.0.0.1:8765/healthz ``` - `Connection refused` — FrankenPHP didn't start. Check `tail -f symfony/var/log/dev.log` and the terminal `make dev` is running in. - `401` on `/api/*` — the host is sending the wrong bearer. In dev that's `BRIDGE_TOKEN` from `.env`; the Qt host reads it via `BackendConnection.token` which defaults to `qgetenv("BRIDGE_TOKEN")`. - Port 8765 already taken — another `make dev` is still running. `pkill -f frankenphp` and retry. ### `composer install` fails with "your php version (8.3.x) does not satisfy" Symfony 8 is PHP 8.4+. Either install PHP 8.4 (see distro instructions above) or downgrade Symfony in `composer.json` — but then several framework features (typed iterables, etc.) won't work. ### `make build` fails with "Could not find a package configuration file provided by 'Qt6'" Qt 6 dev packages aren't installed, or CMake can't find them. Try: ```bash CMAKE_PREFIX_PATH=/usr/lib/qt6 make build ``` On Debian/Ubuntu the path is typically `/usr/lib/x86_64-linux-gnu/cmake/Qt6`. ### `make:bridge:resource` exits with "no maker bundle" `composer install` finished without dev dependencies (`--no-dev`). Re-install with dev deps: ```bash cd symfony && composer install ``` The maker bundle is `require-dev`, so production AppImage builds (which use `--no-dev`) intentionally don't include it. ### `bridge:doctor --connect` reports "tokenRotated received" Cosmetic in dev mode; the bundled-mode supervisor uses that signal when restarting the FrankenPHP child. In dev mode `BRIDGE_TOKEN` is fixed and the signal never fires. ### Two windows of the same app open instead of one The single-instance lock socket is per-`SingleInstance(name)` value (which `php-qml-init` derives from the app name). If you renamed the binary between runs, stale `~/.local/share//.sock` lives on. Either remove it or just close all running instances. ### Linker error `undefined reference to qt_static_metacall` QML module wasn't picked up by `qt_add_qml_module`. Make sure your `qml/CMakeLists.txt`'s `qt_add_qml_module( URI …)` includes every `.qml` file via `QML_FILES`. ### AppImage launches but immediately exits / "no DISPLAY" CI / headless runs — the smoke harness sets `QT_QPA_PLATFORM=offscreen`. For interactive use you need a display. From a headless server, `xvfb-run -a ./MyApp-x86_64.AppImage` or `ssh -X`. If you hit something not on this list, the failure mode is usually visible in either `var/log/dev.log` (Symfony) or the terminal `make dev` is running in (FrankenPHP / Qt). Open an issue with both pasted in.