TokenManager
\Benecaster\Token\TokenManager
Service layer for the subscriber token lifecycle. Handles token generation, reactivation, revocation, and reset — each operation fires the appropriate action hook and invalidates any affected feed cache entries. It also recovers a subscriber’s existing feed URL with feed_url_for().
Resolve it from the container via $container->make( \Benecaster\Token\TokenManager::class ). To react to token events, use the lifecycle hooks (benecaster_token_generated, benecaster_token_reactivated, benecaster_token_revoked, benecaster_token_reset). An add-on that ends somebody’s access because they never paid calls revoke_unless_subscribed(), never revoke().
Constructor Dependencies
| Type | Description |
|---|---|
\Benecaster\Token\TokenRepository |
Data access for token rows. |
Methods
| Method | Visibility | Since | Description |
|---|---|---|---|
generate( int $user_id, int $show_id, string $tier_slug, string $token_type = 'subscriber', ?string $created_at = null ): string |
Public | — | Mints a new token row and returns the plaintext token. Fires benecaster_token_generated once the row is persisted, passing the plaintext as the fifth argument so listeners can build the subscriber's private feed URL. An encrypted copy of the token is stored beside its hash, so feed_url_for() can usually rebuild the URL later. Do not rely on that from here: recovery needs an encryption key and libsodium, so it can return null. A caller that needs the token should use the value this method returns. $token_type defaults to subscriber; pass follower for free direct-signup tokens. $created_at backdates the row's creation timestamp so a migration can carry a subscriber's original join date across from the platform they are leaving — see TokenRepository::insert() for the format and the reasoning. Every other caller leaves it null and gets the column default. It is threaded through here rather than left on the repository because generate() is the only path that returns the plaintext token, so an importer that needs to email a feed URL cannot bypass it. A refused $created_at throws out of BackdateGuard. Nothing is persisted and no hook fires — but the plaintext token generated for the attempt is discarded with the exception, so the row cannot simply be retried into the same feed URL. Validate the whole file with BackdateGuard::reason() before you start writing rows. |
feed_url_for( int $user_id, int $show_id ): ?string |
Public | — | The subscriber's own working feed URL, or null when it cannot be built. This is the method to call when you need to show or send someone their URL. It builds the URL with benecaster_get_feed_url(), so the result has the shape the Pretty feed URLs setting selects and passes through the benecaster_token_url filter. A URL you assemble yourself from recover_token() skips that filter. It returns null, never a partial URL, in every case recover_token() does, and also when a filter returns an empty string. Treat null as an ordinary answer: show the subscriber the masked URL and the reset option instead. Only show the result to the subscriber who owns it. No podcaster-facing screen in wp-admin displays a subscriber's working URL; the admin route is POST /subscribers/{id}/email-feed-link, which emails it to the subscriber. |
recover_token( int $user_id, int $show_id ): ?string |
Public | — | The subscriber's plaintext feed token, decrypted from the stored copy, or null when it cannot be recovered. It returns a token, not a URL; use feed_url_for() for the URL. Null is an ordinary answer and covers several cases that are deliberately indistinguishable: a token created before encrypted copies were stored, an install with no encryption key, a key that has changed since the token was stored, and a host without libsodium. It also returns null for a revoked token even when the stored copy decrypts, because a URL that no longer works is worse than none. |
generate_or_reactivate( int $user_id, int $show_id, string $tier_slug ): void |
Public | — | Called by the subscription bridge listener on activation events. Returns void — if you need the plaintext token, call generate() directly, which returns it. It does not return an array of token_id, token_prefix and plaintext. Code that expects a return value from it fails. Takes one of four paths: Fresh insert — no token row exists for the user/show pair. Delegates to generate(), which mints the token and fires benecaster_token_generated. Already active — a live token exists. The row is kept so the subscriber's feed URL is unchanged, and only tier_slug is updated, as an analytics snapshot only — feed delivery resolves tiers live via the bridge, not from this column. This is the path for a subscriber who upgraded or added a second membership. No hook fires. Reactivation — a revoked row exists and its revocation_reason is recoverable. The row is restored in place and benecaster_token_reactivated fires. The token_hash is preserved, so the subscriber's podcast app keeps working across the cancellation → resubscribe boundary. No new plaintext is minted and benecaster_token_generated does not fire on this branch. Blocked — a revoked row exists and its reason is not recoverable (in practice ADMIN_REVOKED). Nothing is reactivated. benecaster_token_reactivation_blocked fires instead, and the subscriber gets no feed despite an active paying subscription. This is what stops a membership plugin undoing a podcaster's deliberate revoke. A NULL or empty reason is treated as recoverable, so a row carrying no reason is never blocked. |
revoke( int $user_id, int $show_id, ?string $reason = null ): bool |
Public | — | Revokes the token for a user/show pair, fires benecaster_token_revoked, and returns true on success. Returns false when no token exists for that pair. It takes a user ID and a show ID, not a token ID. $reason is stamped into revocation_reason and decides whether a later bridge activation event may bring the token back. Callers revoking on behalf of a human admin action must pass RevocationReasons::ADMIN_REVOKED — otherwise the row is silently resurrected the next time the subscriber's membership plugin fires an activation event, and the podcaster's decision quietly stops holding. The null default is recoverable. |
revoke_unless_subscribed( int $user_id, int $show_id, string $reason ): string |
Public | — | Revokes the person's token for the show unless they hold an active membership there, as one operation, so a payment landing at the same moment cannot cost a payer their feed URL. This is the call for an add-on that has decided access should end because the person never paid; revoke() stays the call for a revocation that should happen whatever the person holds. Returns a ConditionalRevocation string. Only revoked took anything away; treat any value you do not recognise as "nothing was taken", because more may be added. - revoked: the token was active and is now revoked; benecaster_token_revoked fired. - converted: they hold something bought outright, so the token became a purchaser instead: same feed URL, public episodes plus what they own. - subscribed: a member; nothing changed. - no_token: no active token. An already-revoked row is left untouched, so a podcaster's own revoke is never overwritten. - unknown: the membership system cannot answer (none configured, or its plugin is inactive); nothing changed. Ask again later. "Member" means any active membership, free or paid, as benecaster_user_has_active_membership() answers it. $reason must be recoverable. Register it with benecaster_token_reactivation_recoverable_reasons first. An empty or non-recoverable reason throws InvalidArgumentException, because the promise of this call is that a later payment brings the feed back. On a show using a membership plugin, the subscription lives in that plugin's tables, so the token is revoked, the plugin is asked again, and the revoke is undone if a membership has appeared. For that instant the token is revoked, so a feed poll landing exactly then gets the public feed once. |
convert_to_purchaser( int $user_id, int $show_id ): bool |
Public | — | Converts a lapsed subscriber's token to the purchaser type, in place, when they still hold non-tier access. Returns true only when the row was converted. This is what a cancellation does instead of revoking, for a listener who bought something. The token keeps its row, its feed URL and status = 'active', and no revocation_reason is written. The feed stops carrying their tier and keeps carrying what they own. The is_over_limit flag is cleared, because a purchaser is exempt from both limits and nothing else would ever clear it. Fires benecaster_token_type_changed. It refuses, returning false, when the row is not active, the token is already a purchaser, the resolved base is follower, the listener holds no standalone grant, or a concurrent change wins the race. Only standalone grants convert: a buy-up is a subscription item that ends with the membership, so holding one does not keep a lapsed member's feed alive. A follower who subscribes, or a purchaser who subscribes, is promoted to subscriber in place on identical terms; that transition also fires benecaster_token_type_changed. |