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>
This commit is contained in:
337
docs/qml-api.md
Normal file
337
docs/qml-api.md
Normal file
@@ -0,0 +1,337 @@
|
||||
# QML API reference
|
||||
|
||||
Public API exposed by the `PhpQml.Bridge` QML module. Internal helpers and slots aren't documented here — the source is at [`framework/qml/src/`](../framework/qml/src/) if you need to read them.
|
||||
|
||||
```qml
|
||||
import PhpQml.Bridge
|
||||
```
|
||||
|
||||
| Symbol | Kind | Purpose |
|
||||
| --- | --- | --- |
|
||||
| [`BackendConnection`](#backendconnection) | Singleton | App lifecycle, dev/bundled mode, connection state, auto-update. |
|
||||
| [`RestClient`](#restclient) | Component (.qml) | Promise-style HTTP wrapper. |
|
||||
| [`MercureClient`](#mercureclient) | Component (C++) | SSE subscription with auto-reconnect. |
|
||||
| [`ReactiveListModel`](#reactivelistmodel) | Component (C++) | Mercure-fed list model. |
|
||||
| [`ReactiveObject`](#reactiveobject) | Component (C++) | Mercure-fed single-entity twin. |
|
||||
| [`AppShell`](#appshell) | Component (.qml) | Reconnecting banner + Offline overlay. |
|
||||
| [`DevConsole`](#devconsole) | Component (.qml) | Bundled child stdout/stderr viewer. |
|
||||
| [`SingleInstance`](#singleinstance) | C++ object exposed via context | QLocalServer-backed lock + arg forwarding. |
|
||||
|
||||
---
|
||||
|
||||
## `BackendConnection`
|
||||
|
||||
QML singleton. Lifecycle owner: detects dev vs bundled mode at construction, supervises the FrankenPHP child in bundled mode, drives the connection state machine, and brokers the auto-update sidecar.
|
||||
|
||||
### Properties
|
||||
|
||||
| Property | Type | Notes |
|
||||
| --- | --- | --- |
|
||||
| `mode` | `Mode` enum (`Dev` \| `Bundled`) | `CONSTANT`. Auto-detected from `BRIDGE_URL`. |
|
||||
| `url` | string | Effective backend URL (e.g. `http://127.0.0.1:8765`). |
|
||||
| `token` | string | Bearer token. Static in dev mode; rotated per session in bundled mode. |
|
||||
| `connectionState` | `ConnectionState` enum | `Connecting` / `Online` / `Reconnecting` / `Offline`. |
|
||||
| `error` | string | Last reported error message; empty when healthy. |
|
||||
|
||||
### Methods
|
||||
|
||||
| Method | Description |
|
||||
| --- | --- |
|
||||
| `restart()` | Bundled mode: tear down + respawn FrankenPHP. Dev mode: re-probe. |
|
||||
| `checkForUpdates()` | Bundled mode: invoke AppImageUpdate sidecar `--check-for-update`. |
|
||||
| `applyUpdate()` | Bundled mode: invoke AppImageUpdate sidecar `--remove-old`. |
|
||||
| `childLogTail()` | Bundled mode: returns `QStringList` of last ≤500 child output lines. |
|
||||
|
||||
### Signals
|
||||
|
||||
| Signal | Notes |
|
||||
| --- | --- |
|
||||
| `urlChanged()`, `tokenChanged()`, `connectionStateChanged()`, `errorChanged()` | Property-change notifiers. |
|
||||
| `tokenRotated(QString newToken)` | Bundled mode: emitted when supervisor restarts FrankenPHP with a fresh secret. `RestClient` and `MercureClient` are wired to swap. |
|
||||
| `updatesAvailable()` | AppImageUpdate sidecar reported a newer version. |
|
||||
| `noUpdatesAvailable()` | Sidecar confirmed up-to-date. |
|
||||
| `updateCheckFailed(QString reason)` | Sidecar errored, env unset, or dev mode. |
|
||||
| `updateApplied()` | Update was downloaded and applied; user should restart. |
|
||||
| `updateApplyFailed(QString reason)` | Apply errored. |
|
||||
| `childLogLine(QString line)` | Emitted per line read from the bundled child's merged stdout+stderr. |
|
||||
|
||||
### Example
|
||||
|
||||
```qml
|
||||
Item {
|
||||
Connections {
|
||||
target: BackendConnection
|
||||
function onConnectionStateChanged() {
|
||||
console.log("state:", BackendConnection.connectionState);
|
||||
}
|
||||
function onTokenRotated(t) {
|
||||
// Bundled mode supervisor cycled the secret; existing
|
||||
// RestClient/MercureClient instances pick this up automatically.
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `RestClient`
|
||||
|
||||
Promise-style HTTP wrapper backed by `XMLHttpRequest`. Auto-attaches `Idempotency-Key` to every non-GET request. Maps `application/problem+json` error bodies into structured rejections.
|
||||
|
||||
### Properties
|
||||
|
||||
| Property | Type | Notes |
|
||||
| --- | --- | --- |
|
||||
| `baseUrl` | string | Joined with the path on every call. |
|
||||
| `token` | string | If set, sent as `Authorization: Bearer <token>`. |
|
||||
|
||||
### Methods
|
||||
|
||||
```qml
|
||||
rest.get("/path") // → Promise<{status, body, headers}>
|
||||
rest.post("/path", body) // → Promise<…>
|
||||
rest.patch("/path", body) // → Promise<…>
|
||||
rest.del("/path", body) // → Promise<…>
|
||||
```
|
||||
|
||||
Resolved value: `{ status: int, body: any, headers: string }`. Rejected value: `{ status, problem, raw, method, path }`.
|
||||
|
||||
### Example
|
||||
|
||||
```qml
|
||||
RestClient {
|
||||
id: rest
|
||||
baseUrl: BackendConnection.url
|
||||
token: BackendConnection.token
|
||||
}
|
||||
|
||||
Button {
|
||||
onClicked: {
|
||||
rest.post("/api/todos", { title: "buy milk", done: false })
|
||||
.then(function(r) { log.append("created " + r.body.id); })
|
||||
.catch(function(e) { log.append("× " + e.status); });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `MercureClient`
|
||||
|
||||
SSE subscriber for a single Mercure topic. Reconnects automatically with exponential backoff and `Last-Event-ID` to replay events that fired during the gap.
|
||||
|
||||
### Properties
|
||||
|
||||
| Property | Type | Notes |
|
||||
| --- | --- | --- |
|
||||
| `hubUrl` | string | E.g. `BackendConnection.url + "/.well-known/mercure"`. |
|
||||
| `topic` | string | Single topic per client. Multi-topic apps spawn multiple `MercureClient`s. |
|
||||
| `token` | string | Optional bearer (Mercure JWT). Empty in dev mode where Caddy allows anonymous subscribers. |
|
||||
| `active` | bool | Currently subscribed? |
|
||||
| `lastEventId` | string | Highest event id seen. Used as `Last-Event-ID` on reconnect. |
|
||||
|
||||
### Methods
|
||||
|
||||
| Method | Description |
|
||||
| --- | --- |
|
||||
| `start()` | Open the subscription. |
|
||||
| `stop()` | Close it. |
|
||||
|
||||
### Signals
|
||||
|
||||
| Signal | Notes |
|
||||
| --- | --- |
|
||||
| `update(QString data, QString id)` | Per-event payload + id. `data` is the raw SSE `data:` field (typically JSON). |
|
||||
| `error(QString detail)` | Transport-level error; auto-reconnect handles it but apps may want to log. |
|
||||
|
||||
### Example
|
||||
|
||||
```qml
|
||||
MercureClient {
|
||||
id: mercure
|
||||
hubUrl: BackendConnection.url + "/.well-known/mercure"
|
||||
topic: "app://ping"
|
||||
onUpdate: function(data, id) { log.append(data); }
|
||||
onError: function(detail) { log.append("× " + detail); }
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: BackendConnection
|
||||
function onConnectionStateChanged() {
|
||||
if (BackendConnection.connectionState === BackendConnection.Online) {
|
||||
if (!mercure.active) mercure.start();
|
||||
} else {
|
||||
if (mercure.active) mercure.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In practice you rarely instantiate `MercureClient` directly — `ReactiveListModel` and `ReactiveObject` own one each.
|
||||
|
||||
---
|
||||
|
||||
## `ReactiveListModel`
|
||||
|
||||
`QAbstractListModel` subclass that does the initial GET, subscribes to Mercure, and applies events. See [Reactive models](reactive-models.md) for the conceptual writeup.
|
||||
|
||||
### Properties
|
||||
|
||||
| Property | Type | Notes |
|
||||
| --- | --- | --- |
|
||||
| `baseUrl` / `token` / `source` / `topic` | string | Same as documented in [Reactive models](reactive-models.md#reactivelistmodel). |
|
||||
| `ready` | bool | `true` after initial GET completes. |
|
||||
| `error` | string | Last error, or empty. |
|
||||
|
||||
### Roles
|
||||
|
||||
Every JSON field on the entity becomes a role of the same name. Plus:
|
||||
|
||||
| Role | Type | Notes |
|
||||
| --- | --- | --- |
|
||||
| `pending` | bool | `true` while an optimistic mutation against this row is in flight. |
|
||||
|
||||
### Methods
|
||||
|
||||
| Method | Description |
|
||||
| --- | --- |
|
||||
| `refresh()` | Re-do the initial GET. Useful after a long offline window. |
|
||||
| `invoke(method, urlSuffix, body, optimistic)` | Optimistic mutation. See [Reactive models §invoke](reactive-models.md#methods). |
|
||||
|
||||
### Signals
|
||||
|
||||
| Signal | Notes |
|
||||
| --- | --- |
|
||||
| `commandSucceeded(key, response)` | Mutation echoed back via Mercure with matching `correlationKey`. |
|
||||
| `commandFailed(key, status, problem)` | Mutation HTTP failed; rollback applied. |
|
||||
| `commandTimedOut(key)` | Mutation HTTP succeeded but Mercure echo never arrived; model re-fetched. |
|
||||
|
||||
---
|
||||
|
||||
## `ReactiveObject`
|
||||
|
||||
Single-entity twin. Wraps a `QQmlPropertyMap` so QML accesses fields as plain properties.
|
||||
|
||||
### Properties
|
||||
|
||||
| Property | Type | Notes |
|
||||
| --- | --- | --- |
|
||||
| `baseUrl` / `token` / `source` / `topic` | string | `source` is the entity URL, `topic` is `app://model/<name>/<id>`. |
|
||||
| `data` | `QQmlPropertyMap*` | `CONSTANT`. Field access — `obj.data.title`. |
|
||||
| `ready` | bool | Initial GET done. |
|
||||
| `pending` | bool | Optimistic mutation in flight. |
|
||||
| `exists` | bool | False after the entity was deleted. |
|
||||
| `error` | string | Last error or empty. |
|
||||
|
||||
### Methods
|
||||
|
||||
| Method | Description |
|
||||
| --- | --- |
|
||||
| `refresh()` | Re-fetch. |
|
||||
| `invoke(method, urlSuffix, body, optimistic)` | Same shape as `ReactiveListModel.invoke`. |
|
||||
|
||||
### Signals
|
||||
|
||||
`commandSucceeded` / `commandFailed` / `commandTimedOut` — same contract as `ReactiveListModel`. Also `existsChanged()` so detail UIs can react to a delete arriving from another window.
|
||||
|
||||
---
|
||||
|
||||
## `AppShell`
|
||||
|
||||
Optional convenience root component that surfaces the [Update Semantics](update-semantics.md) state machine as default UI:
|
||||
|
||||
- `Reconnecting` → orange banner across the top.
|
||||
- `Offline` → modal overlay with the last error and a Retry button.
|
||||
|
||||
### Default property
|
||||
|
||||
`content` is a default property alias, so children of `AppShell` populate the inner content slot:
|
||||
|
||||
```qml
|
||||
AppShell {
|
||||
anchors.fill: parent
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
// your UI
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Skip `AppShell` if you want full control over the chrome — `BackendConnection.connectionState` is the source of truth.
|
||||
|
||||
---
|
||||
|
||||
## `DevConsole`
|
||||
|
||||
Optional in-window log viewer for the bundled FrankenPHP child's stdout+stderr. Captures passively in `BackendConnection`, so opening the console is free.
|
||||
|
||||
### Properties
|
||||
|
||||
| Property | Type | Notes |
|
||||
| --- | --- | --- |
|
||||
| `maxLines` | int | Default 500. The model trims to this. |
|
||||
| Standard `Item`/`Rectangle` properties | — | Anchors, sizing, etc. |
|
||||
|
||||
### Usage
|
||||
|
||||
```qml
|
||||
DevConsole {
|
||||
id: devConsole
|
||||
visible: false
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 220
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequences: ["Ctrl+`", "Ctrl+~"]
|
||||
onActivated: devConsole.visible = !devConsole.visible
|
||||
}
|
||||
```
|
||||
|
||||
The console:
|
||||
- Seeds from `BackendConnection.childLogTail()` on completion *and* whenever `visible` flips back to `true`.
|
||||
- Listens for `BackendConnection.childLogLine` to populate live.
|
||||
- Has Auto-scroll + Clear controls.
|
||||
- In dev mode (`BackendConnection.mode === Dev`), shows an explanatory hint instead of an empty log.
|
||||
|
||||
---
|
||||
|
||||
## `SingleInstance`
|
||||
|
||||
C++ object exposed via `QQmlContext::setContextProperty` (not a singleton — one per app), bound in `main.cpp`:
|
||||
|
||||
```cpp
|
||||
PhpQml::Bridge::SingleInstance singleInstance("my-app");
|
||||
if (!singleInstance.acquireOrForward(app.arguments())) {
|
||||
return 0; // forwarded; existing instance handles it
|
||||
}
|
||||
engine.rootContext()->setContextProperty("SingleInstance", &singleInstance);
|
||||
```
|
||||
|
||||
### Signals
|
||||
|
||||
| Signal | Notes |
|
||||
| --- | --- |
|
||||
| `launchArgsReceived(QStringList args)` | Fired in the *running* instance when a new launch forwards its `argv`. |
|
||||
|
||||
### Usage from QML
|
||||
|
||||
```qml
|
||||
Connections {
|
||||
target: SingleInstance
|
||||
function onLaunchArgsReceived(args) {
|
||||
window.requestActivate(); // show the window
|
||||
if (args.length > 1) openFile(args[1]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The lock socket lives at `~/.local/share/<name>/<name>.sock`. If the lock can't be acquired and the existing instance doesn't respond on the socket (stale file), `SingleInstance` removes it and retries — handles the typical "host crashed without cleanup" case.
|
||||
|
||||
---
|
||||
|
||||
## See also
|
||||
|
||||
- [PHP API reference](php-api.md)
|
||||
- [Configuration reference](configuration.md)
|
||||
- [Update semantics](update-semantics.md) for what these primitives implement.
|
||||
Reference in New Issue
Block a user