Files
php-qml/docs/getting-started.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

279 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.56.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.