Phase 1 sub-commit 4: Qt foundation types
All checks were successful
CI / Quality (push) Successful in 5s

BackendConnection (QML singleton via create() factory) reads BRIDGE_URL
and BRIDGE_TOKEN from env, periodically probes <url>/healthz with a 2s
transfer timeout, and exposes a Connecting/Online/Error state machine
plus error/token properties to QML. Bundled-mode startup (spawning the
embedded FrankenPHP child) is a Phase 4 deliverable; restart() is a
no-op for now. tokenRotated signal is reserved for the per-session
secret rotation described in PLAN.md §3.

SingleInstance is C++-only — main() must call acquireOrForward() before
the QML engine boots, so it's exposed via context property rather than
QML_SINGLETON. QLocalServer-based lock with stale-socket detection,
launch-arg forwarding via QDataStream, and the deadlock-avoiding race
fallback specified in §3 *Edge cases*.

CMakeLists.txt declares the PhpQml.Bridge static QML module with both
sources and is dual-mode: stands alone for sanity builds, integrates
via add_subdirectory from the skeleton's top-level CMake (Phase 1
sub-commit 6). Standalone build verified clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-02 01:18:43 +02:00
parent b3932674dd
commit 87b5b2283c
5 changed files with 358 additions and 9 deletions

View File

@@ -0,0 +1,91 @@
#pragma once
#include <QObject>
#include <QString>
#include <QtQmlIntegration>
class QNetworkAccessManager;
class QNetworkReply;
class QTimer;
class QQmlEngine;
class QJSEngine;
namespace PhpQml::Bridge {
/// Owns the backend lifecycle and exposes its health to QML.
///
/// Phase 1 implements **dev mode**: reads `BRIDGE_URL` and `BRIDGE_TOKEN`
/// from env, periodically probes `<url>/healthz`, and reports the result
/// as `connectionState`. Bundled mode (spawning FrankenPHP as a child)
/// arrives in Phase 4. See PLAN.md §3 (Run modes), §7 (BackendConnection).
class BackendConnection : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
Q_PROPERTY(Mode mode READ mode CONSTANT)
Q_PROPERTY(QString url READ url CONSTANT)
Q_PROPERTY(QString token READ token NOTIFY tokenChanged)
Q_PROPERTY(ConnectionState connectionState READ connectionState NOTIFY connectionStateChanged)
Q_PROPERTY(QString error READ error NOTIFY errorChanged)
public:
enum class Mode {
Dev,
Bundled, // Phase 4
};
Q_ENUM(Mode)
/// Phase 1 surfaces only Connecting / Online / Error. The full enum
/// (Reconnecting, Offline) lands with the Update Semantics layer in
/// Phase 2 (PLAN.md §5).
enum class ConnectionState {
Connecting,
Online,
Error,
};
Q_ENUM(ConnectionState)
explicit BackendConnection(QObject* parent = nullptr);
~BackendConnection() override;
static BackendConnection* create(QQmlEngine* engine, QJSEngine*);
Mode mode() const noexcept { return m_mode; }
QString url() const { return m_url; }
QString token() const { return m_token; }
ConnectionState connectionState() const noexcept { return m_state; }
QString error() const { return m_error; }
Q_INVOKABLE void restart();
signals:
void tokenChanged();
void connectionStateChanged();
void errorChanged();
/// Forward-compatible signal for §3 *Edge cases — Per-session secret
/// rotation*. In Phase 1 dev mode it is never emitted; bundled mode
/// in Phase 4 will fire it on child restart.
void tokenRotated(const QString& newToken);
private slots:
void probe();
void onProbeFinished();
private:
void setState(ConnectionState s);
void setError(const QString& msg);
Mode m_mode = Mode::Dev;
QString m_url;
QString m_token;
ConnectionState m_state = ConnectionState::Connecting;
QString m_error;
QNetworkAccessManager* m_nam = nullptr;
QNetworkReply* m_pendingReply = nullptr;
QTimer* m_retryTimer = nullptr;
};
} // namespace PhpQml::Bridge