Declare entitlement to a sibling plugin from another plugin

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_active filter, 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-main installed and active. It registers the mam_plugin_entitlement filter that backs everything here. (Filter registration lives in mam-main/mam-main.php; the implementation is mam-main/includes/plugin-update-manager/entitlement-manager.php, class mam_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 null on a cache miss, and the manager fails open (returns entitled) in that case — but a defensive caller should still guard against null before indexing ['state'].
  • This array shape (state / reason / plugin_slug / optional expires_at) is the contract returned by mam_entitlement_manager::mam_plugin_entitlement(). There is no bare boolean and no top-level blocked / expired key — the status is always under state.

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-main is missing), the add_filter line never runs, so the filter has no handler and returns whatever default the caller passes — typically false.
  • __return_true is 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 expired but not while blocked (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 returns null from the entitlement filter. The manager fails open elsewhere, but your own code indexing $entitlement['state'] will warn on null. Use empty( $entitlement ) or is_array() first, as the examples do.
  • Check has_filter( 'mam_plugin_entitlement' ) before relying on entitlement. If mam-main is not loaded, the filter is unregistered and apply_filters just 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.com the entitlement manager short-circuits to entitled. Useful to know when testing against the licensing server itself.

  • 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_active filter 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.

Was this article helpful?
Contents

    Need Support?

    Can't find the answer you're looking for? Don't worry we're here to help!