Files
php-qml/README.md
magdev 26a2b3771b Phase 5 sub-commit 4: release readiness — README + CHANGELOG + status line
README.md rewritten to reflect actual onboarding (clone → php-qml-init
→ make dev / make appimage) instead of the planning-stage placeholder.
Phase status checklist now reflects 0–4a green and 5 in progress.

CHANGELOG.md created at repo root following Keep-a-Changelog
conventions, with a v0.1.0 entry that summarises Phases 0–4a plus the
Phase 5 polish work (DevConsole, php-qml-init, editor configs,
hot-reload docs). Date is TBD; tagging is the user's call.

PLAN.md gains a Status banner so it's obvious at a glance which phase
the implementation tracks against the design.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 21:30:08 +02:00

6.0 KiB
Raw Blame History

php-qml

A framework for building 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 04a are merged: working framework skeleton, reactive models, the headline make:bridge:* makers, a real POC (examples/todo), Linux AppImage packaging, AppImageUpdate auto-update, performance smoke harness, release CI on v* tags. Full design lives in PLAN.md and the per-version log in CHANGELOG.md.

What's not done yet: macOS and Windows packaging (Phase 4b/4c).

What it is

php-qml lets a PHP developer write a desktop application 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 process owns the window, input, and rendering.
  • A bundled FrankenPHP child runs a Symfony application in worker mode.
  • They communicate over a local socket — HTTP for commands and queries, Mercure SSE for state push.

The framework provides the lifecycle, transport, reactive models, and scaffolding so application code stays idiomatic on both sides.

What it is not

Not a PHP↔Qt language binding. It does not embed PHP into a Qt event loop and it does not generate Qt classes from PHP. The two languages run in separate processes; the bridge is a wire protocol, not an FFI layer.

If you've watched php-gtk and php-qt go quiet, that is the failure mode this project deliberately avoids — the framework owns the boring parts (lifecycle, transport, conventions) so it doesn't depend on a single maintainer keeping a language binding alive.

Tech stack

  • Backend: PHP 8.4+, Symfony 8, Doctrine ORM 3, FrankenPHP (worker mode), Mercure
  • Frontend: Qt 6.5+, QML, C++ plugin where required
  • Build: CMake, Composer
  • CI: Gitea Actions, Gitea Releases
  • Packaging: Linux AppImage today; macOS (.app + .dmg) and Windows (NSIS / MSIX) on the roadmap

Quick start

Prerequisites: Qt 6.5+ dev packages, CMake, gcc-c++, PHP 8.4+, Composer, FrankenPHP on PATH.

git clone https://gitea.example/<you>/php-qml
cd php-qml

# 1) Scaffold a fresh app (auto-detects this checkout as the framework)
./bin/php-qml-init my-app

# 2) Boot it
cd my-app
make doctor      # bridge:doctor — readiness check
make dev         # FrankenPHP --watch + Qt host

make dev opens the Qt window, connection state flips to Online, and a Ping button round-trips through /api/ping and back via Mercure SSE.

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

The QML side gets a TodoList.qml whose ReactiveListModel does an initial GET, subscribes to app://model/todo, and applies diffs as Mercure pushes them. There is no handwritten cross-side glue.

For a non-trivial example with a multi-window test, crash-recovery test, and AppImage packaging, see examples/todo/.

Packaging (Linux)

cd examples/todo
FRANKENPHP=/path/to/frankenphp make appimage
./build/Todo-x86_64.AppImage

make appimage bundles Qt, the Symfony app, the FrankenPHP binary, and the AppImageUpdate sidecar into one ~150 MB AppImage that auto-detects bundled mode (no BRIDGE_URL set), spawns its own FrankenPHP child, generates a per-session bearer token, and runs first-launch migrations. CI builds this on every v* tag and uploads to Gitea Releases with a latest.json appcast and zsync metadata for in-place updates. See PLAN.md §13 Phase 4a for the full pipeline.

Project structure

bin/
  php-qml-init        # bash scaffolder — copies the skeleton into a new project
framework/
  php/                # the Symfony bundle (php-qml/bridge)
  qml/                # the Qt module (PhpQml.Bridge): BackendConnection, ReactiveListModel, …
  skeleton/           # reference application — what php-qml-init copies
examples/
  todo/               # POC todo app exercising every primitive
packaging/linux/      # build-appimage.sh + AppImageUpdate sidecar wiring
.gitea/workflows/     # ci.yml (PR gate) + release.yml (v* tags)
PLAN.md               # design source of truth
CHANGELOG.md          # release log

See PLAN.md §9 for the conceptual layout.

Roadmap

Six phases, each ending with something runnable. Detail in PLAN.md §13.

  • Phase 0 throwaway spike — prove transport on Linux.
  • Phase 1 framework skeleton, dev mode, single-instance lock, CI quality gate.
  • Phase 2 reactive models, update semantics, headline maker (make:bridge:resource).
  • Phase 3 POC todo application generated via the makers; testing infrastructure.
  • Phase 4a bundled mode, Linux AppImage, release CI, AppImageUpdate auto-update, performance smoke.
  • Phase 4b/4c macOS and Windows packaging.
  • Phase 5 🚧 DX polish (in progress) — 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.

Quality gate that CI runs:

cd framework/php && composer quality   # PHPStan + cs-fixer (check) + PHPUnit
cd examples/todo  && make quality      # adds qmllint + integration test

A dedicated CONTRIBUTING.md will arrive alongside Phase 5's wrap-up.

Versioning

Semantic VersioningMAJOR.MINOR.BUGFIX. MAJOR for breaking changes, MINOR for backwards-compatible features, BUGFIX for backwards-compatible fixes. 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; note that Qt is shipped under LGPL and that carries obligations for distributors — see PLAN.md §12 (Qt LGPL relinkability).