A Data Bundle is a serialized snapshot of the Wonderful Relations configuration owned by a single Project — Entities, Queries, DataTables, Forms, Form Fields, Linked Forms, Templates, PDF Templates, Services, Buttons, Tasks and the Wonderful Relations links that tie them together. Bundles are how a child plugin ships its baseline configuration to a customer install.
Why bundles instead of initial.sql
A raw SQL dump cannot be replayed safely against a customer database: auto-increment IDs already exist, foreign keys collide, references break. Bundles solve this in three ways:
- UUID-keyed identity. Every mapped row carries a
uuidcolumn. The importer matches by UUID, not byid, so updates land on the correct row even if its numeric id differs between developer and customer install. - Project-scoped ownership. Each bundle declares its owning project
UUID. The Sync importer refuses to touch rows owned by another
project (
UNKNOWN/COLLISION/foreign-ownerhard stops). - FK heal. After the rows are written, a Foreign-Key Heal pass
rebuilds
wr_linkand field references using the resolved UUIDs.
The pipeline
┌──────────────┐ ┌────────────────┐
wp wr │ │ JSON │ │ wp wr
bundle-export │ Developer │ ─────▶ │ Customer │ bundle-sync
──────────────▶│ install │ │ install │ ──────────▶
│ │ │ │
└──────────────┘ └────────────────┘
│ │
▼ ▼
Bundle JSON Bundle JSON
(project + + UuidResolver
hydrated tree) + Ownership check
+ Backup
+ Bundle write
+ FK heal
+ Conflict policy
Three CLI verbs you actually use
# Developer side: export the configuration owned by a project.
wp wr bundle-export \
--uuid=<project-uuid> \
--output=/path/to/bundle.json \
[--fingerprint=ts-2026-05-07]
# Customer side: import a single bundle file.
wp wr bundle-import \
--input=/path/to/bundle.json \
[--policy=bundle_wins|db_wins] \
[--strict]
# Customer side: sync a folder of bundle files (the typical update flow).
wp wr bundle-sync \
--dir=/path/to/bundles/ \
[--policy=bundle_wins|db_wins] \
[--strict]The Sync importer is idempotent: running bundle-sync twice in a
row does nothing on the second pass. That is the property tests rely on.
Conflict policy
When the same row exists on both sides with different content, the importer falls back to the policy:
BundleWins(default) — the bundle’s value replaces what is in DB.DbWins— the DB value is kept; bundle changes are skipped.
Pass --strict to refuse silent fallbacks. Strict mode raises a hard
error on UNKNOWN/COLLISION/foreign-owner hits and lists the
offending rows.
What gets persisted on the customer side
Three layers cooperate to keep query/data definitions readable across installs:
- Database — all WR tables (
wp_wr_query,wp_wr_form, …) hold the definitive rows. - Bootstrap JSON —
locale_bootstrapped_*.jsonsnapshots inwp-content/wonderful-relations-data/are the locale-resolved variants used at runtime to skip a DB read on hot paths. - Query cache JSON —
wr_query_cache.jsonandwr_project_snapshot.jsoncache compiled queries.
Never delete or move these JSON files manually. They are persistent runtime state, not throwaway cache. The Bundle importer writes them as part of a successful sync; deleting them between sync runs leaves the system serving stale or missing query SQL.
Bundle file structure
A bundle is a single JSON document with three top-level sections:
{
"fingerprint": "ts-2026-05-07",
"project": { "uuid": "…", "identifier": "…", "title": "…" },
"entries": [ { "kind": "wr_entity", "uuid": "…", "data": {...} },
{ "kind": "wr_query", "uuid": "…", "data": {...} },
{ "kind": "wr_form", "uuid": "…", "data": {...} },
… ]
}The kind enum lists every WR concept the bundle can carry; entries
form a logical tree thanks to UUID references inside data.
Multi-project export (wr-bundle-multi-v1)
A plugin can own more than one Project — e.g. yourproject plus its extension
yourproject_invoice. A single-project export (--uuid=<project>) only carries
that project’s closure, so it can’t represent both. Exporting the set
produces a multi-bundle envelope that wraps one wr-bundle-v1
sub-bundle per project:
{
"format": "wr-bundle-multi-v1",
"fingerprint": "wr-export-multi-…",
"bundles": [ { /* project A v1 bundle */ }, { /* project B v1 bundle */ } ]
}On read the loader flattens a multi-v1 envelope to a single
wr-bundle-v1 (BundleEnvelope::flattenMulti()): the sub-bundles are
dedup-merged across manifest / entities / links,
first-occurrence-wins so a shared row keeps its earliest — dependency-safe
— slot (the parent project’s sub-bundle is listed first, before its
extension’s). Everything downstream (schema validation, importer,
bootstrap) only ever sees a single v1, so a multi-v1 file can be checked in
directly as the plugin’s bundle.json.
A
bundle.jsoninwr-bundle-multi-v1format requires a Wonderful Relations build that ships the multi-v1 loader. A plain singlewr-bundle-v1loads on every version — deploy the matching WR first when switching a child plugin to the multi format.
Backup, rollback and FK heal
bundle-sync always takes a mysqldump backup of the affected tables
before writing. Pair this with the safety verbs if a sync goes wrong:
wp wr bootstrap-bundle-dryrun --dir=/path/to/bundles/ # plan only
wp wr bootstrap-bundle-write --dir=/path/to/bundles/ # apply with backup
wp wr bootstrap-bundle-rollback --backup=/path/to/dump.sql
wp wr fk-heal [--dry-run] [--no-backup]The legacy verbs are also available:
wp wr orphan-sweep— delete rows whose parent UUIDs have vanished.wp wr orphan-link-sweep— same idea for Wonderful Relations links.wp wr health-check— pre-flight integrity checks.wp wr fk-drift-audit— print divergent foreign keys without changing anything.
Legacy migration
Plugins that still ship a JSON export from the old JSONExporter
pipeline can convert it once:
use TS\WonderfulRelations\System\DataStorage\Bundle\LegacyJsonToBundleConverter;
$bundle = ( new LegacyJsonToBundleConverter() )->convert( $legacy_export_path );
file_put_contents( $output_path, json_encode( $bundle, JSON_PRETTY_PRINT ) );After conversion, switch the plugin to ship the bundle file and stop producing the legacy export.
Where to put the bundle in your plugin
The convention used by the WR mother and the child plugins is:
your-plugin/
└── src/
└── Database/
└── Config/
├── config.json # Update Center declarative plan
└── bundles/
└── your-plugin.bundle.json
The Database/Config/config.json file references the bundle so the
Update Center can sync it as part of wp_admin → Update Center.
