laravel-feature-flags maintained by webrek
Laravel Feature Flags
Feature flags for Laravel with percentage rollouts, rule-based targeting and A/B variants — flip features at runtime without a deploy.
Quickstart
composer require webrek/laravel-feature-flags
php artisan vendor:publish --tag=feature-flags-migrations
php artisan migrate
use Webrek\FeatureFlags\Facades\Features;
// Define a feature rolled out to 25% of users:
Features::create('new-checkout', rollout: 25);
// Check it (defaults to the authenticated user):
if (Features::active('new-checkout')) {
// ...
}
// Or for a specific scope:
Features::for($user)->active('new-checkout');
@feature('new-checkout')
<x-checkout.v2 />
@endfeature
Why not roll your own boolean column
A boolean column on a settings table answers one question: is this on for
everyone? Real feature work needs more:
- Gradual rollout. Ship to 5% of users, watch your metrics, raise it to 25%, then 100% — and a user who was in the 5% stays in as you climb, because bucketing is deterministic, not random per request.
- Targeting. "Enterprise plans only", "users in MX and US", "accounts older than 30 days" — expressed as constraints, not branches scattered through code.
- A/B variants. Assign each user a stable variant (
bluevsgreen) and measure which converts. - Runtime control. Flip a flag from the database or an artisan command without a deploy, and kill a misbehaving feature instantly.
This package does all of that, and unlike Laravel Pennant it stores rollouts, constraints and variants as data you can manage — not just closures in code.
Defining features
With the database store (the default), define and manage at runtime:
Features::create(
'enterprise-export',
active: true,
constraints: [
['attribute' => 'plan', 'operator' => 'in', 'value' => ['pro', 'enterprise']],
],
);
Features::create('button-color', variants: [
['name' => 'blue', 'weight' => 50],
['name' => 'green', 'weight' => 50],
]);
Features::activate('new-checkout');
Features::deactivate('new-checkout');
Features::rollout('new-checkout', 50);
Features::forget('old-flag');
Or declare them in code with the array store (great for tests or simple
apps) — set FEATURE_FLAGS_STORE=array and fill config/feature-flags.php:
'features' => [
'new-checkout' => ['active' => true, 'rollout' => 25],
'button-color' => ['active' => true, 'variants' => [
['name' => 'blue', 'weight' => 50],
['name' => 'green', 'weight' => 50],
]],
],
Checking features
Features::active('new-checkout'); // default scope (auth user)
Features::active('new-checkout', $user); // explicit scope
Features::inactive('new-checkout', $team);
Features::variant('button-color', $user); // 'blue' | 'green' | null
Features::for($user)->active('new-checkout'); // fluent
feature('new-checkout'); // helper, returns bool
feature(); // helper, returns the manager
A feature resolves to active only when every gate passes: the master switch is on, the scope matches all constraints, it falls within the rollout percentage, and (for a variant feature) a variant is assigned.
Scopes
Pass anything as a scope:
null(or omit) — the authenticated user, falling back to a global scope.- An Eloquent model — bucketed by class + key; its attributes feed targeting.
- Anything implementing
FeatureScope— you control the identifier and the attributes exposed to constraints. - A string or int — used directly as the bucketing identity.
use Webrek\FeatureFlags\Contracts\FeatureScope;
class Team extends Model implements FeatureScope
{
public function featureScopeIdentifier(): string
{
return 'team:' . $this->id;
}
public function featureScopeAttributes(): array
{
return ['plan' => $this->plan, 'seats' => $this->seats];
}
}
Targeting constraints
Each constraint is ['attribute' => ..., 'operator' => ..., 'value' => ...] and
all must pass. Supported operators:
= · != · in · not_in · > · >= · < · <= · contains
constraints: [
['attribute' => 'plan', 'operator' => 'in', 'value' => ['pro', 'enterprise']],
['attribute' => 'seats', 'operator' => '>=', 'value' => 10],
]
Blade & middleware
@feature('new-dashboard')
<x-dashboard.v2 />
@endfeature
@unlessfeature('new-dashboard')
<x-dashboard.v1 />
@endfeature
Route::get('/beta', BetaController::class)->middleware('feature:new-dashboard');
// 404 unless the feature is active for the current user
Artisan
php artisan feature:list
php artisan feature:activate new-checkout
php artisan feature:deactivate new-checkout
php artisan feature:rollout new-checkout 50
Dashboard
A built-in web UI to toggle features, adjust rollout, and create or delete flags
at runtime — no deploy, no database client. It is server-rendered (no JS build,
no CDN) and lives at /feature-flags by default.
// config/feature-flags.php
'dashboard' => [
'enabled' => env('FEATURE_FLAGS_DASHBOARD', true),
'path' => 'feature-flags',
'middleware' => ['web'],
],
The dashboard controls your flags, so protect it. Add auth/authorization middleware (e.g.
['web', 'auth', 'can:manage-features']) and, in production, restrict who can reach it. It manages the active store, so use the database store. Publish the views to customise them:
php artisan vendor:publish --tag=feature-flags-views
Requirements
| Component | Version |
|---|---|
| PHP | 8.2+ |
| Laravel | 12.x |
Testing
composer install
composer test
Contributing
See CONTRIBUTING.md.
Security
Please review the security policy before reporting a vulnerability.
License
The MIT License (MIT). See LICENSE.