# PHP API reference Public API exposed by the `php-qml/bridge` Symfony bundle. Internal services and event subscribers are documented for awareness; you don't usually inject them yourself. The bundle is registered automatically by `framework/skeleton/config/bundles.php`: ```php return [ PhpQml\Bridge\BridgeBundle::class => ['all' => true], ]; ``` ## At a glance | Symbol | Kind | Use it when… | | --- | --- | --- | | [`#[BridgeResource]`](#bridgeresource-attribute) | PHP attribute | You want a Doctrine entity to dual-publish on Mercure automatically. | | [`ModelPublisher`](#modelpublisher) | Service | You want to publish a custom event without persist/update/remove. | | [`CorrelationContext`](#correlationcontext) | Service | You're inside a non-controller code path and need the current request's `Idempotency-Key`. | | [`bridge:doctor`](#bridgedoctor) | Console command | You want to verify the dev environment is wired correctly. | | [`SessionAuthenticator`](#sessionauthenticator) | Security authenticator | (Internal) checks the `Authorization: Bearer` header against `BRIDGE_TOKEN`. | | [`CorrelationKeyListener`](#correlationkeylistener) | Event subscriber | (Internal) reads `Idempotency-Key` into `CorrelationContext`. | --- ## `#[BridgeResource]` attribute ```php namespace PhpQml\Bridge\Attribute; #[\Attribute(\Attribute::TARGET_CLASS)] final readonly class BridgeResource { public function __construct(public ?string $name = null) {} } ``` Tag a Doctrine entity to make its lifecycle events publish to Mercure: ```php use PhpQml\Bridge\Attribute\BridgeResource; #[ORM\Entity] #[BridgeResource] class Todo { /* … */ } ``` | Attribute arg | Default | Notes | | --- | --- | --- | | `name` | Lowercased class basename | Used as `` in `app://model/` and `app://model//` topics. | After tagging, every `postPersist` / `postUpdate` / `postRemove` event triggers a dual-publish via `ModelPublisher`: - `app://model/` (collection topic — for `ReactiveListModel`). - `app://model//` (entity topic — for `ReactiveObject`). The maker (`make:bridge:resource`) attaches this attribute automatically. ### Custom resource name ```php #[BridgeResource(name: 'task')] class Todo { /* … */ } ``` Topics become `app://model/task` and `app://model/task/`. Useful when the storage class name doesn't match the API noun. ### What it doesn't do - It doesn't set up the `id` field, the routes, or the controller. Those come from the maker (or you write them). - It doesn't drive serialisation. The Symfony normalizer handles that — by default every public field becomes a JSON property. - It doesn't add validation. Use Symfony Validator constraints as you would in any Symfony app. --- ## `ModelPublisher` Service that does the dual-publish. Auto-fired by the bundle's Doctrine subscriber on `postPersist` / `postUpdate` / `postRemove` for any `#[BridgeResource]` entity. Inject it directly when you want to publish a *custom* event (e.g. progress on a long-running command). ```php namespace PhpQml\Bridge; final class ModelPublisher { public function publishEntityChange(object $entity, string $op): void; } ``` `$op` is one of `"upsert"` / `"delete"`. The published JSON payload: ```json { "op": "upsert", "id": "", "data": { /* normalised entity */ }, "correlationKey": "", "version": 42 } ``` `version` is incremented per resource name. The version table (`bridge_resource_version`) is migrated automatically. ### Example: republish on a custom event ```php final class MarkAllDoneController { public function __construct( private EntityManagerInterface $em, private ModelPublisher $publisher, ) {} public function __invoke(): JsonResponse { foreach ($this->em->getRepository(Todo::class)->findAll() as $todo) { $todo->setDone(true); $this->publisher->publishEntityChange($todo, 'upsert'); } $this->em->flush(); return new JsonResponse(['ok' => true]); } } ``` In practice you usually don't need to call `publishEntityChange` manually — `flush()` triggers the Doctrine subscriber, which calls it for every changed entity. Direct calls are for cases where the model state isn't in Doctrine (e.g. a read model fed from a cache). --- ## `CorrelationContext` Request-scoped service holding the current `Idempotency-Key`. The bundle's `CorrelationKeyListener` reads the request header into it; `ModelPublisher` reads it back when emitting events. ```php namespace PhpQml\Bridge; final class CorrelationContext { public function set(?string $key): void; public function get(): ?string; public function clear(): void; } ``` You rarely need to touch this — it auto-plumbs the `correlationKey` field on every `ModelPublisher` event. Inject it if you're writing a custom controller that publishes through some other mechanism and wants to thread the same key through. ```php final class CustomController { public function __invoke(CorrelationContext $ctx, /* … */): JsonResponse { $key = $ctx->get(); // → "01HX…" (uuid set by the QML client) // … } } ``` --- ## `bridge:doctor` Console command. Verifies a dev environment is set up correctly. ```bash bin/console bridge:doctor bin/console bridge:doctor --connect # also probe BRIDGE_URL ``` Default checks: - Bundle is registered. - Mercure URL reachable. - `BRIDGE_TOKEN` configured. - `MERCURE_JWT_SECRET` length is ≥256 bits (lcobucci/jwt's minimum). - SQLite database exists / can be created. - Doctrine connection works. - No pending migrations. `--connect` adds: - HTTP probe against `BRIDGE_URL`. - Mercure subscribe + publish round-trip with a uuid topic. Exit code `0` if everything passes, non-zero otherwise. CI runs this as part of the `quality` target. --- ## Event subscribers These run automatically; documented for awareness. ### `BridgeResourceLifecycleSubscriber` Listens to Doctrine's `postPersist` / `postUpdate` / `postRemove`. For each affected entity, checks whether it carries `#[BridgeResource]`; if so, calls `ModelPublisher::publishEntityChange($entity, $op)`. To opt out for a specific operation (e.g. you don't want to publish a soft-delete that flips a flag), don't tag the entity with `#[BridgeResource]` — but then your QML side also stops getting events. Better: keep the attribute, accept the publish; QML clients can ignore events they don't need. ### `CorrelationKeyListener` Listens to `kernel.request`. Reads `Idempotency-Key` from the headers into `CorrelationContext`. Listens to `kernel.terminate` to clear the context (request-scoped). Header is propagated through `ModelPublisher` regardless of whether the request body actually triggered any persist — so a `POST /api/mark-all-done` that flushes 100 entities propagates the same `Idempotency-Key` to all 100 events. --- ## `SessionAuthenticator` Custom Symfony Security authenticator wired into `framework/skeleton/config/packages/security.yaml`. Validates the `Authorization: Bearer ` header against the configured `BRIDGE_TOKEN`. Anonymous in dev mode; per-session token in bundled mode. If you want to layer real user authentication on top (e.g. an app that has multiple human users), add a second authenticator higher in the stack — `BRIDGE_TOKEN` is the *transport* secret between Qt host and FrankenPHP child, not an end-user credential. --- ## Layout & wiring ``` framework/php/ ├── src/ │ ├── BridgeBundle.php bundle registration │ ├── Attribute/BridgeResource.php │ ├── ModelPublisher.php dual-publish + version increment │ ├── Publisher.php thin Mercure facade │ ├── CorrelationContext.php request-scoped key holder │ ├── SessionAuthenticator.php BRIDGE_TOKEN check │ ├── EventSubscriber/ │ │ └── CorrelationKeyListener.php request → context │ ├── EventListener/ Doctrine + Symfony listeners │ ├── Command/ │ │ └── BridgeDoctorCommand.php bridge:doctor │ ├── Controller/ (skeleton route resource lives here) │ └── Maker/ symfony/maker-bundle integrations ├── config/services.yaml service wiring └── tests/ unit + integration + maker snapshot ``` Service config follows Symfony conventions: autowire on; constructor-injected dependencies; no static state. The bundle has no PHP-side configuration tree at the moment — the bundled FrankenPHP / Caddyfile / env vars supply everything. ## See also - [Update semantics](update-semantics.md) — what `correlationKey` is and how it interacts with QML rollback. - [Reactive models](reactive-models.md) — what the QML side does with the events. - [Makers](makers.md) — what the `#[BridgeResource]` attribute is generated alongside.