52. Two Value Stores, and the Record Sweep
A site migrated from ACF reads its values from two places, and knowing which is
which explains most “the site shows it, the editor does not” reports.
Where a value can live
| Store | Written by | Read by |
|---|---|---|
wp_fieldforge_values | Field Forge (editor save, update_field(), REST, importer) | get_field() and the metabox |
wp_postmeta, ACF-era keys (topside_video_preview, with the _topside_video_preview = field_xxx companion) | ACF, before the migration | get_field() through includes/class-acf-compat.php when the table has no row; the metabox through FIELDFORGE_Field_Renderer::legacy_meta_read_owned() |
The editor’s reader is fenced: it shows a legacy value only when ACF’s own
_ companion is present, so no unrelated plugin’s postmeta can be adopted
into a field. The front-end reader is deliberately not fenced — it has always
fallen back, and narrowing it would blank live pages. The first save after an
upgrade writes what the editor showed into wp_fieldforge_values, and the post
leaves its legacy storage behind without losing a value.
Nested containers and their path
A container nested in a group posts with its whole ancestor path
(fieldforge_group_named[topside][preview]) and declares that path in a
hidden fieldforge_named_parent[]. The save handler validates the path
against the field definitions before it creates or reuses any record: a
stale or forged declaration therefore writes nothing at all, rather than
leaving an empty group record behind and saving the field at the top level.
The 1.0.6 record sweep
Builds 1.0.25–1.0.33 filed such a container as its own top-level record
(video with parent_id = 0). The site still rendered it — have_rows()
resolves by name — while get_field('topside')['video'] was empty, so the next
save wrote the group empty. FIELDFORGE_Orphan_Repair (armed once by the 1.0.6
schema step, run in cron-sized batches with an admin_init fallback) moves
those rows back under their parent. It moves rows and creates none, and it
refuses anything it cannot prove:
- the name must have exactly one home in the definitions and must not be a
- that home must be a container (group / repeater / flexible content);
- the row must carry the group id of that home — code that files a nested name
update_field() from a
theme) writes group id 0, and must not be touched;
- the row must have children, the parent chain must already exist on the post,
The UPDATE carries parent_id = 0 in its WHERE clause, so an editor save
that fixes the post first wins the race.
// Observe the sweep.
add_action( 'fieldforge/orphan_repaired', function ( $post_id ) { /* one post */ } );
add_action( 'fieldforge/orphans_repaired', function ( $moved, $skipped ) { /* done */ }, 10, 2 );
// What the finished sweep found.
$report = get_option( 'fieldforge_orphan_repair_report' ); // checked, moved, skipped, finishedTo find the damage by hand on a site that has not updated yet, scan
wp_fieldforge_values for parent_id = 0 rows whose field_name is not a
top-level field of any group; repair one post by re-selecting the value in the
editor and saving.