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