resolveFrankenphpBin() returned `applicationDirPath() + "/bin/frankenphp"`, but applicationDirPath() inside an AppImage is already `usr/bin/` — where the host binary itself runs from. The "/bin/" prefix produced the doubled path, e.g.: /tmp/appimage_extracted_<hash>/usr/bin/bin/frankenphp …which doesn't exist. The supervisor logged the lookup failure and kept retrying, eventually hitting the perfsmoke 8s cold-start deadline. build-appimage.sh:148 installs frankenphp at `usr/bin/frankenphp` (sibling of the host binary, per the layout comment at line 18). The fix is to drop the spurious `/bin/`. Other resolvers in the same file (resolveSymfonyDir, resolveCaddyfilePath) already use the correct `here + "/<file>"` pattern. Bug shipped since the bundled-mode supervisor was added — would have hit anyone running the AppImage. Local `make quality` only exercises dev mode (BRIDGE_URL set), so the integration test loop never reached this codepath; CI's perfsmoke against the actual AppImage is the only thing that catches it. Manual test would be: launch the AppImage with BRIDGE_FRANKENPHP_BIN unset and watch the phpqml.bridge.bundled log. 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.