Files
php-qml/docs/qml-api.md

338 lines
11 KiB
Markdown
Raw Normal View History

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