Files
php-qml/docs/qml-api.md
magdev da048434b8 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>
2026-05-02 22:18:37 +02:00

338 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.