Decouples /healthz from the publisher contract. v0.1.1 wired
HealthController to constructor-inject Publisher purely as a "is the
bundle resolvable" probe — that worked but cemented the publisher's
API as a readiness-test dependency, which was awkward once
PublisherInterface landed in v0.2.0 chunk 1.
Replace with BridgeBundleInfo: a tiny readonly VO carrying the
bundle's name + class FQCN. HealthController depends on this instead.
Same deep-load semantics (broken bundle → can't construct the VO →
500 on /healthz), no leaky publisher dep.
/healthz response shape:
- `bundle`: was `PhpQml\\Bridge\\Publisher`,
now `PhpQml\\Bridge\\BridgeBundle`
- `name`: new field, reports `php-qml/bridge`
bundled-supervisor.sh's grep updated to match the new canary value
plus an assertion that the new `name` field is present (catches a
botched BridgeBundleInfo wire-up that the bundle-class-name assertion
alone would miss).
Quality + maker snapshot + bundled-supervisor integration test all
pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
13 KiB
Changelog
All notable changes to this project are documented here.
The format is based on Keep a Changelog and this project adheres to Semantic Versioning. Pre-v1.0.0, minor bumps may break public API.
Unreleased
This section tracks work landing on dev toward v0.2.0 (next minor; pre-1.0 SemVer permits API breaks). See PLAN.md §13 for the full v0.2.0 scope.
Added
PublisherInterface,ModelPublisherInterface,CorrelationContextInterface. The bridge's three public services now ship as interfaces (same namespace as concrete, mirroring upstreamHubInterface/Hub). App controllers and listeners should typehint these instead of the concrete classes so swappable implementations (offline-buffer publisher, request-stamp correlation context, etc.) remain non-breaking. ExistingPublisher/ModelPublisher/CorrelationContextclasses implement the new interfaces unchanged.BridgeOpenum. PHP 8.1 string-backed enum (Upsert/Delete/Replace/Event) replacing the raw'upsert'/'delete'strings previously passed betweenDoctrineBridgeListenerandModelPublisher::publishEntityChange. Values match PLAN.md §4's envelopeopwire format. Typo'd ops are now caught at the type level instead of silently producing envelopes clients ignore.BridgeBundleInfovalue object carrying the bundle's name + class FQCN.HealthControllernow constructor-injects this instead ofPublisherInterfaceas the deep-load canary, so the readiness probe is no longer coupled to the publisher's contract./healthzresponse gains anamefield (php-qml/bridge); thebundlefield now reportsPhpQml\Bridge\BridgeBundle(wasPhpQml\Bridge\Publisher).
Changed
ModelPublisher::publishEntityChange()signature:string $op→BridgeOp $op. Pre-1.0 SemVer break. Internal callers updated; external callers (rare) need to migrate from raw strings to enum cases.- Internal typehints switched to interfaces.
ModelPublisherconstructor takesPublisherInterface+CorrelationContextInterface;DoctrineBridgeListenertakesModelPublisherInterface;HealthControllertakesBridgeBundleInfo; the skeleton'sPingControllertakesPublisherInterface. Autowire continues to inject the concrete implementations transparently. /healthzresponse shape.bundlefield's value changes fromPhpQml\Bridge\PublishertoPhpQml\Bridge\BridgeBundle; newnamefield reports the Composer package name. JSON consumers ignoring unknown keys are unaffected; consumers asserting thebundlevalue need to migrate.
Fixed
ModelPublisher::extractIdreflection cleanup. Removed thesetAccessible(true)call (deprecated since PHP 8.1; all properties are accessible via Reflection without it).
0.1.2 — 2026-05-03
Bugfix release. Bundles the v0.1.1 follow-up that surfaced during the v0.1.2 cycle (bundled-mode supervisor cleanly SIGTERMs its child on host exit) with three non-breaking fixes from a post-v0.1.1 architecture audit.
Fixed
- Bundled supervisor: clean child shutdown.
BackendConnection's destructor was the only path that calledteardownChild(), but it ran during stack unwinding afterapp.exec()returned — by then the Qt event loop was already mid-shutdown andQProcess::waitForFinishedcouldn't reliably reap the child. Symptom: Qt loggedQProcess: Destroyed while process ("...frankenphp") is still running, frankenphp + its PHP workers became orphans. The constructor now also connectsQCoreApplication::aboutToQuit→teardownChild, so the child is SIGTERM'd while the event loop is still active. The bundled-supervisor integration test gained a clean-shutdown assertion (no Qt warning + no orphan frankenphp under the host's PGID after SIGTERM). bridge.qml_pathis now actually configurable. TheBridgeResourceMakerandBridgeWindowMakerdocstrings claimed the QML scaffold path was settable via the bundle'sqml_pathoption, but the bundle'sconfigure()was empty and the constructor default ('../qml/') was the only knob.BridgeBundle::configurenow defines aqml_pathscalar node;loadExtensionexposes it as thebridge.qml_pathcontainer parameter;services.yamlbinds it into both makers. Apps can override withconfig/packages/bridge.yaml:bridge: { qml_path: ../qml/ }. Default unchanged.SessionAuthenticator: problem+json on the entry-point path.onAuthenticationFailurealready returned RFC 7807application/problem+jsonfor bad-token requests, but Symfony's defaultAuthenticationEntryPointInterface::startfired for no-token requests, returning a Form-flavoured 302/401 with the wrong shape for QML'sRestClienterror mapping. The authenticator now implementsAuthenticationEntryPointInterfaceand routes both paths through a sharedproblemJson()helper so QML sees one error shape regardless of which firewall path was taken. New test covers the entry-point response.CorrelationKeyListener::onTerminatesub-request guard.onRequestalready guarded withisMainRequest(), butonTerminatecleared unconditionally — a sub-request finishing mid-controller would wipe the main request's correlation key, causing the matching Mercure echo to lose itscorrelationKeyfield and the optimistic UI to never reconcile. FrankenPHP worker mode does not currently emit sub-requests so the user-visible impact is nil, but the asymmetry was a defensive bug.
0.1.1 — 2026-05-03
Bugfix release closing the four follow-ups identified during the v0.1.0 shakedown. No new public API surface; /healthz response gains an additive bundle field (existing JSON consumers ignore unknown keys).
Fixed
- Wipe Symfony cache on bundled-mode launch. Symfony's compiled container bakes
kernel.project_diras an absolute path. In bundled mode that path lives inside the AppImage's FUSE mount (/tmp/.mount_<random>), which is regenerated every launch. So the cache from launch N referenced mount-N's path; launch N+1 (different mount) hitInvalidDirectoryfrom doctrine-migrations on the first launch-2 (and similar at any kernel.project_dir-sensitive lookup).BackendConnection::initBundledModenowrmdirs the cache before each spawn. Costs ~1-2s of warmup per launch; build-time cache warmup is the permanent fix (PLAN.md §13 v0.2.0). The bundled-supervisor integration test gained a 2nd-launch-from-fresh-staging step so this regresses if forgotten. HealthControllerdeep-loads the bundle. Constructor-injectsPublisherso/healthzreturns 200 only whenBridgeBundleis fully container-resolvable. v0.1.0's/healthzreturned 200 against half-loaded bundles — both the path-repo symlink dangling at runtime and the read-only-cache failure shipped green through perfsmoke as a result. Response body now includesbundle: "PhpQml\\Bridge\\Publisher"as the canary value.- Caddyfile formatting.
framework/skeleton/Caddyfileandexamples/todo/Caddyfilereformatted withcaddy fmt. The "Caddyfile input is not formatted; run 'caddy fmt --overwrite'" warning that fired on every FrankenPHP boot is gone.
Added
- Bundled-mode supervisor integration test (
examples/todo/tests/bundled-supervisor.sh,make integration-bundled). Stages a fake AppImage layout in/tmp(host binary copied — Qt'sapplicationDirPath()dereferences symlinks via/proc/self/exe, so the real layout has to be mimicked closely; staged Symfony tree ischmod -R a-wto actually exercise the read-only-mount cache redirect) and exercises the supervisor end-to-end without needing a real.AppImagebuild. Asserts/healthzdeep-load + cache redirect. Wired into.gitea/workflows/ci.ymlafter the existing dev-mode integration test. - Skeleton AppImage parity.
framework/skeleton/Makefilegainsstaging-symfony+appimagetargets mirroringexamples/todo/Makefile's. Newframework/skeleton/packaging/skeleton.{desktop,png}provide minimal AppImage assembly inputs.bin/php-qml-initnow: (a) renames packaging files to match the scaffolded app name, (b) rewrites the.desktopfile'sName/Exec/Icon, (c) substitutes the newBUNDLE_SRCandPACKAGINGMakefile variables to either absolute framework paths (default) or vendored.bridge/.bridge-packagingpaths (--vendor). Scaffolded apps inheritmake appimageworking out of the box.
Notes
BackendConnection::m_portstays hardcoded to 8765 — port-collision between two installed php-qml apps is a real bug surfaced during v0.1.1 prep, but the fix touches every consumer that hardcodes 8765 (perfsmoke, the new bundled-supervisor test) so it's tracked as a v0.2.0 item rather than a v0.1.x bugfix.
0.1.0 — 2026-05-03
First public preview. Phases 0 through 4a in PLAN.md are complete plus the Phase 5 DX-polish sub-commits. Linux is the only packaged target; macOS and Windows are deferred to 4b / 4c. Tagging is the user's call (release CI runs on v* tags).
Added
- Process-pair architecture. Qt/QML host owns rendering; bundled FrankenPHP child runs the Symfony app in worker mode. They communicate via local HTTP and Mercure SSE.
- Symfony bundle (
php-qml/bridge).BridgeBundlewires Doctrine subscriber,ModelPublisher,bridge:doctorconsole command, and the#[BridgeResource]attribute so app code stays idiomatic Symfony. - Qt module (
PhpQml.Bridge).BackendConnection(lifecycle + Update Semantics state machine: Connecting / Online / Reconnecting / Offline),RestClient,MercureClient,ReactiveListModel,ReactiveObject,AppShell,SingleInstance(QLocalServer-backed lock with launch-arg forwarding),DevConsole. - Update Semantics. Optimistic mutations with
Idempotency-Keyround-tripped to Mercure ascorrelationKey; in-flightpendingrole; offline overlay + reconnecting banner viaAppShell. - Headline makers (
symfony/maker-bundle):make:bridge:resource <Name>— entity (#[BridgeResource]+ UUIDv7 by default,--int-idfor auto-increment), CRUD controller, starter<Name>List.qml.make:bridge:command <Name>— controller stub for non-CRUD endpoints.make:bridge:window <Name>— second-window QML scaffold.
- Skeleton application (
framework/skeleton) — minimal reference app exercised by every CI job. - POC todo app (
examples/todo) — full list UI, multi-window mirror, mark-all-done command, end-to-end test of multi-window coherence and crash-recovery. - bundled mode. When
BRIDGE_URLis unset (typical AppImage case),BackendConnectionspawns the embedded FrankenPHP, generates a per-session bearer token, runs first-launch migrations into~/.local/share/<app>/var/data.sqlite, and supervises the child withprctl(PR_SET_PDEATHSIG, SIGTERM)for cleanup safety. - Linux AppImage packaging.
packaging/linux/build-appimage.sh+make appimageproduce a single ~150 MB binary (Qt + Symfony + FrankenPHP + AppImageUpdate sidecar). - AppImageUpdate auto-update. Embedded
update-infoELF section points at the canonical Gitea Releases URL.BackendConnection.checkForUpdates()/applyUpdate()invoke the bundled sidecar. - Release CI (
.gitea/workflows/release.yml). Triggers onv*tags. Builds the AppImage, runstests/perfsmoke.shagainst PLAN.md §11 budgets (bundle ≤ 200 MB, cold start ≤ 4 s on shared CI runners, idle RSS ≤ 200 MB), generates zsync metadata +latest.jsonappcast +SHA256SUMS, optionally GPG-signs them, and uploads everything to the Gitea Release. - Quality CI (
.gitea/workflows/ci.yml). PHPStan + php-cs-fixer (check) + PHPUnit + qmllint + maker snapshot test + bridge-integration test (HTTP/SSE round-trip + crash-recover) on every push tomain. - DX polish (Phase 5):
DevConsole.qml— opt-in window into the bundled FrankenPHP child's merged stdout/stderr; 500-line ring buffer; `Ctrl+`` toggles it in skeleton + todo example.bin/php-qml-init <name>— bash scaffolder. Copiesframework/skeleton/, rewrites identifiers, repoints the path-composer-repo and CMakeadd_subdirectory(framework/qml)reference, runscomposer installand migrations.--vendorproduces a portable copy..vscode/launch.json+tasks.json+settings.jsonand.idea/runConfigurations/shipped with skeleton and todo example.- Hot-reload story documented end-to-end (FrankenPHP
--watch, Qt Creator Reload,qmllslive preview).
Notes
- Tooling versions enforced: PHP 8.4+, Symfony 8, Doctrine ORM 3, Qt 6.5+, FrankenPHP 1.12.2.
- The bundle ships without
composer.lock(it's a library); the skeleton and the todo example carry their own. - Licensed under LGPL-3.0-or-later (
LICENSE+LICENSE.GPLat the repo root). Chosen to align with Qt 6's LGPLv3 licensing — see PLAN.md §12 for the relinkability obligations the AppImage build already honours.