Skip to main content

StagingManager

\Benecaster\Staging\StagingManager

Class Free

The entry point for staging mode detection and staging subscriber-set management. Add-on developers use it for two things: to find out whether the current site is a staging environment, and to read or adjust which subscribers hold live feed tokens there.

Staging sites deliberately run with a reduced subscriber set. A staging copy of a production database would otherwise poll feeds and report counts as though it were the real site, which is why the active set exists and why it is capped on the lower plans.

Resolve the class from the service container rather than constructing it: \Benecaster\Plugin::instance()->container()->make( \Benecaster\Staging\StagingManager::class ).

Constructor Dependencies

Type Description
\Benecaster\Token\TokenRepository Data access for the token rows the staging active set is built from.
\Benecaster\Staging\EnvironmentResolver Resolves the two environment constants and the precedence between them. Optional — it defaults to a fresh instance, since it holds no dependencies of its own.
\Benecaster\Staging\StagingPatternFetcher Supplies the vendor-specific hostname patterns Benecaster fetches from the license server. Optional — defaults to a fresh instance, matching the EnvironmentResolver pattern above, so no call site needs to change to pick this up. A fetched pattern can only ever ADD a staging match on top of the hardcoded rules below; it can never remove one.

Methods

Method Visibility Since Description
is_staging(): bool Public — Returns true when the current site is operating in staging mode. It asks EnvironmentResolver::declared() first and only falls through to hostname patterns (see Notes) when neither constant is set: BENECASTER_IS_PRODUCTION returns false here, BENECASTER_STAGING returns true, and with both set the site is production. Detection runs once per request and is cached for the remainder of it. This method does not read either constant itself, and neither should you. Read them through EnvironmentResolver, which is the only place the precedence is expressed — going direct is how an install came to be staging by this answer and production by LiveKeyGuard's at the same moment.
get_staging_cap( int|string|null $show_context = null ): ?int Public — Returns the staging subscriber cap for this license, or null when the plan has none. Launch is capped at 10, Starter and Growth at 25; Pro, Multi-Show and Studio return null, as does an operator-set null override. The value comes from the daily validation response. When null, add_to_set() never takes anyone out and rebuild_default_set() includes every subscriber.
get_active_set(): array Public — Returns the staging active set as an array of WordPress user IDs, oldest first, or an empty array when staging is not active. A set larger than the cap is cut to its newest cap entries, and the cut set is saved. Use this rather than reading the underlying option directly — it handles the empty-state edge cases and is the stable surface.
add_to_set( int $user_id ): array|WP_Error Public — Adds a user to the staging active set, first in, first out. At the cap it takes the oldest entries out to make room and returns their user IDs, oldest first; it returns an empty array when nobody was taken out or the user was already in the set. The only WP_Error is benecaster_staging_at_cap, when the cap is 0 and nobody can be added. On success it fires benecaster_clear_feed_cache once per distinct show and tier pair among the user's active tokens, so the newly-active subscriber is not served a stale empty feed on their first poll.
remove_from_set( int $user_id ): void Public — Removes a user from the staging active set. Silently succeeds when the user is not in the set, so it is safe to call without checking first.
rebuild_default_set(): void Public — Regenerates the active set from scratch. For each configured membership tier it selects at least two subscribers, then fills the remaining cap slots by join date, oldest first. When get_staging_cap() returns null, every subscriber is included. Runs automatically on first staging detection when the set is empty, and when an admin chooses "Reset to defaults". It overwrites the set entirely — manual additions are discarded.

Constants

Name Value Description
FeedCache::STAGING_CACHE_TTL_DEFAULT 45 Feed cache TTL in seconds while staging is active, down from the production TTL so feed changes show up quickly while you are testing.
BENECASTER_STAGING_CACHE_TTL (not defined) Define in wp-config.php to override the staging TTL, in seconds. Raise it to test caching behaviour itself; setting it to PHP_INT_MAX effectively disables the staging reduction, which is not recommended.

Hooks Fired

Notes

## URL patterns

Detection uses two sources, unioned — a fetched pattern can only ever ADD a match, never remove one. is_staging()'s hostname check first tries the hardcoded, vendor-neutral rules below, then the fetched vendor-specific list (see Fetched patterns below). Matching is case-insensitive and considers only the host portion of the site URL, for both sources.

Hardcoded, vendor-neutral — work fully offline, ship with the plugin:

| Pattern | Example match | |---|---| | staging / staging{n} label anywhere in the host | staging.mypodcast.com, staging1.mypodcast.com, app.staging.mypodcast.com, mypodcast.staging.wpengine.com | | *.local | mypodcast.local | | localhost | http://localhost | | *.test | mypodcast.test | | Private or reserved IP literal | 192.168.1.20, 10.0.0.5, 127.0.0.1, ::1 |

The staging label must match a whole dot-delimited label, so stagingfoo.example.com and staging-example.com are not detected. Public IP literals are not detected either.

## Fetched patterns

Vendor-specific hostname shapes are not on the hardcoded list. They belong to a small list the plugin fetches from the license server once a day via WP-Cron and caches in the benecaster_staging_patterns option — {patterns: string[], match_rule: string, updated_at: string|null, last_fetched: string}. The cache never expires and is never cleared on a failed fetch; a fetch failure leaves the option exactly as it was and retries on the next daily tick, so a site that has fetched successfully at least once keeps detecting vendor-specific staging hosts even if the license server is later unreachable. Two private methods implement the match: host_matches_fetched_pattern() walks the cached list, and host_matches_glob() implements the license server's host-glob-v1 rule exactly (see 05-license-server.md → GET /staging-patterns) — a leading *. matches one or more whole labels, any other * matches within a single label only.

*.wpengine.com and *.kinsta.cloud are not matched. Both are those hosts' documented production temporary URLs — the address a live site serves from until a custom domain is pointed at it — so the suffix matched exactly the sites it most needed not to. WP Engine's legacy staging shape {install}.staging.wpengine.com is unaffected: it carries a staging label and is matched by the label rule, which is the row it now belongs to in the table above. Both exclusions follow the same rule the pattern list is built on, and an ambiguous host fails to detect — but the two mistakes are not the ones the rule is often quoted as weighing, so it is worth knowing what each actually costs.

A false positive — a production site detected as staging — is the expensive direction. is_staging() suppresses the live-Stripe-key guard's approval, and on a capped plan the daily /validate ping reports no more than the staging cap's worth of subscribers, so the over-limit windows and threshold notices that should warn the podcaster never open. It is not invisible, though — the site wears a 4px orange border on every admin screen, so the state is visible even where the consequence is not.

A false negative — a staging site not detected — costs no activation slot, because StagingManager never decides the slot in either direction. It is not consulted at /connect, whose URL carries no staging parameter, and the is_staging it sends on /validate is recorded by benecaster.com but never acted on. Whether a connected site costs a slot is benecaster.com's call alone, made from the site's verified hostname against a pattern list Benecaster maintains (or by support marking the site staging), and re-checked on every /validate ping. What a false negative actually costs is that live Stripe keys become saveable on the staging site (clones are still refused by InstallIdentity, which is hostname-blind) and the staging subscriber set does not apply.

A domain that matches none of these needs define('BENECASTER_STAGING', true) in wp-config.php to run in staging mode. If you are unsure whether your host's staging URL is recognised, set the constant — it costs nothing and removes the guesswork. It switches on staging mode on that install only: it does not stop the site using an activation slot. A staging copy that is using a slot is a support request.

## Staging and the daily validation payload

The daily validation request carries an is_staging boolean on every ping, in both states, taken straight from is_staging(). It is not an omit-when-false flag.

It is reported, not obeyed. benecaster.com records the value but decides for itself whether the site is staging, from the verified hostname or a support-set mark; sending true does not free an activation slot or take the site out of tier logic, and omitting the key changes nothing either.

Two further adjustments apply while staging:

- The reported subscriber count is capped at the staging cap, so a staging site carrying a full production database reports the cap's worth of subscribers rather than the whole copied list. - When the cap is null (Pro, Multi-Show, Studio) the real count is sent uncapped, consistent with those plans having no ceiling in production either.

For a site benecaster.com classifies as staging, the license server excludes its pings from tier upgrade logic, benchmark aggregation, and the estimated-reach preview for announcements.