The audit's substantive items shipped in chunks 1–4. Two remaining loose ends inspected and parked: - Generated controller findOr404 boilerplate. MapEntity changes the 404 response shape away from problem+json unless framework-level RFC 7807 error config is updated; a private helper is net-zero on lines. Parking until either (a) skeleton-level RFC 7807 error wiring, or (b) --with-dto flipping to default-on and the legacy template's polish becoming irrelevant. - ModelPublisher::extractId reflection branch. Looks dead because every maker-output entity has getId(), but it remains a safety net for hand-written entities that don't. Keeping. This commit ships: - BridgeOpTest — locks the enum case values against accidental rename. Every case value is a documented wire-format token QML clients hardcode, so renaming a `value` is a wire-protocol break and this fails the build before it ships. - PLAN.md §13 v0.2.0 status block with what's shipped on dev (interfaces / BridgeOp / BridgeBundleInfo / Maker DRY / --with-dto) and what's still open (findOr404 polish, --with-dto default flip). Test count 23 → 24, all passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
15 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).Maker\Support\NameInput— shared interactive name prompt. All threemake:bridge:*makers (resource,command,window) re-implemented the same "prompt, trim, ucfirst, reject empty" closure inline; collapsed into one call site so empty-argument and validation behaviour stay in lockstep.Maker\Support\Naming—camelTo($name, $separator)helper. Replaces inlinepreg_replace('/(?<!^)[A-Z]/', $sep.'$0', $name)regex copies (BridgeResourceMaker emits_-joined route plurals, BridgeCommandMaker emits--joined kebab slugs).make:bridge:resource --with-dtoopt-in. GeneratesCreate<Name>Dto+Update<Name>Dtoundersrc/Dto/alongside the controller, and the controller dispatches via#[MapRequestPayload]. Closes the input-validation gap from the audit: malformed JSON, missing required fields, or#[Assert\NotBlank]violations now produce RFC 7807application/problem+jsonautomatically (Symfony'sRequestPayloadValueResolver) — no moreif (isset($data['title']))boilerplate, no silent type coercion. Update DTOs use nullable defaults so PATCH callers send only the fields they want changed. Without--with-dtothe legacy template still ships unchanged. Maker fails loud ifsymfony/validatorisn't autoloadable. Skeleton + example/todo composer.json pullsymfony/validatorso scaffolded apps work out of the box. Snapshot test exercises both modes.
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).
Tests
BridgeOpTestwire-format contract. Locks the four enum case values (upsert/delete/replace/event) against accidental rename — QML clients hardcode the strings, so avaluechange is a wire-protocol break and the test fails the build before it ships.
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.