Goal
You have two MAM Suite plugins. Plugin A owns a feature (say, GeoFilters). Plugin B wants to light up extra behavior only when Plugin A is installed, active, and the customer is actually entitled to it. You do not want Plugin B to call into Plugin A’s classes directly, and you definitely do not want it to assume Plugin A is licensed just because the files are present on disk.
This how-to shows the two-sided pattern MAM Suite uses for exactly this:
- The owning plugin advertises its licensed state by registering a simple
mam_<plugin>_is_activefilter, but only after it clears its own entitlement check. - The consuming plugin checks that filter (or queries entitlement directly) before turning on the dependent feature.
This keeps the licensing decision in one place — mam-main‘s entitlement manager — while letting sibling plugins coordinate without hard PHP dependencies.
Prerequisites
mam-maininstalled and active. It registers themam_plugin_entitlementfilter that backs everything here. (Filter registration lives inmam-main/mam-main.php; the implementation ismam-main/includes/plugin-update-manager/entitlement-manager.php, classmam_entitlement_manager.)- Both plugins follow the MAM Suite text-domain convention: their slug starts with
mam-(e.g.mam-geofilters). Entitlement is keyed on this slug. - Familiarity with bootstrapping a sibling plugin behind entitlement. If you have not gated a plugin’s bootstrap yet, read Recipe: Gate a sibling plugin behind entitlement first — this article builds on that gate.
Background: what “entitlement” returns
mam-main answers the mam_plugin_entitlement filter from a cached option (mam_plugin_entitlements) that is refreshed asynchronously from the WPMAM licensing server. Synchronous reads never make an HTTP call, so it is cheap to query at bootstrap.
The filter returns an array describing the entitlement, not a bare boolean. The key you care about is state:
$entitlement = apply_filters( 'mam_plugin_entitlement', null, 'mam-geofilters' );
// e.g. array( 'state' => 'entitled', 'reason' => 'purchased', 'plugin_slug' => 'mam-geofilters' )
state value |
Meaning |
|---|---|
entitled |
Purchased / licensed (or fail-open on a cold cache or the source server). |
grace |
In a grace period after lapse; expires_at present. Generally still treat as usable. |
expired |
Subscription expired; expires_at present. Treat as not usable. |
blocked |
Not purchased / explicitly revoked by the server. Do not bootstrap. |
Notes that bite if you skip them:
- The record can be
nullon a cache miss, and the manager fails open (returnsentitled) in that case — but a defensive caller should still guard againstnullbefore indexing['state']. - This array shape (
state/reason/plugin_slug/ optionalexpires_at) is the contract returned bymam_entitlement_manager::mam_plugin_entitlement(). There is no bare boolean and no top-levelblocked/expiredkey — the status is always understate.
Steps
1. In the owning plugin: gate your bootstrap on your own entitlement
Inside the owning plugin’s bootstrap (run on plugins_loaded), confirm mam-main is present and that this plugin is entitled before you register anything. This is the standard gate — shown here as it appears in mam-geofilters/mam-geofilters.php:
public function __construct() {
// mam-main must be loaded for the entitlement filter to be answerable.
if ( ! has_filter( 'mam_plugin_entitlement' ) ) {
return;
}
$entitlement = apply_filters( 'mam_plugin_entitlement', null, $this->plugin_slug );
// No record, or explicitly blocked → do not bootstrap.
if ( empty( $entitlement ) || 'blocked' === $entitlement['state'] ) {
return;
}
// (Update-info registration is skipped while expired, but the plugin still runs.)
if ( 'expired' !== $entitlement['state'] ) {
do_action( 'mam_set_plugin_update_info', basename( __DIR__ ), __FILE__ );
}
// ... continue to step 2
}
$this->plugin_slug here is the plugin’s own slug, e.g. 'mam-geofilters'. Choose how strict to be about expired/grace based on the feature — some plugins keep running while expired (and merely stop receiving updates), others should fully stand down.
2. In the owning plugin: declare the is-active filter
Once past the gate, register a boolean filter named after your plugin that simply returns true. This is the signal other plugins read. From mam-geofilters:
// mam_geofilters_is_active — signals that GeoFilters is installed, active,
// and entitled. Other plugins gate geofilter behavior on this.
add_filter( 'mam_geofilters_is_active', '__return_true' );
Two things make this work cleanly:
- It is registered only inside the entitled branch. If the plugin is blocked (or
mam-mainis missing), theadd_filterline never runs, so the filter has no handler and returns whatever default the caller passes — typicallyfalse. __return_trueis a WordPress core helper. No custom callback needed; the mere presence of the registration is the “yes.”
mam-main itself follows the same convention with mam_main_is_active (see mam-main/mam-main.php), so the pattern is consistent across the suite.
Naming: use
mam_<your-plugin-feature>_is_active, lowercase, underscores. The consuming side has to know this exact string, so treat it as a small public contract — see Frozen public contracts reference before renaming one that ships.
3. In the consuming plugin: check the is-active filter before using the feature
In the dependent plugin, query the filter at the point where you would otherwise reach into the sibling. Pass false as the default so the answer is “no” whenever the owning plugin did not register its handler. This is exactly what mam-geodirectory does to decide whether to apply geofilter behavior (mam-geodirectory/content-classes/local-app-geodirectory-v2-class.php):
if ( apply_filters( 'mam_geofilters_is_active', false ) ) {
// GeoFilters is present and licensed — apply the geofilter-aware path.
}
Because the default is false, every “not installed / not entitled / blocked” case collapses to the same safe branch. No class_exists(), no function_exists(), no version sniffing.
4. (Optional) Check the sibling’s entitlement directly instead
If the consuming plugin needs more than a yes/no — for example, to behave differently in a grace period, or to surface an “expired” message — it can query the sibling’s entitlement record itself rather than relying on the is-active filter:
$geo = apply_filters( 'mam_plugin_entitlement', null, 'mam-geofilters' );
if ( is_array( $geo ) && in_array( $geo['state'], array( 'entitled', 'grace' ), true ) ) {
// Usable now.
} elseif ( is_array( $geo ) && 'expired' === $geo['state'] ) {
// Show a "renew GeoFilters to keep X" hint, but don't crash.
}
Prefer the is-active filter (step 3) for the common case — it is the sibling’s own declaration of “I’m on,” and it respects whatever stricter rules the owner applied in step 1. Reach for the direct entitlement query only when you genuinely need the finer-grained state.
Why two mechanisms, not one
It is reasonable to ask why a consumer doesn’t always just call mam_plugin_entitlement for the sibling’s slug. The is-active filter exists because the owning plugin is the authority on whether it is truly “on”:
- The owner may decide to keep running while
expiredbut not whileblocked(geofilters does this). A raw entitlement read by the consumer would not know that local policy. - The owner might add other preconditions (a required setting, a companion plugin) before declaring itself active. Folding all of that into one boolean filter keeps consumers simple.
So: the is-active filter is the owner’s verdict; the entitlement filter is the raw license fact. Gate on the verdict unless you specifically need the fact.
Gotchas
- Guard against
null. A cold cache returnsnullfrom the entitlement filter. The manager fails open elsewhere, but your own code indexing$entitlement['state']will warn onnull. Useempty( $entitlement )oris_array()first, as the examples do. - Check
has_filter( 'mam_plugin_entitlement' )before relying on entitlement. Ifmam-mainis not loaded, the filter is unregistered andapply_filtersjust echoes your default. The owning plugin’s gate already does this; consuming plugins that query entitlement directly should too. - Don’t re-check on every request. Bootstrap-time is enough. The entitlement read is cheap, but the is-active filter is cheaper still and is the intended hot-path check.
- Slug is the key. Entitlement is looked up by the
mam-text domain. A typo in the slug silently reads as “no record” and (depending on path) either fails open or returns nothing. - Source server is always entitled. On
wpmobileappmanager.comthe entitlement manager short-circuits toentitled. Useful to know when testing against the licensing server itself.
Related
- Recipe: Gate a sibling plugin behind entitlement — the bootstrap gate this article extends.
- Hook: mam_plugin_entitlement — full reference for the entitlement filter and record shape.
- Hook: mam_check_required_version — the companion gate for “is mam-main new enough?”
- Frozen public contracts reference — before you rename a
mam_<plugin>_is_activefilter that other plugins read.
What’s next
Once your plugin declares its own mam_<plugin>_is_active filter, document that filter as a public hook in your plugin’s reference so other developers know it exists and can gate on it. If your feature also needs the sibling’s data (not just its presence), expose a dedicated read filter from the owning plugin rather than having consumers reach into its classes.
