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>
11 KiB
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/ if you need to read them.
import PhpQml.Bridge
| Symbol | Kind | Purpose |
|---|---|---|
BackendConnection |
Singleton | App lifecycle, dev/bundled mode, connection state, auto-update. |
RestClient |
Component (.qml) | Promise-style HTTP wrapper. |
MercureClient |
Component (C++) | SSE subscription with auto-reconnect. |
ReactiveListModel |
Component (C++) | Mercure-fed list model. |
ReactiveObject |
Component (C++) | Mercure-fed single-entity twin. |
AppShell |
Component (.qml) | Reconnecting banner + Offline overlay. |
DevConsole |
Component (.qml) | Bundled child stdout/stderr viewer. |
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
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
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
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 MercureClients. |
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
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 for the conceptual writeup.
Properties
| Property | Type | Notes |
|---|---|---|
baseUrl / token / source / topic |
string | Same as documented in Reactive models. |
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. |
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 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:
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
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 whenevervisibleflips back totrue. - Listens for
BackendConnection.childLogLineto 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:
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
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
- Configuration reference
- Update semantics for what these primitives implement.