README is now tight and link-heavy: 60-second tour, then deep links into docs/. The wall of detail moved out. docs/ covers the framework end-to-end: - getting-started.md — prerequisites by distro (Tumbleweed, Fedora, Debian/Ubuntu, Arch), full first-run walkthrough, troubleshooting. - architecture.md — process pair, transport, dev/bundled mode. - update-semantics.md — state machine + optimistic mutations + key round-tripping. - reactive-models.md — ReactiveListModel, ReactiveObject, Mercure dual-publish. - makers.md — make:bridge:resource/command/window. - dev-workflow.md — hot reload (PHP + QML), dev console, editor configs, bridge:doctor, snapshot/integration test loops, perfsmoke. - bundled-mode.md — supervisor, per-session secret rotation, first-launch migrations, auto-update wiring. - packaging-linux.md — make appimage, build-appimage.sh CLI, AppImageUpdate sidecar, perfsmoke budgets, release CI, bundle-size breakdown. - qml-api.md / php-api.md — exhaustive symbol reference with all Q_PROPERTY/Q_INVOKABLE/signals + every public PHP service / attribute / command. - configuration.md — every env var (host, Symfony, dev script, packaging script, perfsmoke), every CLI flag (php-qml-init, build-appimage.sh), make targets, default ports/paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
242 lines
8.9 KiB
Markdown
242 lines
8.9 KiB
Markdown
# 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 `<name>` in `app://model/<name>` and `app://model/<name>/<id>` topics. |
|
|
|
|
After tagging, every `postPersist` / `postUpdate` / `postRemove` event triggers a dual-publish via `ModelPublisher`:
|
|
|
|
- `app://model/<name>` (collection topic — for `ReactiveListModel`).
|
|
- `app://model/<name>/<id>` (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/<id>`. 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": "<entity-id>",
|
|
"data": { /* normalised entity */ },
|
|
"correlationKey": "<from CorrelationContext or null>",
|
|
"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 <token>` 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.
|