338 lines
11 KiB
Markdown
338 lines
11 KiB
Markdown
|
|
# 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.
|