The README still framed the project as "Phase 5 / pre-v0.1.0" and the docs predated the v0.2.0 surface (typed BridgeOp, public service interfaces, port negotiation, pre-migration auto-backup, bridge:export, periodic auto-update, two new makers, qmltestrunner). Bring them in line with what's actually shipped, and add badges (release, license, PHP, Symfony, Qt, FrankenPHP, CI, platform) to the README so the status is legible at a glance. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
13 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. The supervisor also calls this automatically on launch (10 s after Online) and every 6 h thereafter — see Bundled mode §periodic check. |
applyUpdate() |
Bundled mode: invoke AppImageUpdate sidecar --remove-old. Never auto-restarts the app. |
exportDatabase(path) |
Q_INVOKABLE bool. Copies the active SQLite database to path; returns success synchronously and emits databaseExported(path) / databaseExportFailed(reason) for async UX. Mirrors the bridge:export console command. See below. |
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. |
databaseExported(QString path) |
exportDatabase() succeeded. |
databaseExportFailed(QString reason) |
exportDatabase() errored (non-SQLite DATABASE_URL, missing source, write failed). |
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.
}
}
}
exportDatabase
Pair with Qt.labs.platform.FileDialog so the user picks a destination natively:
import Qt.labs.platform as Platform
Platform.FileDialog {
id: saveDlg
title: "Export database"
fileMode: Platform.FileDialog.SaveFile
nameFilters: ["SQLite (*.sqlite)"]
onAccepted: BackendConnection.exportDatabase(Qt.url.toLocalFile(currentFile))
}
Connections {
target: BackendConnection
function onDatabaseExported(path) { tray.showMessage("Saved", path) }
function onDatabaseExportFailed(reason) { error.text = reason }
}
Button { text: "Export…"; onClicked: saveDlg.open() }
exportDatabase() returns synchronously (true on success, false on failure) — the signals exist for cases where the caller is decoupled from the click handler. See PHP API §bridge:export for the equivalent CLI command.
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.