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>
8.9 KiB
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:
return [
PhpQml\Bridge\BridgeBundle::class => ['all' => true],
];
At a glance
| Symbol | Kind | Use it when… |
|---|---|---|
#[BridgeResource] |
PHP attribute | You want a Doctrine entity to dual-publish on Mercure automatically. |
ModelPublisher |
Service | You want to publish a custom event without persist/update/remove. |
CorrelationContext |
Service | You're inside a non-controller code path and need the current request's Idempotency-Key. |
bridge:doctor |
Console command | You want to verify the dev environment is wired correctly. |
SessionAuthenticator |
Security authenticator | (Internal) checks the Authorization: Bearer header against BRIDGE_TOKEN. |
CorrelationKeyListener |
Event subscriber | (Internal) reads Idempotency-Key into CorrelationContext. |
#[BridgeResource] attribute
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:
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 — forReactiveListModel).app://model/<name>/<id>(entity topic — forReactiveObject).
The maker (make:bridge:resource) attaches this attribute automatically.
Custom resource name
#[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
idfield, 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).
namespace PhpQml\Bridge;
final class ModelPublisher
{
public function publishEntityChange(object $entity, string $op): void;
}
$op is one of "upsert" / "delete". The published JSON payload:
{
"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
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.
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.
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.
bin/console bridge:doctor
bin/console bridge:doctor --connect # also probe BRIDGE_URL
Default checks:
- Bundle is registered.
- Mercure URL reachable.
BRIDGE_TOKENconfigured.MERCURE_JWT_SECRETlength 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 — what
correlationKeyis and how it interacts with QML rollback. - Reactive models — what the QML side does with the events.
- Makers — what the
#[BridgeResource]attribute is generated alongside.