PLAN.md §12 *Data backup / export* called for "a bridge:export console
command and a UI hook for backup-to-file from v1". Shipping both now
so the v0.2.0 surface has the data-portability story end-to-end:
PHP side — `bin/console bridge:export <destination>`:
- Reads the source path from DATABASE_URL so it works in dev mode
(developer's source-tree var/data.sqlite) and bundled mode (user
data dir SQLite) without environment-aware logic.
- SQLite-only by design (PLAN.md §6 — single-instance SQLite-first);
emits a clear error for non-sqlite:// URLs rather than pretending
to support drivers that need driver-specific dump tooling.
- Overwrites the destination if it exists (the FileDialog or shell
redirect that produced the path has already confirmed).
- 4 unit tests: happy path, non-SQLite URL, missing source,
overwrite. Test count 24 → 28.
QML side — Q_INVOKABLE BackendConnection.exportDatabase(path):
- Bundled mode only; dev mode emits databaseExportFailed and
returns false (developers own their SQLite directly).
- Accepts both filesystem paths and `file://` URLs (FileDialog
results).
- Returns synchronously with bool but also emits async signals
databaseExported(dst) / databaseExportFailed(reason) so QML
can drive a snackbar / log without polling the return value.
- Removes any existing destination first (QFile::copy refuses
to overwrite); the picker has already confirmed the choice.
Drive-by: parse_url() rejects sqlite:///abs/path on PHP 8.5+ (the
host-less triple-slash trips its strictness). Switched to a
prefix-strip — Doctrine DBAL only emits two URL shapes for
SQLite anyway (sqlite:///abs and sqlite://relative).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>