diff --git a/CHANGELOG.md b/CHANGELOG.md index 5db9953..9f6b6f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,10 @@ This section tracks work landing on `dev` toward **v0.2.0** (next minor; pre-1.0 - **`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. + ## [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. diff --git a/PLAN.md b/PLAN.md index a8e8a7d..ce97e70 100644 --- a/PLAN.md +++ b/PLAN.md @@ -554,17 +554,18 @@ Bugfix release. Bundles the v0.1.1 follow-up that surfaced during the cycle (cle Pulls in the originally-Phase-3/5-deferred items that don't need new operational dependencies, plus the smaller §12 risks **and** the public-API / DX items surfaced by the post-v0.1.2 audit. Cross-platform packaging is parked at v0.9.0 — the operational lift (self-hosted runners + platform certs) is too big to fold into the next minor and Linux remains the primary target until the framework's surface stabilises. -**Public-API surface (audit-driven, breaks pre-1.0 SemVer permitted):** +**Shipped on `dev` toward v0.2.0** (audit-driven cleanup batch — see CHANGELOG `[Unreleased]` for full notes): -- **Ship interfaces for the bridge's three public services.** `Publisher`, `ModelPublisher`, and `CorrelationContext` are typehinted concretely everywhere (the Doctrine listener, the example `PingController`, every user controller that wants to fire a manual envelope) — the matching upstream Symfony idiom is `HubInterface` / `EventDispatcherInterface` / `NormalizerInterface`. Extract `PublisherInterface`, `ModelPublisherInterface`, `CorrelationContextInterface`; have the concrete classes implement them; switch every internal typehint over; document the interfaces as the public contract. Lets app code mock at the seam without a concrete-class spy and lets us iterate the implementations behind the contract. -- **`BridgeOp` enum.** `'upsert'` / `'delete'` are passed as raw strings between `DoctrineBridgeListener` and `ModelPublisher::publishEntityChange`. PHP 8.1 backed enum is the obvious typed replacement; PLAN.md §4's envelope `op` field already enumerates `upsert` | `delete` | `replace` | `event` so the enum encodes a documented contract. Method signature change is API-visible — pre-1.0 SemVer permits it; ship deprecation paths if the audit surfaces external callers. -- **`HealthController` deep-load canary refactor.** Constructor-injects `Publisher` only as a "is the bundle resolvable" probe (added in v0.1.1). Switching the dependency to a tiny `BridgeBundleInfo` value object that the bundle registers documents intent and decouples `/healthz` from the publisher contract — important once `PublisherInterface` lands. +- ✅ `PublisherInterface` / `ModelPublisherInterface` / `CorrelationContextInterface`. Concrete classes implement them; internal typehints switched over. +- ✅ `BridgeOp` enum (`Upsert` / `Delete` / `Replace` / `Event`). `ModelPublisher::publishEntityChange()` now takes `BridgeOp` instead of `string` (pre-1.0 SemVer break). +- ✅ `BridgeBundleInfo` VO. `HealthController` constructor-injects this instead of `PublisherInterface` as the deep-load canary; `/healthz` `bundle` field reports `PhpQml\Bridge\BridgeBundle`, plus a new `name` field (`php-qml/bridge`). +- ✅ `Maker\Support\NameInput::askOrFail()` + `Maker\Support\Naming::camelTo()`. The duplicated name-prompt closure and camel-conversion regex collapsed into single call sites. +- ✅ `make:bridge:resource --with-dto` opt-in. Generates `CreateDto` + `UpdateDto` and rewrites controller actions to `#[MapRequestPayload]` dispatch — closes the input-validation gap (malformed JSON → 400 problem+json automatically; no more `if (isset($data['title']))` boilerplate). Skeleton + example/todo composer.json pull `symfony/validator`. Snapshot test covers both modes. -**Maker DRY + DX (audit-driven):** +**Still open for v0.2.0** (PLAN.md notes preserved below): -- **Maker shared helpers.** All three makers re-implement the same name-prompt-or-fail closure (`ucfirst(trim(…))` plus throw on empty) and re-spell their own camel-to-snake / camel-to-kebab regexes inline. Extract `Maker\Support\NameInput::askOrFail()` and `Maker\Support\Naming::camelTo($name, '_'|'-')` — single source of truth, three call sites. -- **DTO-shaped controller scaffold (`make:bridge:resource --with-dto`).** Generated CRUD controllers currently accept any JSON shape: `if (isset($data['title'])) …` with silent type coercion, no required-field enforcement, malformed JSON swallowed as `?? []`. Add a `--with-dto` option that emits `CreateDto` + `UpdateDto` DTOs alongside the controller and rewrites the action signatures to `#[MapRequestPayload] CreateTodoDto $dto`. Pulls `symfony/validator` into the skeleton/example dependencies; `#[Assert\NotBlank]` on title fields is the headline default. Symfony's payload-mapping infrastructure produces RFC 7807 problem+json on validation failure for free, fixing the field-mapping repetition between `create()` and `update()` at the same time. Once stable, flip `--with-dto` to default-on. -- **Generated controller `findOr404` boilerplate.** `update()` and `delete()` both inline the find-or-404 problem+json response. Either factor a private helper into the template or migrate to Symfony's `#[MapEntity]` attribute (ships in 7.x). +- **Generated controller `findOr404` boilerplate.** `update()` and `delete()` still inline the find-or-404 problem+json response. Audit suggested factoring a private helper or migrating to Symfony's `#[MapEntity]` attribute. MapEntity changes the 404 response shape away from problem+json unless framework-level error config is updated; a private helper is net-zero on lines. Parking until either (a) we bake skeleton-level RFC 7807 error wiring (so MapEntity preserves shape), or (b) we flip `--with-dto` to default-on and the legacy template's polish becomes irrelevant. +- **Flip `--with-dto` to default-on.** Once snapshot-test churn settles and the DTO templates have surface-feedback, make it the default and gate `--no-dto` for users who want the legacy shape. **Makers + reactive types (Phase 3.x deferred):** diff --git a/framework/php/tests/BridgeOpTest.php b/framework/php/tests/BridgeOpTest.php new file mode 100644 index 0000000..d23d58e --- /dev/null +++ b/framework/php/tests/BridgeOpTest.php @@ -0,0 +1,28 @@ +value); + self::assertSame('delete', BridgeOp::Delete->value); + self::assertSame('replace', BridgeOp::Replace->value); + self::assertSame('event', BridgeOp::Event->value); + } +}