-
v0.2.0
Pre-Releasereleased this
2026-05-03 19:14:01 +00:00 | 0 commits to main since this releaseFirst minor release. Pre-1.0 SemVer permits API breaks; the only one is
ModelPublisher::publishEntityChange()'sstring $op→BridgeOp $opsignature change. Apps that only consumed the framework via the makers, the Doctrine listener, and the QML module are unaffected.The release closes the post-v0.1.2 architecture audit (interfaces, typed enum,
BridgeBundleInfo, maker DRY, DTO-shaped controller scaffold) and delivers the §12 Operations row from PLAN.md (port negotiation, pre-migration auto-backup,bridge:export, periodic auto-update check, native-dialogs boundary doc) plus two new makers (make:bridge:event,make:bridge:read-model) andqmltestrunner-based QML unit tests in CI.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.make:bridge:event <Name>maker. Generates a domain-event class (src/Event/<Name>Event.php, readonly value object), a subscriber (src/EventSubscriber/<Name>Subscriber.php) that republishes viaPublisherInterfaceonapp://event/<kebab-name>, and a QML stub ({qml_path}/<Name>EventHandler.qml) that listens viaMercureClientand re-emits as a typedsignal. Closes the third row of PLAN.md §8's makers table; pairs with the existingmake:bridge:resource/command/windowmakers so domain events have a one-command path from PHP through to QML.make:bridge:read-model <Name>maker. Generates a query-only projection:src/ReadModel/<Name>ReadModel.php(query service stub injectingEntityManagerInterface),src/Controller/<Name>Controller.php(single GET handler at/api/<kebab-plural>), and{qml_path}/<Name>List.qml(ReactiveListModelbound to the route, deliberately no Mercure topic — read-models aren't auto-reactive; invalidation is event-driven viamake:bridge:event). Closes the fourth row of PLAN.md §8's makers table.- Pre-migration auto-backup of
var/data.sqlite. Bundled-mode supervisor copies the SQLite file tovar/data.sqlite.<unix-timestamp>.bakbefore invokingdoctrine:migrations:migrate; trims to the 5 most recent. SQLite's lack of transactional DDL means a half-applied migration can corrupt the database with no rollback path; cheap insurance against that. Skipped on first launch (no DB to back up); failure to copy logs a warning and continues (a missing safety-net is not a reason to refuse to boot). Backup runs only in bundled mode — dev mode users own theirvar/data.sqlitelifecycle. Bundled-supervisor integration test gained an assertion that a.bakfile appears under the user data dir on second launch. bridge:exportconsole command + QML hook. Newbin/console bridge:export <destination>copies the active SQLite database to a user-chosen path (overwrites if the destination exists; reads the source path fromDATABASE_URLso it works in both dev and bundled mode). Mirrored on the QML side asBackendConnection.exportDatabase(path)(Q_INVOKABLE bool) returning success synchronously and emittingdatabaseExported(path)/databaseExportFailed(reason)for async UX. QML callers typically pair it withQt.labs.platform.FileDialog(seedocs/native-dialogs.md). 4 unit tests cover the command's success / non-SQLite-URL / missing-source / overwrite paths.- Periodic auto-update check. Bundled-mode supervisor arms an
AppImageUpdatepoll on the firstOnlinetransition: a launch-time check 10 s after backend ready, then a recurring check every 6 hours. PLAN.md §11 Auto-update called for "check on launch and once per N hours; offer install on next restart, never auto-restart" — the existingcheckForUpdates()Q_INVOKABLE remains the install trigger, this just automates the polling. Disable withBRIDGE_AUTO_UPDATE_DISABLE=1; override the period withBRIDGE_AUTO_UPDATE_PERIOD_MIN=<minutes>. Dev mode skips entirely. - Bundled-mode port negotiation. The hardcoded
m_port = 8765is replaced with a runtime-negotiated free ephemeral port: bind aQTcpServertoQHostAddress::LocalHostport 0, captureserverPort(), close, then hand the port to FrankenPHP via the existingPORTenv var (the Caddyfile already reads{$PORT:8765}). Two installed php-qml apps no longer collide on first launch — whichever loses the port-8765 race used to go Offline; now each picks its own. Test harnesses can pin the port viaBRIDGE_PORT=<n>for reproducibility (the existingbundled-supervisor.shandperfsmoke.shboth export it). Each launch also writes the chosen port tovar/bridge.portso any external tool that needs the runtime address can read it without parsing Qt's log. qmltestrunnerQML unit tests + CI wiring.framework/qml/tests/now ships a Qt Quick Test executable target (qml_unit_tests) discovered by CTest. Built only when configured with-DBUILD_TESTING=ONso production AppImages don't carry it. One smoke test (tst_smoke.qml) proves the harness; future per-feature tests land beside it astst_<feature>.qml. Wired intomake qmltest(skeleton + example/todo) and into the Gitea ActionsQualityjob after qmllint.
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.
Documentation
docs/native-dialogs.md. Documents the framework boundary §12 already implied: native UI affordances (file pickers, confirmations, system notifications) live on the QML side viaQt.labs.platform, not in PHP. Includes copy-pasteable examples ofFileDialog,MessageDialog,SystemTrayIcon, and the trigger-vs-effect split for server-pushed-event-driven dialogs.
Downloads