The plugin had grown several settings pages, API, AI Rewrite, Branding, Sources, and users started reporting an impossible-sounding bug, save your branding, and your API key vanishes. It reproduced instantly and made no sense until I read my own save handler honestly, and then it made perfect, terrible sense. This post is the settings-wipe bug, the classic architecture flaw behind it, and the fix worth stealing.
The flaw, my settings lived in one option array, and the save handler rebuilt that array from the submitted form, walking every known field and writing what it found, with absent fields reset to a default:
// the broken handler, in essence
$new = array();
foreach (jab_all_setting_keys() as $key) {
$new[$key] = isset($_POST[$key]) ? sanitize_text_field($_POST[$key]) : '0';
}
update_option('jab_settings', $new);
Read it as the bug it is, the Branding page’s form only contains branding fields, so when it submits, every field from every other page is absent from $_POST, and the handler faithfully resets them all to ‘0’. Saving anything erased everything else, by design, my design. The handler assumed one form containing all settings, and the plugin had outgrown that assumption without anyone telling the handler.
The fix has two parts, and the pattern applies to any multi-page settings system. First, start from what is saved, not from empty, so absence means untouched rather than deleted. Second, have each form declare which fields it actually contains, a hidden marker emitted beside every field, so the handler updates only what was legitimately present:
// each rendered field also emits its marker
function jab_field($key, $value) {
printf('<input type="hidden" name="jab_present[]" value="%s" />', esc_attr($key));
printf('<input type="text" name="%s" value="%s" />', esc_attr($key), esc_attr($value));
}
// the fixed handler: merge onto existing, touch only declared fields
$saved = get_option('jab_settings', array());
$present = array_map('sanitize_key', (array) ($_POST['jab_present'] ?? array()));
foreach ($present as $key) {
$saved[$key] = isset($_POST[$key]) ? sanitize_text_field($_POST[$key]) : '0';
}
update_option('jab_settings', $saved);
Checkboxes are why the marker earns its keep, an unticked checkbox is absent from $_POST exactly like a field from another page, and the marker is what distinguishes deliberately-blank on this form from not-on-this-form-at-all. The general principle underneath outlives the bug, in any partial update, absence is ambiguous, and systems must be told the difference between a value cleared and a value never asked about, the jab_present list is simply that distinction made explicit.
A few things people ask me about this
Why does saving one settings page erase my other settings? The handler rebuilds the whole option from the submitted form and defaults absent fields. Merge onto existing saved values and update only fields the submitting form declares.
How do unticked checkboxes fit this? They submit nothing, identical to fields from other pages. A per-form field manifest, hidden present markers, lets the handler treat declared-but-absent as unticked and undeclared as untouched.
Next
The next failure was louder, the plugin refused to activate at all, and the cause was a function I had deleted weeks earlier still being called from two places. That fatal, and the discipline it taught, is the next post.
