Laravel Pennant is a simple, lightweight feature flag package. Feature flags let you incrementally roll out new application features with confidence, run A/B tests on new interface designs, complement trunk-based development strategies, and more.
Define features using the Feature facade’s define method, typically in a service provider’s boot method. The closure receives the feature’s “scope” — usually the authenticated user.
Class-based features may define a before method that runs in-memory before the stored value is retrieved. If a non-null value is returned, it overrides the stored value for the duration of the request.
class NewApi{ public function before(User $user): mixed { if (Config::get('features.new-api.disabled')) { return $user->isInternalTeamMember(); } } public function resolve(User $user): mixed { // ... }}
The before method is useful for emergency kill switches or scheduling a global rollout on a specific date.
Use the active method to check whether a feature is active. By default, the currently authenticated user is used as the scope.
use Laravel\Pennant\Feature;if (Feature::active('new-api')) { // Use the new API}
For class-based features, pass the class name:
use App\Features\NewApi;if (Feature::active(NewApi::class)) { // ...}
Additional helper methods are available:
// All of the given features are activeFeature::allAreActive(['new-api', 'site-redesign']);// Any of the given features are activeFeature::someAreActive(['new-api', 'site-redesign']);// Feature is inactiveFeature::inactive('new-api');// All features are inactiveFeature::allAreInactive(['new-api', 'site-redesign']);// Any feature is inactiveFeature::someAreInactive(['new-api', 'site-redesign']);
Pennant provides @feature and @featureany Blade directives:
@feature('site-redesign') {{-- 'site-redesign' is active --}}@else {{-- 'site-redesign' is inactive --}}@endfeature@featureany(['site-redesign', 'beta']) {{-- at least one is active --}}@endfeatureany
Use EnsureFeaturesAreActive to require features to be active before a route can be accessed. If any listed feature is inactive, a 400 Bad Request response is returned.
use Laravel\Pennant\Middleware\EnsureFeaturesAreActive;Route::get('/api/servers', function () { // ...})->middleware(EnsureFeaturesAreActive::using('new-api', 'servers-api'));
Customize the response using whenInactive:
EnsureFeaturesAreActive::whenInactive( function (Request $request, array $features) { return new Response(status: 403); });
Pennant caches resolved feature values in memory for the duration of a request. The same feature will not trigger additional database queries when checked multiple times.Manually flush the cache with:
If the scope is null (unauthenticated routes, Artisan commands, queued jobs) and the feature definition does not handle null, Pennant returns false. Use nullable types to handle this:
@feature('purchase-button', 'blue-sapphire') {{-- blue-sapphire is active --}}@elsefeature('purchase-button', 'seafoam-green') {{-- seafoam-green is active --}}@elsefeature('purchase-button', 'tart-orange') {{-- tart-orange is active --}}@endfeature
When using rich values, a feature is considered “active” when it has any value other than false.
The rich value is passed to the first closure of when:
Feature::when('purchase-button', fn ($color) => /* $color contains the value */, fn () => /* inactive */,);
Avoid N+1 queries when checking features in a loop by using load:
// Without eager loading: one query per userforeach ($users as $user) { if (Feature::for($user)->active('notifications-beta')) { $user->notify(new RegistrationSuccess); }}// With eager loading: single queryFeature::for($users)->load(['notifications-beta']);foreach ($users as $user) { if (Feature::for($user)->active('notifications-beta')) { $user->notify(new RegistrationSuccess); }}
Toggle a feature on or off using activate and deactivate:
// Activate for the default scopeFeature::activate('new-api');// Deactivate for a specific scopeFeature::for($user->team)->deactivate('billing-v2');// Set a rich valueFeature::activate('purchase-button', 'seafoam-green');
To forget a stored value so the feature resolves from its definition again:
Remove all stored values for a feature using purge:
// Purge a single featureFeature::purge('new-api');// Purge multiple featuresFeature::purge(['new-api', 'purchase-button']);// Purge all featuresFeature::purge();
Use the Artisan command to purge from the command line:
The easiest way to control feature values in tests is to re-define the feature:
tab=Pest
use Laravel\Pennant\Feature;test('it can control feature values', function () { Feature::define('purchase-button', 'seafoam-green'); expect(Feature::value('purchase-button'))->toBe('seafoam-green');});
tab=PHPUnit
use Laravel\Pennant\Feature;public function test_it_can_control_feature_values(): void{ Feature::define('purchase-button', 'seafoam-green'); $this->assertSame('seafoam-green', Feature::value('purchase-button'));}
Class-based features work the same way:
tab=Pest
test('it can control feature values', function () { Feature::define(NewApi::class, true); expect(Feature::value(NewApi::class))->toBeTrue();});
tab=PHPUnit
use App\Features\NewApi;public function test_it_can_control_feature_values(): void{ Feature::define(NewApi::class, true); $this->assertTrue(Feature::value(NewApi::class));}
If the built-in drivers don’t meet your needs, implement the Laravel\Pennant\Contracts\Driver interface:
<?phpnamespace App\Extensions;use Laravel\Pennant\Contracts\Driver;class RedisFeatureDriver implements Driver{ public function define(string $feature, callable $resolver): void {} public function defined(): array {} public function getAll(array $features): array {} public function get(string $feature, mixed $scope): mixed {} public function set(string $feature, mixed $scope, mixed $value): void {} public function setForAllScopes(string $feature, mixed $value): void {} public function delete(string $feature, mixed $scope): void {} public function purge(array|null $features): void {}}
Register it using Feature::extend in a service provider:
Feature::extend('redis', function (Application $app) { return new RedisFeatureDriver($app->make('redis'), $app->make('events'), []);});