docs: refresh README + docs/ for v0.2.0
The README still framed the project as "Phase 5 / pre-v0.1.0" and the docs predated the v0.2.0 surface (typed BridgeOp, public service interfaces, port negotiation, pre-migration auto-backup, bridge:export, periodic auto-update, two new makers, qmltestrunner). Bring them in line with what's actually shipped, and add badges (release, license, PHP, Symfony, Qt, FrankenPHP, CI, platform) to the README so the status is legible at a glance. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
164
docs/php-api.md
164
docs/php-api.md
@@ -15,9 +15,13 @@ return [
|
||||
| Symbol | Kind | Use it when… |
|
||||
| --- | --- | --- |
|
||||
| [`#[BridgeResource]`](#bridgeresource-attribute) | PHP attribute | You want a Doctrine entity to dual-publish on Mercure automatically. |
|
||||
| [`BridgeOp`](#bridgeop-enum) | Enum | You're calling `ModelPublisher::publishEntityChange` directly. |
|
||||
| [`PublisherInterface`](#publisherinterface) / [`ModelPublisherInterface`](#modelpublisherinterface) / [`CorrelationContextInterface`](#correlationcontextinterface) | Interfaces | You're typehinting bridge services in your own controllers / listeners. |
|
||||
| [`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`. |
|
||||
| [`BridgeBundleInfo`](#bridgebundleinfo) | Value object | You want a deep-load canary on the bundle (e.g. a custom `/healthz`). |
|
||||
| [`bridge:doctor`](#bridgedoctor) | Console command | You want to verify the dev environment is wired correctly. |
|
||||
| [`bridge:export`](#bridgeexport) | Console command | You want to copy the active SQLite database to a user-chosen path. |
|
||||
| [`SessionAuthenticator`](#sessionauthenticator) | Security authenticator | (Internal) checks the `Authorization: Bearer` header against `BRIDGE_TOKEN`. |
|
||||
| [`CorrelationKeyListener`](#correlationkeylistener) | Event subscriber | (Internal) reads `Idempotency-Key` into `CorrelationContext`. |
|
||||
|
||||
@@ -56,6 +60,26 @@ After tagging, every `postPersist` / `postUpdate` / `postRemove` event triggers
|
||||
|
||||
The maker (`make:bridge:resource`) attaches this attribute automatically.
|
||||
|
||||
### `BridgeOp` enum
|
||||
|
||||
Wire-format enum for `op` field on every Mercure event the bundle publishes:
|
||||
|
||||
```php
|
||||
namespace PhpQml\Bridge;
|
||||
|
||||
enum BridgeOp: string
|
||||
{
|
||||
case Upsert = 'upsert';
|
||||
case Delete = 'delete';
|
||||
case Replace = 'replace';
|
||||
case Event = 'event';
|
||||
}
|
||||
```
|
||||
|
||||
The string values are the on-the-wire format — QML clients hardcode them, so renaming a case (without changing its `value`) is safe; changing a `value` is a wire-protocol break (and `BridgeOpTest` will fail the build before it ships).
|
||||
|
||||
You only deal with this directly when calling `ModelPublisher::publishEntityChange` from a custom code path. The Doctrine subscriber, the makers, and the `#[BridgeResource]` plumbing pick the right case for you.
|
||||
|
||||
### Custom resource name
|
||||
|
||||
```php
|
||||
@@ -73,20 +97,67 @@ Topics become `app://model/task` and `app://model/task/<id>`. Useful when the st
|
||||
|
||||
---
|
||||
|
||||
## `ModelPublisher`
|
||||
## `PublisherInterface`
|
||||
|
||||
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).
|
||||
Thin Mercure facade. Concrete implementation: `Publisher` (autowired). Typehint the interface in app code so a swappable implementation (e.g. an offline-buffer publisher that queues events when Mercure is unreachable) stays non-breaking.
|
||||
|
||||
```php
|
||||
namespace PhpQml\Bridge;
|
||||
|
||||
final class ModelPublisher
|
||||
interface PublisherInterface
|
||||
{
|
||||
public function publishEntityChange(object $entity, string $op): void;
|
||||
public function publish(string $topic, array $data): void;
|
||||
}
|
||||
```
|
||||
|
||||
`$op` is one of `"upsert"` / `"delete"`. The published JSON payload:
|
||||
Mirrors upstream Symfony's `HubInterface`/`Hub` split. Existing call sites that typehint the concrete `Publisher` class keep working — autowire continues to inject the concrete implementation transparently.
|
||||
|
||||
## `ModelPublisherInterface`
|
||||
|
||||
The dual-publish surface, one level up from `PublisherInterface`. Concrete implementation: `ModelPublisher`.
|
||||
|
||||
```php
|
||||
namespace PhpQml\Bridge;
|
||||
|
||||
interface ModelPublisherInterface
|
||||
{
|
||||
public function publishEntityChange(object $entity, BridgeOp $op): void;
|
||||
}
|
||||
```
|
||||
|
||||
`DoctrineBridgeListener` typehints this interface, not the concrete class.
|
||||
|
||||
## `CorrelationContextInterface`
|
||||
|
||||
Request-scoped key holder; concrete implementation: `CorrelationContext`. See [`CorrelationContext`](#correlationcontext) for the contract.
|
||||
|
||||
```php
|
||||
namespace PhpQml\Bridge;
|
||||
|
||||
interface CorrelationContextInterface
|
||||
{
|
||||
public function set(?string $key): void;
|
||||
public function get(): ?string;
|
||||
public function clear(): void;
|
||||
}
|
||||
```
|
||||
|
||||
## `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 (or `ModelPublisherInterface`) directly when you want to publish a *custom* event (e.g. progress on a long-running command).
|
||||
|
||||
```php
|
||||
namespace PhpQml\Bridge;
|
||||
|
||||
final class ModelPublisher implements ModelPublisherInterface
|
||||
{
|
||||
public function publishEntityChange(object $entity, BridgeOp $op): void;
|
||||
}
|
||||
```
|
||||
|
||||
> **API break in v0.2.0:** the second arg used to be `string $op`. It is now the typed `BridgeOp` enum — typo'd ops are caught at compile time instead of silently producing envelopes clients ignore. Migration: replace raw `'upsert'` / `'delete'` strings with `BridgeOp::Upsert` / `BridgeOp::Delete`.
|
||||
|
||||
`$op` is one of the `BridgeOp` cases. The published JSON payload:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -106,15 +177,15 @@ final class ModelPublisher
|
||||
final class MarkAllDoneController
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManagerInterface $em,
|
||||
private ModelPublisher $publisher,
|
||||
private EntityManagerInterface $em,
|
||||
private ModelPublisherInterface $publisher,
|
||||
) {}
|
||||
|
||||
public function __invoke(): JsonResponse
|
||||
{
|
||||
foreach ($this->em->getRepository(Todo::class)->findAll() as $todo) {
|
||||
$todo->setDone(true);
|
||||
$this->publisher->publishEntityChange($todo, 'upsert');
|
||||
$this->publisher->publishEntityChange($todo, BridgeOp::Upsert);
|
||||
}
|
||||
$this->em->flush();
|
||||
return new JsonResponse(['ok' => true]);
|
||||
@@ -128,25 +199,14 @@ In practice you usually don't need to call `publishEntityChange` manually — `f
|
||||
|
||||
## `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;
|
||||
}
|
||||
```
|
||||
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. Implements [`CorrelationContextInterface`](#correlationcontextinterface).
|
||||
|
||||
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
|
||||
public function __invoke(CorrelationContextInterface $ctx, /* … */): JsonResponse
|
||||
{
|
||||
$key = $ctx->get(); // → "01HX…" (uuid set by the QML client)
|
||||
// …
|
||||
@@ -156,6 +216,34 @@ final class CustomController
|
||||
|
||||
---
|
||||
|
||||
## `BridgeBundleInfo`
|
||||
|
||||
Value object carrying the bundle's name + class FQCN. Used by `HealthController` as the deep-load canary on `/healthz` — if the container can construct `BridgeBundleInfo`, the bundle is wired up correctly.
|
||||
|
||||
```php
|
||||
namespace PhpQml\Bridge;
|
||||
|
||||
final readonly class BridgeBundleInfo
|
||||
{
|
||||
public function __construct(
|
||||
public string $name, // 'php-qml/bridge'
|
||||
public string $class, // PhpQml\Bridge\BridgeBundle::class
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
App code rarely injects this directly — but if you're rolling a custom `/healthz` endpoint and want the same canary semantic without coupling to `Publisher` (which `/healthz` used to do pre-v0.2.0), this is the shape to typehint.
|
||||
|
||||
`/healthz` response shape (changed in v0.2.0):
|
||||
|
||||
```json
|
||||
{ "status": "ok", "name": "php-qml/bridge", "bundle": "PhpQml\\Bridge\\BridgeBundle" }
|
||||
```
|
||||
|
||||
Pre-v0.2.0 the `bundle` field was `PhpQml\Bridge\Publisher`. Consumers asserting that exact value need to migrate; consumers reading any-truthy / unknown-keys-ok are unaffected.
|
||||
|
||||
---
|
||||
|
||||
## `bridge:doctor`
|
||||
|
||||
Console command. Verifies a dev environment is set up correctly.
|
||||
@@ -184,6 +272,26 @@ Exit code `0` if everything passes, non-zero otherwise. CI runs this as part of
|
||||
|
||||
---
|
||||
|
||||
## `bridge:export`
|
||||
|
||||
Console command. Copies the active SQLite database to a user-chosen path.
|
||||
|
||||
```bash
|
||||
bin/console bridge:export /home/me/backup-2026-05-03.sqlite
|
||||
# → wrote 1245184 bytes to /home/me/backup-2026-05-03.sqlite
|
||||
```
|
||||
|
||||
Behaviour:
|
||||
|
||||
- Reads the source path from `DATABASE_URL`. Works in dev and bundled mode without configuration.
|
||||
- Overwrites the destination if it exists.
|
||||
- Errors with exit code `1` if `DATABASE_URL` doesn't point at a SQLite file (`sqlite:///…`), or the source file doesn't exist.
|
||||
- Mirrored on the QML side as [`BackendConnection.exportDatabase(path)`](qml-api.md#exportdatabase) — apps typically pair the QML hook with `Qt.labs.platform.FileDialog` so the user picks a destination natively (see [Native dialogs §file pickers](native-dialogs.md#file-pickers)).
|
||||
|
||||
This is the export half of a "backup my data" UX. The restore half is just `cp <backup> <data-dir>/data.sqlite` while the app is closed; bundled mode also keeps automatic [pre-migration backups](bundled-mode.md#pre-migration-auto-backup) for the migration-corruption case.
|
||||
|
||||
---
|
||||
|
||||
## Event subscribers
|
||||
|
||||
These run automatically; documented for awareness.
|
||||
@@ -215,7 +323,12 @@ If you want to layer real user authentication on top (e.g. an app that has multi
|
||||
```
|
||||
framework/php/
|
||||
├── src/
|
||||
│ ├── BridgeBundle.php bundle registration
|
||||
│ ├── BridgeBundle.php bundle registration + DI extension
|
||||
│ ├── BridgeBundleInfo.php deep-load canary value object
|
||||
│ ├── BridgeOp.php wire-format enum
|
||||
│ ├── PublisherInterface.php ─┐
|
||||
│ ├── ModelPublisherInterface.php │ public service interfaces
|
||||
│ ├── CorrelationContextInterface.php ─┘
|
||||
│ ├── Attribute/BridgeResource.php
|
||||
│ ├── ModelPublisher.php dual-publish + version increment
|
||||
│ ├── Publisher.php thin Mercure facade
|
||||
@@ -225,9 +338,12 @@ framework/php/
|
||||
│ │ └── CorrelationKeyListener.php request → context
|
||||
│ ├── EventListener/ Doctrine + Symfony listeners
|
||||
│ ├── Command/
|
||||
│ │ └── BridgeDoctorCommand.php bridge:doctor
|
||||
│ │ ├── BridgeDoctorCommand.php bridge:doctor
|
||||
│ │ └── BridgeExportCommand.php bridge:export
|
||||
│ ├── Controller/ (skeleton route resource lives here)
|
||||
│ └── Maker/ symfony/maker-bundle integrations
|
||||
│ ├── Maker/ symfony/maker-bundle integrations
|
||||
│ │ └── Support/ shared helpers (NameInput, Naming)
|
||||
│ └── ReadModel/ (apps' read-model query services land here)
|
||||
├── config/services.yaml service wiring
|
||||
└── tests/ unit + integration + maker snapshot
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user