CI was failing on the Install-bundle-dependencies step because shivammathur/setup-php was installing 8.3 while Symfony 8.x dependencies declare php >= 8.4. Local composer install worked because the dev box runs PHP 8.5.5; CI doesn't. Bumps: - framework/php/composer.json - framework/skeleton/symfony/composer.json - examples/todo/symfony/composer.json - .gitea/workflows/ci.yml php-version: '8.3' → '8.4' - .gitea/workflows/release.yml same - PLAN.md §13 Phase 1 *Detailed scope* PHP minimum row PHPStan / cs-fixer / PHPUnit stay green locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
141 lines
4.4 KiB
C++
141 lines
4.4 KiB
C++
#pragma once
|
|
|
|
#include <QElapsedTimer>
|
|
#include <QObject>
|
|
#include <QProcess>
|
|
#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.
|
|
///
|
|
/// Mode is auto-detected on construction:
|
|
/// - `BRIDGE_URL` env set → **dev mode**: connect to a developer-managed
|
|
/// backend at that URL.
|
|
/// - `BRIDGE_URL` unset → **bundled mode** (Phase 4a): spawn the embedded
|
|
/// `frankenphp` next to the host binary, generate a per-session bearer
|
|
/// token, run first-launch migrations, and supervise the child.
|
|
///
|
|
/// See PLAN.md §3 (Run modes), §7 (BackendConnection), §13 Phase 4a.
|
|
class BackendConnection : public QObject
|
|
{
|
|
Q_OBJECT
|
|
QML_ELEMENT
|
|
QML_SINGLETON
|
|
|
|
Q_PROPERTY(Mode mode READ mode CONSTANT)
|
|
Q_PROPERTY(QString url READ url NOTIFY urlChanged)
|
|
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,
|
|
};
|
|
Q_ENUM(Mode)
|
|
|
|
/// Full Update Semantics enum (PLAN.md §5).
|
|
enum class ConnectionState {
|
|
Connecting,
|
|
Online,
|
|
Reconnecting,
|
|
Offline,
|
|
};
|
|
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; }
|
|
|
|
/// Bundled mode: re-spawn the FrankenPHP child (e.g. after the user
|
|
/// hits Retry on the Offline overlay). Dev mode: re-probe.
|
|
Q_INVOKABLE void restart();
|
|
|
|
/// Bundled mode: ask the bundled `AppImageUpdate.AppImage` sidecar
|
|
/// whether a newer release is available. Dev mode: emits
|
|
/// `updateCheckFailed` with an explanatory string. Result is signalled
|
|
/// via updatesAvailable / noUpdatesAvailable / updateCheckFailed.
|
|
Q_INVOKABLE void checkForUpdates();
|
|
|
|
/// Bundled mode: invoke the sidecar to download and apply the
|
|
/// available update in place. The user has to restart the app
|
|
/// after this completes; emits `updateApplied` / `updateApplyFailed`.
|
|
Q_INVOKABLE void applyUpdate();
|
|
|
|
signals:
|
|
void urlChanged();
|
|
void tokenChanged();
|
|
void connectionStateChanged();
|
|
void errorChanged();
|
|
/// Emitted in bundled mode when the supervisor restarts the FrankenPHP
|
|
/// child and a fresh per-session secret is generated. RestClient and
|
|
/// MercureClient pick the new value up on next request (§3 *Edge cases*).
|
|
void tokenRotated(const QString& newToken);
|
|
|
|
void updatesAvailable();
|
|
void noUpdatesAvailable();
|
|
void updateCheckFailed(const QString& reason);
|
|
void updateApplied();
|
|
void updateApplyFailed(const QString& reason);
|
|
|
|
private slots:
|
|
void probe();
|
|
void onProbeFinished();
|
|
void onChildFinished(int exitCode, QProcess::ExitStatus status);
|
|
|
|
private:
|
|
void initDevMode();
|
|
void initBundledMode();
|
|
bool runMigrations();
|
|
bool spawnChild(QString* errorOut = nullptr);
|
|
void teardownChild();
|
|
QString resolveFrankenphpBin() const;
|
|
QString resolveSymfonyDir() const;
|
|
QString resolveCaddyfilePath() const;
|
|
QString userDataDir() const;
|
|
QString databaseUrl() const;
|
|
void setState(ConnectionState s);
|
|
void setError(const QString& msg);
|
|
void setUrl(const QString& url);
|
|
void setToken(const QString& token);
|
|
static QString randomSecret(int bytes);
|
|
|
|
Mode m_mode = Mode::Dev;
|
|
QString m_url;
|
|
QString m_token;
|
|
QString m_jwtSecret;
|
|
ConnectionState m_state = ConnectionState::Connecting;
|
|
QString m_error;
|
|
QString m_appName;
|
|
QString m_dataDir;
|
|
quint16 m_port = 8765;
|
|
|
|
QNetworkAccessManager* m_nam = nullptr;
|
|
QNetworkReply* m_pendingReply = nullptr;
|
|
QTimer* m_retryTimer = nullptr;
|
|
QElapsedTimer m_firstFailureSinceOnline;
|
|
int m_offlineThresholdMs = 30000;
|
|
|
|
QProcess* m_child = nullptr;
|
|
int m_supervisorRetries = 0;
|
|
static constexpr int kMaxSupervisorRetries = 5;
|
|
};
|
|
|
|
} // namespace PhpQml::Bridge
|