The README still framed the project as "Phase 5 / pre-v0.1.0" and the docs predated the v0.2.0 surface (typed BridgeOp, public service interfaces, port negotiation, pre-migration auto-backup, bridge:export, periodic auto-update, two new makers, qmltestrunner). Bring them in line with what's actually shipped, and add badges (release, license, PHP, Symfony, Qt, FrankenPHP, CI, platform) to the README so the status is legible at a glance. 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://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/<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. (Bundled-mode AppImages don't share this failure mode — they negotiate a free ephemeral port at launch; see [Bundled mode §port negotiation](bundled-mode.md#port-negotiation).)
|
||
|
||
### `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.
|