Phase 1 sub-commit 6: skeleton wiring — make dev runs end-to-end
Symfony app under framework/skeleton/symfony/: minimal bin/console,
public/index.php, MicroKernel-based src/Kernel.php, services.yaml,
framework/security/mercure config, and a demo App\Controller\PingController
that GETs /api/ping (returning JSON pong) and republishes the same
payload to the Mercure topic app://ping. composer.json uses a path
repository to symlink the bundle from ../../php so local edits are
picked up live.
QML app under framework/skeleton/qml/: top-level CMake that
add_subdirectory's framework/qml, a main.cpp that creates the Qt
process, runs SingleInstance.acquireOrForward before any QML loads,
exposes SingleInstance via context property, and loadFromModule's
Skeleton.Main. Main.qml uses BackendConnection / RestClient /
MercureClient from PhpQml.Bridge and renders status dots, a Ping
button, and an event log.
Caddyfile binds 127.0.0.1:8765, enables in-memory Mercure with a
256-bit dev JWT (matches symfony/.env, lcobucci/jwt requires this).
Makefile wraps build / dev / doctor / clean; scripts/dev.sh starts
FrankenPHP --watch and the Qt host together with explicit PID-based
teardown (process-group `kill 0` proved unreliable when frankenphp's
watch fork reparented).
Bug fixes uncovered in this sub-commit:
- SingleInstance.acquireOrForward: probe-first, then removeServer +
retry-listen. The original loop-with-removeServer-after-failed-bind
silently exited on stale sockets from prior runs.
- Main.qml: MercureClient does NOT inherit BackendConnection.token —
Mercure subscribes anonymously in dev (Caddyfile), and forwarding
the bridge bearer made it 401-loop.
- /api/ping was 500ing because the dev MERCURE_JWT_SECRET was 144 bits;
bumped to 64-char (>=256 bit) to satisfy lcobucci/jwt.
- Linked the framework lib (php_qml_bridge) explicitly in addition to
the QML plugin so SingleInstance.h resolves.
- Auto-generated config/reference.php gitignored.
Smoke verified offscreen: /healthz 200, /api/ping 200, 1 publish, 1
subscriber, zero 401s, clean shutdown with no zombies.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 01:50:16 +02:00
|
|
|
|
import QtQuick
|
|
|
|
|
|
import QtQuick.Controls
|
|
|
|
|
|
import QtQuick.Layouts
|
|
|
|
|
|
import QtQuick.Window
|
|
|
|
|
|
|
|
|
|
|
|
import PhpQml.Bridge
|
|
|
|
|
|
|
|
|
|
|
|
ApplicationWindow {
|
|
|
|
|
|
id: window
|
|
|
|
|
|
width: 760
|
|
|
|
|
|
height: 540
|
|
|
|
|
|
visible: true
|
|
|
|
|
|
title: "php-qml — skeleton"
|
|
|
|
|
|
|
|
|
|
|
|
RestClient {
|
|
|
|
|
|
id: rest
|
|
|
|
|
|
baseUrl: BackendConnection.url
|
|
|
|
|
|
token: BackendConnection.token
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
MercureClient {
|
|
|
|
|
|
id: mercure
|
|
|
|
|
|
hubUrl: BackendConnection.url + "/.well-known/mercure"
|
|
|
|
|
|
topic: "app://ping"
|
|
|
|
|
|
// No token in Phase 1 dev mode — Caddyfile enables `anonymous`
|
|
|
|
|
|
// for Mercure subscribers. Phase 4 swaps in a Mercure-issued JWT
|
|
|
|
|
|
// when the bundled mode tightens auth.
|
|
|
|
|
|
onUpdate: function(data, id) {
|
|
|
|
|
|
log.append("← mercure " + (id ? id.split(":").pop() : "") + " " + data)
|
|
|
|
|
|
}
|
|
|
|
|
|
onError: function(detail) {
|
|
|
|
|
|
log.append("× mercure error: " + detail)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Connections {
|
|
|
|
|
|
target: BackendConnection
|
|
|
|
|
|
function onConnectionStateChanged() {
|
|
|
|
|
|
if (BackendConnection.connectionState === BackendConnection.Online) {
|
|
|
|
|
|
if (!mercure.active) mercure.start()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if (mercure.active) mercure.stop()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Connections {
|
|
|
|
|
|
target: SingleInstance
|
|
|
|
|
|
function onLaunchArgsReceived(args) {
|
|
|
|
|
|
window.requestActivate()
|
|
|
|
|
|
log.append("· launch args from peer: " + args.join(" "))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ColumnLayout {
|
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
|
anchors.margins: 16
|
|
|
|
|
|
spacing: 12
|
|
|
|
|
|
|
|
|
|
|
|
RowLayout {
|
|
|
|
|
|
spacing: 10
|
|
|
|
|
|
Layout.fillWidth: true
|
|
|
|
|
|
|
|
|
|
|
|
Rectangle {
|
Phase 1 sub-commit 7: CI quality job
PHPStan (level 6 + symfony extension) and PHP CS Fixer (Symfony +
PHP83Migration ruleset) configs at framework/php/. composer.json
exposes phpstan / cs:check / cs:fix / phpunit / quality scripts.
PHPStan-clean across the bundle; cs:check is happy after auto-fix
applied @Symfony idioms (yoda, leading-backslash JSON_*, blank-line
before return). Test mocks consolidated into a HubSpy helper to keep
PHPStan happy about by-ref captures.
Skeleton's Makefile target `quality` chains `composer quality` (in
framework/php/) with cmake's all_qmllint target. Local run is green —
11 tests / 32 assertions, no PHPStan errors, cs-fixer clean, qmllint
emits advisory warnings only.
Layout fix in skeleton's Main.qml: status-dot Rectangles inside
RowLayout now use Layout.preferredWidth/Height instead of width/height
to satisfy Quick.layout-positioning checks.
.gitea/workflows/ci.yml replaces the placeholder with a real `quality`
job: setup-php, composer install (cached), the four PHP checks, Qt 6
via install-qt-action (cached), QML module build, qmllint via the
all_qmllint CMake target. Workflow exists from this commit onward
even if a runner isn't provisioned yet.
bridge:doctor lost the Publisher dependency since it was only used as
a "service is wired" marker — the command being injectable already
proves that.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 02:15:06 +02:00
|
|
|
|
Layout.preferredWidth: 12; Layout.preferredHeight: 12; radius: 6
|
Phase 1 sub-commit 6: skeleton wiring — make dev runs end-to-end
Symfony app under framework/skeleton/symfony/: minimal bin/console,
public/index.php, MicroKernel-based src/Kernel.php, services.yaml,
framework/security/mercure config, and a demo App\Controller\PingController
that GETs /api/ping (returning JSON pong) and republishes the same
payload to the Mercure topic app://ping. composer.json uses a path
repository to symlink the bundle from ../../php so local edits are
picked up live.
QML app under framework/skeleton/qml/: top-level CMake that
add_subdirectory's framework/qml, a main.cpp that creates the Qt
process, runs SingleInstance.acquireOrForward before any QML loads,
exposes SingleInstance via context property, and loadFromModule's
Skeleton.Main. Main.qml uses BackendConnection / RestClient /
MercureClient from PhpQml.Bridge and renders status dots, a Ping
button, and an event log.
Caddyfile binds 127.0.0.1:8765, enables in-memory Mercure with a
256-bit dev JWT (matches symfony/.env, lcobucci/jwt requires this).
Makefile wraps build / dev / doctor / clean; scripts/dev.sh starts
FrankenPHP --watch and the Qt host together with explicit PID-based
teardown (process-group `kill 0` proved unreliable when frankenphp's
watch fork reparented).
Bug fixes uncovered in this sub-commit:
- SingleInstance.acquireOrForward: probe-first, then removeServer +
retry-listen. The original loop-with-removeServer-after-failed-bind
silently exited on stale sockets from prior runs.
- Main.qml: MercureClient does NOT inherit BackendConnection.token —
Mercure subscribes anonymously in dev (Caddyfile), and forwarding
the bridge bearer made it 401-loop.
- /api/ping was 500ing because the dev MERCURE_JWT_SECRET was 144 bits;
bumped to 64-char (>=256 bit) to satisfy lcobucci/jwt.
- Linked the framework lib (php_qml_bridge) explicitly in addition to
the QML plugin so SingleInstance.h resolves.
- Auto-generated config/reference.php gitignored.
Smoke verified offscreen: /healthz 200, /api/ping 200, 1 publish, 1
subscriber, zero 401s, clean shutdown with no zombies.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 01:50:16 +02:00
|
|
|
|
color: BackendConnection.connectionState === BackendConnection.Online
|
|
|
|
|
|
? "#3ab36c"
|
|
|
|
|
|
: (BackendConnection.connectionState === BackendConnection.Error ? "#d8503c" : "#d89614")
|
|
|
|
|
|
}
|
|
|
|
|
|
Label { text: "Backend: " + window._stateName(BackendConnection.connectionState) }
|
|
|
|
|
|
|
|
|
|
|
|
Item { width: 12 }
|
|
|
|
|
|
|
|
|
|
|
|
Rectangle {
|
Phase 1 sub-commit 7: CI quality job
PHPStan (level 6 + symfony extension) and PHP CS Fixer (Symfony +
PHP83Migration ruleset) configs at framework/php/. composer.json
exposes phpstan / cs:check / cs:fix / phpunit / quality scripts.
PHPStan-clean across the bundle; cs:check is happy after auto-fix
applied @Symfony idioms (yoda, leading-backslash JSON_*, blank-line
before return). Test mocks consolidated into a HubSpy helper to keep
PHPStan happy about by-ref captures.
Skeleton's Makefile target `quality` chains `composer quality` (in
framework/php/) with cmake's all_qmllint target. Local run is green —
11 tests / 32 assertions, no PHPStan errors, cs-fixer clean, qmllint
emits advisory warnings only.
Layout fix in skeleton's Main.qml: status-dot Rectangles inside
RowLayout now use Layout.preferredWidth/Height instead of width/height
to satisfy Quick.layout-positioning checks.
.gitea/workflows/ci.yml replaces the placeholder with a real `quality`
job: setup-php, composer install (cached), the four PHP checks, Qt 6
via install-qt-action (cached), QML module build, qmllint via the
all_qmllint CMake target. Workflow exists from this commit onward
even if a runner isn't provisioned yet.
bridge:doctor lost the Publisher dependency since it was only used as
a "service is wired" marker — the command being injectable already
proves that.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 02:15:06 +02:00
|
|
|
|
Layout.preferredWidth: 12; Layout.preferredHeight: 12; radius: 6
|
Phase 1 sub-commit 6: skeleton wiring — make dev runs end-to-end
Symfony app under framework/skeleton/symfony/: minimal bin/console,
public/index.php, MicroKernel-based src/Kernel.php, services.yaml,
framework/security/mercure config, and a demo App\Controller\PingController
that GETs /api/ping (returning JSON pong) and republishes the same
payload to the Mercure topic app://ping. composer.json uses a path
repository to symlink the bundle from ../../php so local edits are
picked up live.
QML app under framework/skeleton/qml/: top-level CMake that
add_subdirectory's framework/qml, a main.cpp that creates the Qt
process, runs SingleInstance.acquireOrForward before any QML loads,
exposes SingleInstance via context property, and loadFromModule's
Skeleton.Main. Main.qml uses BackendConnection / RestClient /
MercureClient from PhpQml.Bridge and renders status dots, a Ping
button, and an event log.
Caddyfile binds 127.0.0.1:8765, enables in-memory Mercure with a
256-bit dev JWT (matches symfony/.env, lcobucci/jwt requires this).
Makefile wraps build / dev / doctor / clean; scripts/dev.sh starts
FrankenPHP --watch and the Qt host together with explicit PID-based
teardown (process-group `kill 0` proved unreliable when frankenphp's
watch fork reparented).
Bug fixes uncovered in this sub-commit:
- SingleInstance.acquireOrForward: probe-first, then removeServer +
retry-listen. The original loop-with-removeServer-after-failed-bind
silently exited on stale sockets from prior runs.
- Main.qml: MercureClient does NOT inherit BackendConnection.token —
Mercure subscribes anonymously in dev (Caddyfile), and forwarding
the bridge bearer made it 401-loop.
- /api/ping was 500ing because the dev MERCURE_JWT_SECRET was 144 bits;
bumped to 64-char (>=256 bit) to satisfy lcobucci/jwt.
- Linked the framework lib (php_qml_bridge) explicitly in addition to
the QML plugin so SingleInstance.h resolves.
- Auto-generated config/reference.php gitignored.
Smoke verified offscreen: /healthz 200, /api/ping 200, 1 publish, 1
subscriber, zero 401s, clean shutdown with no zombies.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 01:50:16 +02:00
|
|
|
|
color: mercure.active ? "#3ab36c" : "#666"
|
|
|
|
|
|
}
|
|
|
|
|
|
Label { text: "Mercure: " + (mercure.active ? "subscribed" : "off") }
|
|
|
|
|
|
|
|
|
|
|
|
Item { Layout.fillWidth: true }
|
|
|
|
|
|
|
|
|
|
|
|
Button {
|
|
|
|
|
|
text: "Ping"
|
|
|
|
|
|
enabled: BackendConnection.connectionState === BackendConnection.Online
|
|
|
|
|
|
onClicked: {
|
|
|
|
|
|
const t0 = Date.now()
|
|
|
|
|
|
rest.get("/api/ping").then(function(r) {
|
|
|
|
|
|
const dt = Date.now() - t0
|
|
|
|
|
|
log.append("→ ping " + dt + "ms " + JSON.stringify(r.body))
|
|
|
|
|
|
}).catch(function(e) {
|
|
|
|
|
|
log.append("× ping " + e.status + " " + JSON.stringify(e.problem || e.raw))
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Label {
|
|
|
|
|
|
text: "URL: " + BackendConnection.url
|
|
|
|
|
|
+ (BackendConnection.error ? " error: " + BackendConnection.error : "")
|
|
|
|
|
|
color: BackendConnection.error ? "#d8503c" : "#888"
|
|
|
|
|
|
font.pixelSize: 12
|
|
|
|
|
|
elide: Text.ElideRight
|
|
|
|
|
|
Layout.fillWidth: true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Frame {
|
|
|
|
|
|
Layout.fillWidth: true
|
|
|
|
|
|
Layout.fillHeight: true
|
|
|
|
|
|
padding: 0
|
|
|
|
|
|
ScrollView {
|
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
|
TextArea {
|
|
|
|
|
|
id: log
|
|
|
|
|
|
readOnly: true
|
|
|
|
|
|
wrapMode: TextArea.Wrap
|
|
|
|
|
|
font.family: "monospace"
|
|
|
|
|
|
font.pixelSize: 12
|
|
|
|
|
|
placeholderText: "events will appear here…"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function _stateName(s) {
|
|
|
|
|
|
switch (s) {
|
|
|
|
|
|
case BackendConnection.Connecting: return "connecting"
|
|
|
|
|
|
case BackendConnection.Online: return "online"
|
|
|
|
|
|
case BackendConnection.Error: return "error"
|
|
|
|
|
|
}
|
|
|
|
|
|
return "?"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|