Files
php-qml/docs/qml-api.md
magdev beb4e3ab9d docs: refresh README + docs/ for v0.2.0
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>
2026-05-03 22:27:52 +02:00

13 KiB
Raw Blame 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/ 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 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:

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