Establishes the contract layer the rest of v0.2.0 builds on. Pre-1.0
SemVer break: ModelPublisher::publishEntityChange() now takes BridgeOp
instead of a raw string.
Interfaces (Symfony idiom: same namespace as concrete, like HubInterface
next to Hub):
- PublisherInterface — publish(string, array, bool)
- ModelPublisherInterface — publishEntityChange(object, BridgeOp)
- CorrelationContextInterface — set/get/clear
App code should typehint these instead of the concretes so swappable
implementations (offline-buffer publisher, multi-hub fan-out, request-
stamp correlation) remain non-breaking. Concrete classes implement them
unchanged; autowire continues to inject the implementations transparently.
BridgeOp: PHP 8.1 string-backed enum with cases Upsert / Delete /
Replace / Event matching PLAN.md §4's envelope `op` wire format.
Internal call sites updated; tests use the cases directly.
Switched typehints:
- ModelPublisher ctor: PublisherInterface + CorrelationContextInterface
- DoctrineBridgeListener ctor: ModelPublisherInterface
- HealthController ctor: PublisherInterface (still emits `Publisher`
as bundle canary value — `::class` resolves to the concrete class
name regardless of typehint, so bundled-supervisor.sh's grep stays
green)
- skeleton PingController ctor: PublisherInterface (canonical app
pattern — example/todo has no Publisher consumer to update)
Drive-by: removed deprecated setAccessible(true) call in
ModelPublisher::extractId — PHP 8.1+ allows reflection without it.
PHPStan + cs-fixer + PHPUnit (17/17) + maker snapshot all pass; dev
container compiles in the example app.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
php-qml
A framework for native desktop applications with a Symfony / FrankenPHP backend and a Qt / QML frontend, packaged as a single distributable per OS.
Status: Phase 5 / pre-v0.1.0. Phases 0–4a are merged (working framework, real POC, Linux AppImage, auto-update, release CI). macOS and Windows packaging are deferred to 4b/4c. See CHANGELOG.md.
What it is
php-qml lets a PHP developer write a desktop app using ordinary Symfony on the backend and ordinary QML on the frontend. The two halves run as a process pair inside one bundled binary:
- A Qt/QML host owns the window, input, and rendering.
- A bundled FrankenPHP child runs a Symfony app in worker mode.
- They communicate over a local socket — HTTP for commands and queries, Mercure SSE for state push.
It is not a PHP↔Qt language binding — the languages run in separate processes; the bridge is a wire protocol, not an FFI layer. That deliberately avoids the failure mode that left php-gtk and php-qt unmaintained.
60-second tour
git clone https://src.bundespruefstelle.ch/magdev/php-qml && cd php-qml
# Scaffold a fresh app
./bin/php-qml-init my-app
# Run it
cd my-app
make doctor # readiness check
make dev # FrankenPHP --watch + Qt host
Add a reactive resource (entity + REST controller + QML snippet) with one maker:
cd my-app/symfony
bin/console make:bridge:resource Todo
bin/console make:migration && bin/console doctrine:migrations:migrate -n
make dev opens the Qt window, connection state flips to Online, and the generated TodoList.qml shows a list whose ReactiveListModel is auto-subscribed to app://model/todo over Mercure. There is no handwritten cross-side glue.
For a non-trivial app with a multi-window test, crash-recovery test, and AppImage packaging, see examples/todo/.
Documentation
The full developer documentation lives under docs/:
- Getting started — prerequisites by distro, first project, troubleshooting.
- Architecture — process pair, transport, dev vs bundled mode.
- Update semantics — connection state machine, optimistic mutations, idempotency.
- Reactive models —
ReactiveListModel,ReactiveObject, Mercure dual-publish. - Makers —
make:bridge:resource/command/window. - Dev workflow — hot reload, dev console, editor setup,
bridge:doctor. - Bundled mode — supervisor, per-session secret rotation, first-launch migrations.
- Linux packaging —
make appimage, auto-update, performance budgets. - Configuration reference — env vars, CLI flags.
- QML API reference / PHP API reference — singletons, components, attributes, services.
Design rationale and roadmap live in PLAN.md. User-facing changes per release are in CHANGELOG.md.
Tech stack
PHP 8.4+ · Symfony 8 · Doctrine ORM 3 · FrankenPHP 1.12+ (worker mode) · Mercure · Qt 6.5+ · CMake · Composer · Gitea Actions
Roadmap
- Phase 0 ✅ throwaway transport spike.
- Phase 1 ✅ framework skeleton, dev mode, single-instance lock, CI quality gate.
- Phase 2 ✅ reactive models, update semantics, headline maker.
- Phase 3 ✅ POC todo app, integration + snapshot tests.
- Phase 4a ✅ bundled mode, Linux AppImage, release CI, AppImageUpdate.
- Phase 4b/4c ⏳ macOS / Windows packaging.
- Phase 5 🚧 DX polish — dev console, init script, hot-reload docs, v0.1.0 prep.
Contributing
Active development happens on the dev branch; main only carries release commits. Pull requests target dev.
cd framework/php && composer quality # PHPStan + cs-fixer + PHPUnit
cd examples/todo && make quality # adds qmllint + integration test
A dedicated CONTRIBUTING.md arrives with Phase 5's wrap-up.
Versioning
Semantic Versioning — MAJOR.MINOR.BUGFIX. Pre-v1.0.0, minor bumps may break public API.
License
To be decided before v0.1.0 is tagged. The framework's own code will be permissively licensed; Qt is shipped under LGPL with relinkability obligations — see PLAN.md §12.