Module Settings API
Modules store their configuration through the theme's option helpers, which write to the theme's own
{prefix}flavor_options table (never wp_options — this is the theme's Data Sovereignty rule). This page
covers the real helper signatures, the settings pattern the built-in modules use, and how to add an
admin settings screen.
The option helpers
// Read — TWO arguments only. There is NO group parameter on get.
flavor_get_option(string $key, mixed $default = null): mixed
// Write — the third argument is the storage GROUP (auto-detected from the key if null).
flavor_set_option(string $key, mixed $value, ?string $group = null): bool
// Delete — use this to clear a value. NEVER set a value to null (the column is NOT NULL).
flavor_delete_option(string $key): bool
flavor_get_option() takes no group argumentReading is flavor_get_option($key, $default) — two arguments. The storage group is derived from the key
automatically (keys prefixed module_ map to the modules group). Passing a group as a third argument to
flavor_get_option() does nothing useful and is a common mistake.
flavor_get_option() also applies the theme's registered defaults before your $default, and includes
a type-safety guard: if the effective default is an array but the stored value is a scalar, it returns the
default array (this prevents array_merge() crashes when a value was stored as a toggle string).
The settings pattern
Built-in modules store all of their settings in a single array option, keyed module_{id}_settings,
and merge over defaults on read. This keeps one row per module and lets you evolve the schema freely.
class SaleBannerModule extends AbstractModule
{
private string $settingsKey = 'module_sale_banner_settings';
private function getDefaultSettings(): array
{
return [
'message' => __('Summer sale — 20% off everything!', 'flavor'),
'bg_color' => '#ef4444',
'enabled' => true,
];
}
public function getSettings(): array
{
$saved = flavor_get_option($this->settingsKey, []);
if (!is_array($saved)) {
$saved = [];
}
return array_merge($this->getDefaultSettings(), $saved);
}
private function saveSettings(array $settings): void
{
// Third arg = storage group. 'modules' matches the auto-detected group
// for module_* keys; pass it explicitly for keys that don't start with "module_".
flavor_set_option($this->settingsKey, $settings, 'modules');
}
}
Naming convention
| Item | Convention | Example |
|---|---|---|
| On/off toggle (managed by the ModuleManager) | module_{id} (hyphens → underscores) | module_sale_banner |
| Your settings array | module_{id}_settings | module_sale_banner_settings |
| Storage group (write only) | 'modules' | — |
Some older built-in modules use a short settings key such as search_settings instead of
module_search_settings. For those, the 'modules' group must be passed explicitly to
flavor_set_option(), because the auto-detected group only maps keys that start with module_. New
modules should use the module_{id}_settings form so the group is detected automatically.
Adding an admin settings screen
A module gets a settings page by overriding getAdminMenuConfig(). The ModuleManager's admin layer
registers a submenu under the Flavor Modules menu for every active module that returns a config.
public function getAdminMenuConfig(): array
{
return [
'page_title' => __('Sale Banner', 'flavor'),
'menu_title' => __('Sale Banner', 'flavor'),
'capability' => 'manage_options',
'menu_slug' => 'flavor-module-sale-banner',
'callback' => [$this, 'renderAdminPage'], // <-- the render key is "callback"
];
}
public function renderAdminPage(): void
{
// Handle the POST, then render your form. Verify a nonce and capability first.
echo '<div class="wrap"><h1>' . esc_html__('Sale Banner', 'flavor') . '</h1>';
// ... your settings form ...
echo '</div>';
}
callback, not renderThe admin layer reads the callback key (falling back to [$instance, 'renderAdminPage'] if it is
absent). Every built-in module uses callback. Do not use a render key — it will be ignored and the
default renderAdminPage() method will be called instead.
Do not use the WordPress Customizer (customize_register) for module settings — that is not the Module
System 2.0 path and there is no flavor_modules_panel Customizer panel.
Enqueuing admin assets safely
If your settings page loads its own CSS/JS, gate the enqueue on the runtime-captured hook suffix rather than a hardcoded page slug — WordPress can rename the slug on registration if it collides:
public function enqueueAdminAssets(string $hook): void
{
if ($hook !== $this->adminHookSuffix) { // set for you by the admin layer
return;
}
wp_enqueue_style('sale-banner-admin', /* … */);
}
Caching computed settings
For settings that are expensive to derive and read often, cache the result. The theme cache helpers write to the theme's own cache table:
function sale_banner_config(): array
{
$cached = flavor_cache_get('sale_banner_config');
if ($cached !== null) { // note: null, not false
return $cached;
}
$config = /* … compute from settings … */;
flavor_cache_set('sale_banner_config', $config, HOUR_IN_SECONDS, 'modules');
return $config;
}
flavor_cache_get(string $key, mixed $default = null): mixed
flavor_cache_set(string $key, mixed $value, int $ttl = 3600, string $group = 'general'): bool
flavor_cache_delete(string $key): bool
flavor_cache_get() returns null (not false) on a miss. Always test with !== null. And never
call flavor_set_option($key, null) to clear a value — use flavor_delete_option($key).
Cleaning up on uninstall
Remove your options in the uninstall() lifecycle method so a full uninstall leaves no residue:
public function uninstall(): void
{
flavor_delete_option('module_sale_banner_settings');
// drop any custom tables you created in activate()
}