skip to content

In Laravel Pennant, how do activate(), deactivate(), activateForEveryone() and purge() change stored flag values, and when would you run pennant:purge?

level: middleimportance: should knowfreq 18%

answer

  1. activate() writes one scope's value
  2. forget() deletes one scope's row
  3. ForEveryone updates stored rows only
  4. purge() deletes, next check re-resolves
  5. --except and --except-registered

basics

~20 s

activate() and deactivate() store a value for one scope, forget() deletes it, and activateForEveryone() overwrites every stored row for a feature. purge() deletes a feature's rows so the next check re-runs its definition; pennant:purge does that from a deploy.

solid answer

~40 s

`Feature::for($user)->activate('new-checkout')` stores `true` for that scope, and `activate('purchase-button', 'seafoam-green')` stores a rich value; `deactivate()` stores `false`; `forget()` deletes the row so the definition runs again next time. `Feature::activateForEveryone('new-checkout')` runs one UPDATE on every **stored** row for the feature, so users never checked yet still get the definition, which is why the docs say to update the definition too. `Feature::purge('new-checkout')` deletes all stored values, so everyone re-resolves on the next check, which re-draws lotteries. `php artisan pennant:purge new-checkout` does the same from a deploy script, `--except=` keeps named features, and `--except-registered` purges everything not defined in the app, which cleans out removed flags.

code

php · 12 lines
php
<?php

use Laravel\Pennant\Feature;

// A support agent opts one customer into the new checkout
Feature::for($customer)->activate('new-checkout');

// Rollout succeeded: flip every stored answer to true
Feature::activateForEveryone('new-checkout');

// The old experiment was deleted from the code: drop its rows
Feature::purge('checkout-button-colour');

go deeper

for a junior

Recall that activate() and deactivate() switch a flag for one user, and purge() clears stored values so the flag is decided again.

for a middle

Explain that bulk updates touch stored rows only, what forget() and purge() delete, and the pennant:purge options for deploy scripts.

for a senior

Plan rollout completion and cleanup without re-rolling users unexpectedly, and use Pennant's events to audit flag changes.

for a principal

Set the team's lifecycle for flags, from rollout to full release to deletion, and decide who may change stored values in production.

## Stored values are the state Pennant stores each feature's first resolved value per scope. After that, the **stored value is the flag**; the definition is only consulted for scopes that have no stored value. Every update method below works on that stored state, which is the key to predicting their effect. ## Per-scope changes | Call | Effect | |---|---| | `Feature::for($user)->activate('new-checkout')` | stores `true` for that user | | `Feature::activate('purchase-button', 'seafoam-green')` | stores a rich value for the default scope | | `Feature::for($user->team)->deactivate('billing-v2')` | stores `false` for that team | | `Feature::for($user)->forget('new-checkout')` | deletes the stored value; the next check runs the definition again | `for()` accepts several scopes at once, so `Feature::for($betaTesters)->activate('new-checkout')` opts a whole collection in. Without `for()`, these calls act on the default scope, usually the authenticated user, which makes an 'opt into the beta' button a one-liner. ## Bulk changes that touch stored rows only `Feature::activateForEveryone('new-checkout')` and `deactivateForEveryone('new-checkout')` call the driver's `setForAllScopes()`. On the database store that is a single `UPDATE features SET value = ... WHERE name = 'new-checkout'`: - every user **already resolved** gets the new value; - a user **never checked** has no row, so the definition still decides for them; - the docs therefore say to change the definition as well, for example to return `true`, when you mean 'everyone'. A common sequence when the 10% rollout of the new checkout goes well: 1. change the definition to `fn () => true`; 2. deploy; 3. run `Feature::activateForEveryone('new-checkout')` so users drawn `false` earlier flip too, or purge so everyone re-resolves against the new definition. ## Purging `Feature::purge('new-checkout')` deletes every stored value for that feature; `purge(['a', 'b'])` deletes several; `purge()` with no argument deletes the whole table. After a purge, each scope re-runs the definition on its next check. That is right when: - the definition changed and you want everyone re-evaluated; - a feature has been removed from the code and its rows are dead weight. It is risky mid-rollout: a purged `Lottery` feature is re-drawn, so roughly 90% of the users who had the new checkout lose it and a different 10% gain it. ## pennant:purge in deploys The Artisan command, alias `pennant:clear`, exposes the same operation for scripts: 1. `php artisan pennant:purge new-checkout` purges named features; 2. `php artisan pennant:purge --except=new-checkout --except=purchase-button` purges everything except those; 3. `php artisan pennant:purge --except-registered` purges everything **not defined** in the running app, the tidy-up after deleting flags from the code; 4. `--store=array` targets a store other than the default. ## Events for auditing Every change dispatches an event: `FeatureUpdated` for one scope, `FeatureUpdatedForAllScopes` for bulk updates, `FeatureDeleted` for `forget()`, and `FeaturesPurged` or `AllFeaturesPurged` for purges. Listening to them is a cheap audit trail of who changed which flag, which matters once support staff can toggle flags for customers. ## Interview traps - **"`activateForEveryone()` turns it on for all users."** Only for users with a stored value. - **"Purging is harmless."** It re-rolls lotteries and changes what users see. - **"`deactivate()` deletes the row."** It stores `false`; `forget()` deletes. ## Where these calls usually live Per-scope `activate()` and `deactivate()` calls typically sit behind an admin screen or a support tool, so an agent can put one customer on the new checkout. Bulk updates and purges usually belong in deploy scripts or one-off Artisan commands, where they run once and are reviewed like a migration. Keeping them out of request code avoids a stray page view rewriting thousands of rows.

  • After Laravel Pennant's activateForEveryone('new-checkout'), a user who signed up today still gets the old checkout. Why?
    `activateForEveryone()` updates only rows that already exist in `features`. The new user had never been checked, so there was no row; their first check ran the definition, which still returned the 10% lottery. Change the definition to return `true` as well.
  • What is the difference between deactivate() and forget() in Laravel Pennant?
    `deactivate()` stores `false` for the scope, so the feature stays off for that user regardless of the definition. `forget()` deletes the stored value, so the next check runs the definition again and may resolve it on.

saying these in an interview costs you the question

  • activateForEveryone() also covers users who were never checked.
  • purge() switches a feature off for everyone permanently.
  • Purging a Lottery feature keeps the same users in the rollout.
  • deactivate() deletes the stored row for the scope.
  • pennant:purge --except-registered deletes the features you still define.