Purpose
The settings-cascade read filter. Every setting read in mam-main flows through this filter. Sibling plugins register handlers per-key to extend reads without touching the app-settings subsystem.
⚠️ Hot path. Runs 100+ times per phone-data build. Subscribers must be cheap.
Signature
$value = apply_filters(
'mam_app_settings_get_setting',
$default, // value to return if no stored value exists
$role, // role slug ('subscriber', 'admin', 'mam-all', etc.)
$button_id, // button-id namespace ('general-settings', 'login', a real button id, ...)
$key // the setting key ('tsl-setting-disable_caching', etc.)
);
Registered by the data manager at priority 10, 4 (four args).
| Parameter | Type | Description |
|---|---|---|
$default |
mixed | Fallback if no stored value exists |
$role |
string | Role scope. Empty falls back to 'mam-all' (global). |
$button_id |
string | Button-id namespace the setting is stored under — a real button id, or a namespace sentinel like 'general-settings' |
$key |
string | The setting key |
Returns: mixed — the resolved value.
The cascade
For a given $button_id + $key, the data manager runs a single query against
wp_mam_app_settings_options that resolves the fallback in one shot. First hit wins.
1. Role-specific row (role = $role)
2. Global row (role = 'mam-all')
3. Default ($default arg, returned when neither row exists)
The fallback is a single ORDER BY inside app-settings-data-manager.php‘s get_setting(),
not chained filter subscribers. Subscribers shouldn’t try to re-implement it.
Example: simple read
$radius = apply_filters(
'mam_app_settings_get_setting',
25, // default
mam_current_request()->user_role(),
'general-settings',
'tsl-setting-geofilter_radius'
);
Example: subscriber that overrides one key
add_filter( 'mam_app_settings_get_setting', function ( $value, $role, $button_id, $key ) {
// Only handle my plugin's special key.
if ( $key !== 'my_plugin_special_value' ) {
return $value;
}
// Cheap: read from my plugin's own option.
return get_option( 'my_plugin_special_value', $value );
}, 10, 4 );
⚠️ Notice the early return — subscribers that don’t apply to this key must short-circuit immediately. Doing work for every key is the most common cause of slow phone-data responses.
Example: cached subscriber
add_filter( 'mam_app_settings_get_setting', function ( $value, $role, $button_id, $key ) {
if ( $button_id !== 'my_plugin' ) {
return $value;
}
static $cache = null;
if ( $cache === null ) {
$cache = wp_cache_get( 'my_plugin_settings', 'mam' ) ?: array();
}
return $cache[ $key ] ?? $value;
}, 10, 4 );
For non-trivial lookups, use wp_cache_get / wp_cache_set — multiplied by hundreds of calls, the savings are large.
Hot-path anti-patterns
- ❌ Make HTTP calls
- ❌ Run unbatched DB queries (
get_post_metaper row) - ❌ Allocate large objects per call
- ❌
apply_filtersrecursion - ❌ Iterate every key looking for a match (early-return based on
$key) - ✅ Early return when the key doesn’t match
- ✅ Use
wp_cache_get/wp_cache_set - ✅ Use
staticfor per-request caching
Gotchas
- Hot path. A 50ms subscriber adds 5+ seconds to every phone-data build (100+ invocations).
- The cascade is in the data manager. Don’t try to re-implement fallback by chaining subscribers.
- Frozen contract. ~20 direct
apply_filterscall sites across the suite, but 100+ invocations per phone-data build (settings are read inside per-button / per-role loops). - Cloning admins read settings for the role they’re cloning, not their actual admin role.
- An empty
$rolefalls back to'mam-all'(the global row). Pass''or'mam-all'; don’t invent a'global'string. - The
$button_idnamespace is'<plugin-slug>'(or a real button id) for sibling plugins. Don’t reuse a core sentinel like'general-settings'for your own keys.
Related articles
- Settings cascade overview
- Per-button and per-role settings
- Hook: mam_app_settings_set_setting
- Frozen public contracts reference
Metadata
| Field | Value |
|---|---|
| Article type | Hook Reference |
| Plugin slug | mam-main |
| Applies to plugin version | 2.1.11+ |
| Hook type | filter |
| Audience | PHP developer |
| Frozen contract | yes — ~20 call sites, 100+ invocations per build |
| Last verified | 2026-05-02 |
