Support Log in
Developer Guide

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

StoreWritten byRead by
wp_fieldforge_valuesField 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 migrationget_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
top-level field of any group;
  • 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
at the top level on purpose (a Calculation total, 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,
and the slot in it must be free.

The UPDATE carries parent_id = 0 in its WHERE clause, so an editor save

that fixes the post first wins the race.

php
// 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, finished

To 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.

Forge AI Assistant Online

Hi! I'm the Field Forge AI assistant. Ask me anything about the plugin — setup, features, troubleshooting, or development.

Just now
Powered by Forge AI · Browse docs