• v0.2.0 340f2881d0

    v0.2.0
    All checks were successful
    CI / Quality (push) Successful in 5m31s
    Release / Linux AppImage (push) Successful in 5m16s
    Pre-Release

    magdev released this 2026-05-03 19:14:01 +00:00 | 0 commits to main since this release

    First minor release. Pre-1.0 SemVer permits API breaks; the only one is ModelPublisher::publishEntityChange()'s string $opBridgeOp $op signature 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) and qmltestrunner-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 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.
    • BridgeBundleInfo value object carrying the bundle's name + class FQCN. HealthController now constructor-injects this instead of PublisherInterface as the deep-load canary, so the readiness probe is no longer coupled to the publisher's contract. /healthz response gains a name field (php-qml/bridge); the bundle field now reports PhpQml\Bridge\BridgeBundle (was PhpQml\Bridge\Publisher).
    • Maker\Support\NameInput — shared interactive name prompt. All three make: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\NamingcamelTo($name, $separator) helper. Replaces inline preg_replace('/(?<!^)[A-Z]/', $sep.'$0', $name) regex copies (BridgeResourceMaker emits _-joined route plurals, BridgeCommandMaker emits --joined kebab slugs).
    • make:bridge:resource --with-dto opt-in. Generates Create<Name>Dto + Update<Name>Dto under src/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 7807 application/problem+json automatically (Symfony's RequestPayloadValueResolver) — no more if (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-dto the legacy template still ships unchanged. Maker fails loud if symfony/validator isn't autoloadable. Skeleton + example/todo composer.json pull symfony/validator so 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 via PublisherInterface on app://event/<kebab-name>, and a QML stub ({qml_path}/<Name>EventHandler.qml) that listens via MercureClient and re-emits as a typed signal. Closes the third row of PLAN.md §8's makers table; pairs with the existing make:bridge:resource / command / window makers 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 injecting EntityManagerInterface), src/Controller/<Name>Controller.php (single GET handler at /api/<kebab-plural>), and {qml_path}/<Name>List.qml (ReactiveListModel bound to the route, deliberately no Mercure topic — read-models aren't auto-reactive; invalidation is event-driven via make: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 to var/data.sqlite.<unix-timestamp>.bak before invoking doctrine: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 their var/data.sqlite lifecycle. Bundled-supervisor integration test gained an assertion that a .bak file appears under the user data dir on second launch.
    • bridge:export console command + QML hook. New bin/console bridge:export <destination> copies the active SQLite database to a user-chosen path (overwrites if the destination exists; reads the source path from DATABASE_URL so it works in both dev and bundled mode). Mirrored on the QML side as BackendConnection.exportDatabase(path) (Q_INVOKABLE bool) returning success synchronously and emitting databaseExported(path) / databaseExportFailed(reason) for async UX. QML callers typically pair it with Qt.labs.platform.FileDialog (see docs/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 AppImageUpdate poll on the first Online transition: 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 existing checkForUpdates() Q_INVOKABLE remains the install trigger, this just automates the polling. Disable with BRIDGE_AUTO_UPDATE_DISABLE=1; override the period with BRIDGE_AUTO_UPDATE_PERIOD_MIN=<minutes>. Dev mode skips entirely.
    • Bundled-mode port negotiation. The hardcoded m_port = 8765 is replaced with a runtime-negotiated free ephemeral port: bind a QTcpServer to QHostAddress::LocalHost port 0, capture serverPort(), close, then hand the port to FrankenPHP via the existing PORT env 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 via BRIDGE_PORT=<n> for reproducibility (the existing bundled-supervisor.sh and perfsmoke.sh both export it). Each launch also writes the chosen port to var/bridge.port so any external tool that needs the runtime address can read it without parsing Qt's log.
    • qmltestrunner QML 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=ON so production AppImages don't carry it. One smoke test (tst_smoke.qml) proves the harness; future per-feature tests land beside it as tst_<feature>.qml. Wired into make qmltest (skeleton + example/todo) and into the Gitea Actions Quality job after qmllint.

    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 takes BridgeBundleInfo; the skeleton's PingController takes PublisherInterface. Autowire continues to inject the concrete implementations transparently.
    • /healthz response shape. bundle field's value changes from PhpQml\Bridge\Publisher to PhpQml\Bridge\BridgeBundle; new name field reports the Composer package name. JSON consumers ignoring unknown keys are unaffected; consumers asserting the bundle value need to migrate.

    Fixed

    • ModelPublisher::extractId reflection cleanup. Removed the setAccessible(true) call (deprecated since PHP 8.1; all properties are accessible via Reflection without it).

    Tests

    • BridgeOpTest wire-format contract. Locks the four enum case values (upsert / delete / replace / event) against accidental rename — QML clients hardcode the strings, so a value change 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 via Qt.labs.platform, not in PHP. Includes copy-pasteable examples of FileDialog, MessageDialog, SystemTrayIcon, and the trigger-vs-effect split for server-pushed-event-driven dialogs.
    Downloads