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

11 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.
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 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