Skip to main content

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
caution
flavor_get_option() takes no group argument

Reading 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

ItemConventionExample
On/off toggle (managed by the ModuleManager)module_{id} (hyphens → underscores)module_sale_banner
Your settings arraymodule_{id}_settingsmodule_sale_banner_settings
Storage group (write only)'modules'
note

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>';
}
caution
Use callback, not render

The 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
caution

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()
}