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>
279 lines
11 KiB
Markdown
279 lines
11 KiB
Markdown
# 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 <https://github.com/php/frankenphp/releases>:
|
||
|
||
```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://gitea.example/<you>/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/<id>` — entity topic, watched by `ReactiveObject`.
|
||
- **`/api/<name>` CRUD controller** — `GET /api/todos`, `POST /api/todos`, `GET/PATCH/DELETE /api/todos/<id>`.
|
||
- **`<Name>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/<name>/<name>.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(<target> URI <Pascal> …)` 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.
|