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 || ^13.0
- illuminate/database: ^12.0 || ^13.0
- illuminate/support: ^12.0 || ^13.0
Requires (Dev)
- infection/infection: ^0.29
- larastan/larastan: ^3.0
- laravel/pint: ^1.18
- mockery/mockery: ^1.6
- orchestra/testbench: ^10.0 || ^11.0
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^11.0 || ^12.0
README
Feature flags for Laravel with percentage rollouts, rule-based targeting and A/B variants — turn features on or off at runtime without a deploy.
Quick start
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 just use your own boolean column
A boolean column on a settings table answers a single question: is it on for
everyone? Real feature work needs more:
- Gradual rollout. Ship to 5% of users, watch your metrics, bump it to 25%, then to 100% — and a user who was in the 5% stays in as you scale, because bucketing is deterministic, not random on every request.
- Targeting. "Enterprise plans only", "users in MX and US", "accounts older than 30 days" — expressed as constraints, not as branches scattered throughout your code.
- A/B variants. Assign each user a stable variant (
bluevsgreen) and measure which one converts. - Runtime control. Turn a flag on or off from the database or an artisan command without a deploy, and instantly disable a feature that is misbehaving.
This package does all of that and, unlike Laravel Pennant, stores rollouts, constraints and variants as data you can manage — not just as closures in code.
Defining features
With the database store (the default), define and manage them 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 (ideal for tests or simple
applications) — set FEATURE_FLAGS_STORE=array and populate 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 (authenticated 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 as active only when every gate passes: the master switch is on, the scope satisfies all constraints, it falls within the rollout percentage and (for a feature with variants) a variant is assigned.
Scopes
Pass anything as a scope:
null(or omit it) — 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 of them must be satisfied. Supported operators:
= · != · in · not_in · > · >= · < · <= · contains
constraints: [
['attribute' => 'plan', 'operator' => 'in', 'value' => ['pro', 'enterprise']],
['attribute' => 'seats', 'operator' => '>=', 'value' => 10],
]
Blade and 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 interface for toggling features, adjusting rollout and creating or deleting 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 authentication/authorization middleware (for example,
['web', 'auth', 'can:manage-features']) and, in production, restrict who can access it. It manages the active store, so use the database store. Publish the views to customize them:
php artisan vendor:publish --tag=feature-flags-views
Requirements
| Component | Version |
|---|---|
| PHP | 8.2+ |
| Laravel | 12.x / 13.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.