Files
php-qml/CHANGELOG.md
magdev 56e3d671d9 v0.2.0 (1/N): public API surface — interfaces + BridgeOp enum
Establishes the contract layer the rest of v0.2.0 builds on. Pre-1.0
SemVer break: ModelPublisher::publishEntityChange() now takes BridgeOp
instead of a raw string.

Interfaces (Symfony idiom: same namespace as concrete, like HubInterface
next to Hub):
  - PublisherInterface — publish(string, array, bool)
  - ModelPublisherInterface — publishEntityChange(object, BridgeOp)
  - CorrelationContextInterface — set/get/clear

App code should typehint these instead of the concretes so swappable
implementations (offline-buffer publisher, multi-hub fan-out, request-
stamp correlation) remain non-breaking. Concrete classes implement them
unchanged; autowire continues to inject the implementations transparently.

BridgeOp: PHP 8.1 string-backed enum with cases Upsert / Delete /
Replace / Event matching PLAN.md §4's envelope `op` wire format.
Internal call sites updated; tests use the cases directly.

Switched typehints:
  - ModelPublisher ctor: PublisherInterface + CorrelationContextInterface
  - DoctrineBridgeListener ctor: ModelPublisherInterface
  - HealthController ctor: PublisherInterface (still emits `Publisher`
    as bundle canary value — `::class` resolves to the concrete class
    name regardless of typehint, so bundled-supervisor.sh's grep stays
    green)
  - skeleton PingController ctor: PublisherInterface (canonical app
    pattern — example/todo has no Publisher consumer to update)

Drive-by: removed deprecated setAccessible(true) call in
ModelPublisher::extractId — PHP 8.1+ allows reflection without it.

PHPStan + cs-fixer + PHPUnit (17/17) + maker snapshot all pass; dev
container compiles in the example app.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 19:50:01 +02:00

12 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 upstream HubInterface/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. Existing Publisher / ModelPublisher / CorrelationContext classes implement the new interfaces unchanged.
  • BridgeOp enum. PHP 8.1 string-backed enum (Upsert / Delete / Replace / Event) replacing the raw 'upsert'/'delete' strings previously passed between DoctrineBridgeListener and ModelPublisher::publishEntityChange. Values match PLAN.md §4's envelope op wire format. Typo'd ops are now caught at the type level instead of silently producing envelopes clients ignore.

Changed

  • ModelPublisher::publishEntityChange() signature: string $opBridgeOp $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. ModelPublisher constructor takes PublisherInterface + CorrelationContextInterface; DoctrineBridgeListener takes ModelPublisherInterface; HealthController and the skeleton's PingController take PublisherInterface. Autowire continues to inject the concrete implementations transparently.

Fixed

  • ModelPublisher::extractId reflection cleanup. Removed the setAccessible(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 called teardownChild(), but it ran during stack unwinding after app.exec() returned — by then the Qt event loop was already mid-shutdown and QProcess::waitForFinished couldn't reliably reap the child. Symptom: Qt logged QProcess: Destroyed while process ("...frankenphp") is still running, frankenphp + its PHP workers became orphans. The constructor now also connects QCoreApplication::aboutToQuitteardownChild, 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_path is now actually configurable. The BridgeResourceMaker and BridgeWindowMaker docstrings claimed the QML scaffold path was settable via the bundle's qml_path option, but the bundle's configure() was empty and the constructor default ('../qml/') was the only knob. BridgeBundle::configure now defines a qml_path scalar node; loadExtension exposes it as the bridge.qml_path container parameter; services.yaml binds it into both makers. Apps can override with config/packages/bridge.yaml: bridge: { qml_path: ../qml/ }. Default unchanged.
  • SessionAuthenticator: problem+json on the entry-point path. onAuthenticationFailure already returned RFC 7807 application/problem+json for bad-token requests, but Symfony's default AuthenticationEntryPointInterface::start fired for no-token requests, returning a Form-flavoured 302/401 with the wrong shape for QML's RestClient error mapping. The authenticator now implements AuthenticationEntryPointInterface and routes both paths through a shared problemJson() helper so QML sees one error shape regardless of which firewall path was taken. New test covers the entry-point response.
  • CorrelationKeyListener::onTerminate sub-request guard. onRequest already guarded with isMainRequest(), but onTerminate cleared unconditionally — a sub-request finishing mid-controller would wipe the main request's correlation key, causing the matching Mercure echo to lose its correlationKey field 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_dir as 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) hit InvalidDirectory from doctrine-migrations on the first launch-2 (and similar at any kernel.project_dir-sensitive lookup). BackendConnection::initBundledMode now rmdirs 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.
  • HealthController deep-loads the bundle. Constructor-injects Publisher so /healthz returns 200 only when BridgeBundle is fully container-resolvable. v0.1.0's /healthz returned 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 includes bundle: "PhpQml\\Bridge\\Publisher" as the canary value.
  • Caddyfile formatting. framework/skeleton/Caddyfile and examples/todo/Caddyfile reformatted with caddy 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's applicationDirPath() dereferences symlinks via /proc/self/exe, so the real layout has to be mimicked closely; staged Symfony tree is chmod -R a-w to actually exercise the read-only-mount cache redirect) and exercises the supervisor end-to-end without needing a real .AppImage build. Asserts /healthz deep-load + cache redirect. Wired into .gitea/workflows/ci.yml after the existing dev-mode integration test.
  • Skeleton AppImage parity. framework/skeleton/Makefile gains staging-symfony + appimage targets mirroring examples/todo/Makefile's. New framework/skeleton/packaging/skeleton.{desktop,png} provide minimal AppImage assembly inputs. bin/php-qml-init now: (a) renames packaging files to match the scaffolded app name, (b) rewrites the .desktop file's Name/Exec/Icon, (c) substitutes the new BUNDLE_SRC and PACKAGING Makefile variables to either absolute framework paths (default) or vendored .bridge / .bridge-packaging paths (--vendor). Scaffolded apps inherit make appimage working out of the box.

Notes

  • BackendConnection::m_port stays 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). BridgeBundle wires Doctrine subscriber, ModelPublisher, bridge:doctor console 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-Key round-tripped to Mercure as correlationKey; in-flight pending role; offline overlay + reconnecting banner via AppShell.
  • Headline makers (symfony/maker-bundle):
    • make:bridge:resource <Name> — entity (#[BridgeResource] + UUIDv7 by default, --int-id for 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_URL is unset (typical AppImage case), BackendConnection spawns the embedded FrankenPHP, generates a per-session bearer token, runs first-launch migrations into ~/.local/share/<app>/var/data.sqlite, and supervises the child with prctl(PR_SET_PDEATHSIG, SIGTERM) for cleanup safety.
  • Linux AppImage packaging. packaging/linux/build-appimage.sh + make appimage produce a single ~150 MB binary (Qt + Symfony + FrankenPHP + AppImageUpdate sidecar).
  • AppImageUpdate auto-update. Embedded update-info ELF section points at the canonical Gitea Releases URL. BackendConnection.checkForUpdates() / applyUpdate() invoke the bundled sidecar.
  • Release CI (.gitea/workflows/release.yml). Triggers on v* tags. Builds the AppImage, runs tests/perfsmoke.sh against PLAN.md §11 budgets (bundle ≤ 200 MB, cold start ≤ 4 s on shared CI runners, idle RSS ≤ 200 MB), generates zsync metadata + latest.json appcast + 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 to main.
  • 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. Copies framework/skeleton/, rewrites identifiers, repoints the path-composer-repo and CMake add_subdirectory(framework/qml) reference, runs composer install and migrations. --vendor produces a portable copy.
    • .vscode/launch.json + tasks.json + settings.json and .idea/runConfigurations/ shipped with skeleton and todo example.
    • Hot-reload story documented end-to-end (FrankenPHP --watch, Qt Creator Reload, qmlls live 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.GPL at 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.