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 override exists
$role, // role slug ('subscriber', 'admin', 'cloning', 'anonymous', etc.)
$category, // settings category ('general-settings', 'login', 'map', ...)
$key // the setting key ('tsl-setting-disable_caching', etc.)
);
| Parameter | Type | Description |
|---|---|---|
$default |
mixed | Fallback if no override exists |
$role |
string | Role scope. Empty = global. |
$category |
string | Settings category — used by sibling plugins to namespace |
$key |
string | The setting key |
Returns: mixed — the resolved value.
The cascade
The data manager walks layers in this order. First hit wins.
1. Per-button override (if invoked from a button context)
2. Per-role override (option key suffixed with $role)
3. Global setting (canonical option key)
4. Default ($default arg)
The cascade is implemented inside mam_app_settings_data_manager, not by chained filter subscribers. Subscribers shouldn’t try to re-implement the fallback.
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, $category, $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, $category, $key ) {
if ( $category !== '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. ~100 call sites across the suite.
- Cloning admins read settings for the role they’re cloning, not their actual admin role.
- An empty
$rolemeans global. Don’t pass'global'as a string; use''. - The category convention is
'<plugin-slug>'for sibling plugins. Don’t use core categories 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 — 100+ call sites |
| Last verified | 2026-05-02 |
