Files
php-qml/docs/getting-started.md

279 lines
11 KiB
Markdown
Raw Normal View History

# 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.