webrek / laravel-feature-flags
Feature flags for Laravel with percentage rollouts, rule-based targeting and A/B variants.
Requires
- php: ^8.2
- illuminate/contracts: ^12.0
- illuminate/database: ^12.0
- illuminate/support: ^12.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.18
- mockery/mockery: ^1.6
- orchestra/testbench: ^10.0
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^11.0
README
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.