diff --git a/CHANGELOG.md b/CHANGELOG.md index e946440..8106667 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,11 +12,13 @@ This section tracks work landing on `dev` toward **v0.2.0** (next minor; pre-1.0 - **`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`). ### 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.** `ModelPublisher` constructor takes `PublisherInterface` + `CorrelationContextInterface`; `DoctrineBridgeListener` takes `ModelPublisherInterface`; `HealthController` and the skeleton's `PingController` take `PublisherInterface`. Autowire continues to inject the concrete implementations transparently. +- **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 diff --git a/examples/todo/tests/bundled-supervisor.sh b/examples/todo/tests/bundled-supervisor.sh index 007a5ac..b6f2be0 100755 --- a/examples/todo/tests/bundled-supervisor.sh +++ b/examples/todo/tests/bundled-supervisor.sh @@ -122,8 +122,10 @@ done step "/healthz body: $HEALTHZ_BODY" echo "$HEALTHZ_BODY" | grep -q '"status":"ok"' \ || fail "/healthz didn't return status:ok" -echo "$HEALTHZ_BODY" | grep -q '"bundle":"PhpQml\\\\Bridge\\\\Publisher"' \ +echo "$HEALTHZ_BODY" | grep -q '"bundle":"PhpQml\\\\Bridge\\\\BridgeBundle"' \ || fail "/healthz missing bundle field — HealthController deep-load broken" +echo "$HEALTHZ_BODY" | grep -q '"name":"php-qml\\/bridge"' \ + || fail "/healthz missing name field — BridgeBundleInfo not wired" # ── Verify the cache/log redirect actually fired ─────────────────────── step "verify Symfony wrote cache to user data dir, not the read-only staging" diff --git a/framework/php/src/BridgeBundleInfo.php b/framework/php/src/BridgeBundleInfo.php new file mode 100644 index 0000000..9c06056 --- /dev/null +++ b/framework/php/src/BridgeBundleInfo.php @@ -0,0 +1,30 @@ +name = 'php-qml/bridge'; + $this->bundle = BridgeBundle::class; + } +} diff --git a/framework/php/src/Controller/HealthController.php b/framework/php/src/Controller/HealthController.php index 67f3cb8..bf79b6e 100644 --- a/framework/php/src/Controller/HealthController.php +++ b/framework/php/src/Controller/HealthController.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace PhpQml\Bridge\Controller; -use PhpQml\Bridge\PublisherInterface; +use PhpQml\Bridge\BridgeBundleInfo; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\Routing\Attribute\Route; @@ -12,18 +12,18 @@ use Symfony\Component\Routing\Attribute\Route; * Readiness probe used by the Qt host to detect when the backend is up. * See PLAN.md §3 (*Startup*, step 4). * - * `PublisherInterface` is injected purely as a deep-health canary: if the - * bridge bundle's autoload or container wiring is broken (e.g. a packaging - * build with a dangling vendor path-repo symlink), this controller can't - * even be constructed, so /healthz fails 500 instead of misleadingly - * returning 200 against a half-loaded bundle. The response includes the - * concrete class name so packagers can detect a wrong-implementation - * deployment from the canary value alone. + * `BridgeBundleInfo` is injected purely as a deep-load canary: if the + * bundle's autoload or container wiring is broken (e.g. a packaging build + * with a dangling vendor path-repo symlink), this controller can't even + * be constructed, so /healthz fails 500 instead of misleadingly returning + * 200 against a half-loaded bundle. Earlier (v0.1.1–v0.2.0) this canary + * was `PublisherInterface`; switching to a dedicated info VO decouples + * the readiness probe from the publisher's evolving contract. */ final class HealthController { public function __construct( - private readonly PublisherInterface $publisher, + private readonly BridgeBundleInfo $info, ) { } @@ -32,7 +32,8 @@ final class HealthController { return new JsonResponse([ 'status' => 'ok', - 'bundle' => $this->publisher::class, + 'bundle' => $this->info->bundle, + 'name' => $this->info->name, ]); } }