CI was failing on the Install-bundle-dependencies step because
shivammathur/setup-php was installing 8.3 while Symfony 8.x dependencies
declare php >= 8.4. Local composer install worked because the dev box
runs PHP 8.5.5; CI doesn't.
Bumps:
- framework/php/composer.json
- framework/skeleton/symfony/composer.json
- examples/todo/symfony/composer.json
- .gitea/workflows/ci.yml php-version: '8.3' → '8.4'
- .gitea/workflows/release.yml same
- PLAN.md §13 Phase 1 *Detailed scope* PHP minimum row
PHPStan / cs-fixer / PHPUnit stay green locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
packaging/linux/build-appimage.sh produces a single-file Linux
AppImage from a built host + Symfony tree + FrankenPHP binary.
Auto-downloads (cached in tools/, gitignored) the three pieces of
upstream tooling:
- linuxdeploy + linuxdeploy-plugin-qt — gathers Qt runtime and QML
modules into the AppDir, and bundles the offscreen platform
plugin via EXTRA_PLATFORM_PLUGINS so headless CI can smoke it.
- appimagetool — squashes the AppDir into the .AppImage.
- runtime-x86_64 — appimagetool's prepended runtime stub, fetched
once and passed via --runtime-file (ad-hoc downloads stalled on
some networks).
The two stages are kept separate (linuxdeploy stages, then we invoke
appimagetool ourselves) so failures are observable rather than
swallowed by linuxdeploy's bundled-tool path.
AppDir layout matches BackendConnection's resolve* fallbacks:
AppDir/usr/bin/<app>
AppDir/usr/bin/frankenphp
AppDir/usr/share/<app>/symfony/
AppDir/usr/share/<app>/Caddyfile
examples/todo gets `make appimage`: stages a no-dev composer install
into build/staging-symfony, points the path repo at the bundle's
absolute path so Composer can find php-qml/bridge from the staging
dir, then drives build-appimage.sh. Output:
build/Todo-x86_64.AppImage (~104 MB).
Verified locally: `make appimage` produces a working AppImage; mount
+ inspect + extract all clean. Headless run requires the bundled
offscreen plugin (now wired); a real desktop launches it normally.
Includes a 64×64 placeholder PNG icon (todo.png) and a minimal
.desktop file for the example.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
framework/php/tests/snapshot/ holds reference output for every shipped
maker (resource Todo, command MarkAllDone, window Todo). The
run.sh script:
- git-archives the skeleton into a temp dir
- composer-installs against the bundle's real path
- removes the existing maker outputs so the regenerators don't bail
- runs the three makers
- diffs each generated file against the matching baseline
CI / make quality fail on any drift; if a template change is intended,
the baselines must be regenerated in the same commit. Wired into:
- framework/skeleton/Makefile's `quality` target (local/dev runs)
- .gitea/workflows/ci.yml (CI runs after qmllint)
Plus a few hardenings discovered while wiring this up:
- The resource maker template now injects NormalizerInterface
(not SerializerInterface — that interface lacks ::normalize()).
All Todo controllers re-rendered to match.
- The command maker template emits a $this->em->flush() so the
injected EntityManager isn't a property.onlyWritten violation
in PHPStan after the user fills in the body.
- phpstan.neon and php-cs-fixer's Finder both exclude tests/snapshot
so the baselines aren't auto-rewritten or analysed as live code.
CI workflow now also installs FrankenPHP, builds the todo example, and
runs the bridge-integration test from Phase 3 sub-commit 4.
Phase 3 done. Outstanding follow-ups (deferred per spec): the
qmltestrunner-driven QML unit tests, make:bridge:event,
make:bridge:read-model, ReactiveObject pagination.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
examples/todo/tests/integration.sh boots FrankenPHP against the example
app on an isolated port (8767) and database (tests/var/integration.sqlite),
then asserts:
- POST /api/todos creates a row, GET returns it.
- SSE stream on app://model/todo carries the §4 envelope with op,
correlationKey echoed from Idempotency-Key, and the JSON payload.
- The backend log shows ≥2 "Update published" lines per change
(collection topic + entity topic — dual publish per ModelPublisher).
- Killing FrankenPHP makes /healthz unreachable; restarting it
restores GET access without losing data.
Wired into make quality alongside the existing PHPStan / cs-fixer /
PHPUnit / qmllint checks. The script is self-contained — runs against
the example without disturbing a developer's `make dev` instance.
qmltestrunner integration deferred: out-of-the-box runner can't see
PhpQml.Bridge because the framework module is statically linked into
the host binary. A proper QML test target would need a custom CMake
executable that links the module + uses QtQuickTest's quick_test_main.
Phase 3.x or Phase 5 polish.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Standalone Composer/CMake project under examples/todo/ derived from the
skeleton, demonstrating every Phase 3 architectural primitive in a
non-trivial app. All cross-side wiring is maker-generated; no
handwritten bridge glue.
Generated and customised:
- src/Entity/Todo.php — make:bridge:resource Todo (UUIDv7 id)
- src/Controller/TodoController.php — make:bridge:resource Todo (CRUD)
- src/Controller/MarkAllDoneController.php — make:bridge:command
MarkAllDone, body filled in to flip done=true on every row
- qml/TodoList.qml — make:bridge:resource Todo (starter ListView)
- qml/TodoWindow.qml — make:bridge:window Todo, body customised to
embed a read-only mirror of the same ReactiveListModel
The Phase 1 ping demo is dropped from this app — it doesn't fit the
todo flow and nothing in Main.qml references it.
Main.qml is the real list UI:
- Add input + button (POST /api/todos with optimistic-friendly key).
- Per-row CheckBox + delete button (PATCH/DELETE via
todoModel.invoke() with `pending` role driving opacity).
- "Mark all done" button (POST /api/mark-all-done).
- "Open second window" button (Component { TodoWindow {} } pattern).
Build / run delegated to the same Makefile shape as the skeleton, with
SCRIPT_DIR/QT_BIN updated for the renamed binary (build/qml/todo).
composer.json's path repo points at ../../../framework/php (one level
deeper than the skeleton's path repo).
Verified end-to-end with offscreen QPA: POST/PATCH/DELETE on /api/todos
all round-trip, /api/mark-all-done flips every row, Mercure dual-
publishes on every change. Clean shutdown.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>