# 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`. The supervisor also calls this automatically on launch (10 s after `Online`) and every 6 h thereafter — see [Bundled mode §periodic check](bundled-mode.md#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 ```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. } } } ``` ### `exportDatabase` Pair with `Qt.labs.platform.FileDialog` so the user picks a destination natively: ```qml 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](php-api.md#bridgeexport) 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 `. | ### 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//`. | | `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//.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.