Files
php-qml/framework/skeleton
magdev a1cc06abbb Phase 4a sub-commit 1: bundled-mode startup in BackendConnection
Auto-detected on construction:

  - BRIDGE_URL env set → dev mode (today's behaviour, unchanged).
  - BRIDGE_URL unset → bundled mode: BackendConnection now
    1. Resolves the user app data dir (QStandardPaths::AppDataLocation,
       ~/.local/share/<org>/<app> on Linux) and ensures var/, var/log/,
       var/cache/ exist there.
    2. Generates a per-session 32-byte URL-safe token and a 48-byte
       Mercure JWT secret.
    3. Runs `frankenphp php-cli bin/console doctrine:migrations:migrate -n`
       against the user's DATABASE_URL with a 60s timeout.
    4. Spawns FrankenPHP via QProcess with BRIDGE_TOKEN/MERCURE_*/PORT
       in the env, prctl(PR_SET_PDEATHSIG, SIGTERM) on the child, and
       a supervisor that re-spawns up to 5 times on unexpected exit.
       Each restart fires tokenRotated(newToken).

Path resolution defaults to applicationDirPath() + bin/frankenphp,
applicationDirPath() + symfony, applicationDirPath() + Caddyfile, with
both `/../share/<app>/...` and `/../usr/share/<app>/...` fallbacks for
AppImage-style layouts. All three are overridable via
BRIDGE_FRANKENPHP_BIN / BRIDGE_SYMFONY_DIR / BRIDGE_CADDYFILE env vars.

Caddyfiles in skeleton + example now use {$VAR:default} substitution
for PORT and the Mercure JWT keys, so the same Caddyfile works in both
modes. Dev defaults match symfony/.env.

restart() in bundled mode re-spawns the child (resets the supervisor
counter); in dev mode it stays a probe-only no-op.

Smoke-tested locally with `BRIDGE_FRANKENPHP_BIN=…  BRIDGE_SYMFONY_DIR=…
BRIDGE_CADDYFILE=…  ./build/qml/todo` (no BRIDGE_URL): bundled mode
created ~/.local/share/php-qml/todo/var/data.sqlite, ran the migration,
spawned FrankenPHP, served /healthz, accepted a POST /api/todos with
the per-session bearer. Dev mode (`make dev`) still works unchanged.

Includes a `phpqml.bridge.bundled` Q_LOGGING_CATEGORY so failures
surface to the user; enable with QT_LOGGING_RULES='phpqml.bridge.bundled.*=true'.

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

php-qml skeleton

The framework's reference application: a minimal Symfony backend plus a Qt/QML host, wired together exactly the way php-qml/bridge is intended to be used. Use this as a starting point or copy parts into your own project.

Prerequisites

  • Linux (other platforms land in Phase 4)
  • Qt 6.5+ dev packages (qt6-base-devel, qt6-declarative-devel, qt6-quickcontrols2-devel, qt6-tools-devel), CMake, gcc-c++
  • PHP 8.3+
  • FrankenPHP on PATH (or set FRANKENPHP=/path/to/frankenphp)
  • Composer

First run

make install        # composer install in symfony/
make build          # cmake + qt host
make doctor         # bridge:doctor — readiness check
make dev            # FrankenPHP --watch + Qt host, tears both down on Ctrl-C

make dev opens a Qt window, status dots flip to green, and the Ping button round-trips through /api/ping and back via Mercure SSE.

Adding a reactive resource

The headline workflow — add a Doctrine entity that ends up reactive in QML with three commands and zero handwritten glue:

cd symfony

# 1) Generate entity + controller + QML snippet
bin/console make:bridge:resource Todo
#   created: src/Entity/Todo.php          # #[BridgeResource] + UUIDv7
#   created: src/Controller/TodoController.php
#   created: ../qml/TodoList.qml

# 2) Schema migration (Doctrine reads the entity)
bin/console make:migration
bin/console doctrine:migrations:migrate -n

# 3) Use the generated TodoList.qml from your QML window.

That's it. The bundle's Doctrine subscriber automatically dual-publishes postPersist/postUpdate/postRemove events to:

  • app://model/todo (collection topic, watched by ReactiveListModel)
  • app://model/todo/{id} (entity topic, watched by ReactiveObject — Phase 3)

The ReactiveListModel in TodoList.qml does an initial GET /api/todos, subscribes to the collection topic, and applies diffs to its rows as they arrive. Adding --int-id switches the maker to auto-incrementing integer IDs.

Verifying end-to-end

With make dev running, post a todo from another terminal and watch it appear:

curl -X POST http://127.0.0.1:8765/api/todos \
  -H 'Authorization: Bearer devtoken' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: my-key-1' \
  -d '{"title":"buy milk","done":false}'

The Mercure SSE stream receives a correlationKey: my-key-1 envelope which the Qt host's ReactiveListModel matches against any in-flight optimistic mutation (PLAN.md §5).

Quality checks

make quality        # PHPStan + php-cs-fixer (check) + PHPUnit + qmllint

The same set CI runs (.gitea/workflows/ci.yml).

Layout

skeleton/
  Caddyfile         # FrankenPHP / Mercure config (dev mode)
  Makefile          # build / dev / doctor / quality
  scripts/dev.sh    # process-group teardown for FrankenPHP + Qt host
  symfony/          # the Symfony app (composer project)
    config/
    src/Entity/     # filled by `make:bridge:resource`
    src/Controller/ # filled by `make:bridge:resource`
    public/index.php
    bin/console
  qml/              # the Qt host
    CMakeLists.txt
    main.cpp
    Main.qml
    <Name>List.qml  # filled by `make:bridge:resource` (relative to symfony/)

Where to go next

  • The framework code lives one level up: framework/php (the bundle) and framework/qml (the Qt module). Both are linked into this skeleton via Composer's path repository / CMake's add_subdirectory, so edits there are picked up live.
  • The full design story is in PLAN.md.
  • Phase 3 wires this skeleton into a real POC todo application with a multi-window test and packaging-CI dry runs.