laravel/pennant
Laravel Pennant is a simple, lightweight feature flag library for Laravel. Define and evaluate feature flags to safely roll out, test, and target functionality in your app. Official docs available on the Laravel website.
## Getting Started
### Minimal Setup
1. **Installation**:
```bash
composer require laravel/pennant
php artisan vendor:publish --provider="Laravel\Pennant\PennantServiceProvider" --tag="pennant-config"
php artisan migrate
config/pennant.php) and runs migrations to create the feature_flags table.First Feature Flag:
use Laravel\Pennant\Facades\Pennant;
// Enable a boolean flag
Pennant::enable('new-dashboard');
// Check if a flag is active
if (Pennant::isEnabled('new-dashboard')) {
// Feature logic
}
Blade Directive (for views):
@feature('new-dashboard')
<div>New Dashboard Content</div>
@endfeature
A/B Testing for a New Checkout Flow:
// Enable for 50% of users (randomized)
Pennant::enableForUsers('new-checkout', 0.5);
// Check in middleware
public function handle(Request $request, Closure $next) {
if (Pennant::isEnabledForUser('new-checkout', $request->user())) {
return redirect()->route('new.checkout');
}
return $next($request);
}
Pennant::enable('feature-x'); // Boolean (true)
Pennant::disable('feature-x'); // Boolean (false)
Pennant::set('feature-x', ['config' => 'value']); // Complex value
Pennant::setMany([
'feature-a' => true,
'feature-b' => ['tier' => 'premium'],
]);
// Enable for specific users
Pennant::enableForUser('premium-features', $user);
// Check
if (Pennant::isEnabledForUser('premium-features', $user)) { ... }
scope):
Pennant::enableForScope('admin-dashboard', 'admin');
// Check
if (Pennant::isEnabledForScope('admin-dashboard', 'admin')) { ... }
use Laravel\Pennant\Http\Middleware\EnsureFeatureIsActive;
// Route middleware
Route::middleware([EnsureFeatureIsActive::class => 'new-api'])->group(...);
// All features check
Route::middleware([EnsureFeatureIsActive::class => 'all'])->group(...);
public function handle($request, Closure $next, $feature) {
if (!Pennant::isEnabled($feature)) {
abort(403, "Feature {$feature} is disabled.");
}
return $next($request);
}
@feature('dark-mode')
<button class="dark-mode-toggle">Toggle Dark Mode</button>
@endfeature
@feature('new-header', false)
<header class="new-header">...</header>
@else
<header class="old-header">...</header>
@endfeature
use Laravel\Pennant\Events\FeatureUpdated;
FeatureUpdated::listen(function (FeatureUpdated $event) {
Log::info("Flag {$event->feature->name} updated to {$event->value}");
});
Pennant::enable('payment-gateway', function ($feature) {
if ($feature->value === 'stripe' && !config('services.stripe.key')) {
throw new \Exception('Stripe key not configured!');
}
});
config/pennant.php:
'driver' => 'database',
'table' => 'custom_feature_flags',
'connection' => 'mysql_replica',
Pennant::setMany([...]);
Pennant::flushCache(); // Clear cached flags
tags in config to invalidate specific flags:
'cache' => [
'prefix' => 'pennant_',
'tags' => ['features'], // Invalidate by tag
],
Pennant::shouldReceive('isEnabled')->with('test-flag')->andReturn(true);
use Laravel\Pennant\Testing\TestsPennant;
class FeatureTest extends TestCase {
use TestsPennant;
public function test_flag() {
$this->enable('test-flag');
$this->assertEnabled('test-flag');
}
}
Driver:
use Laravel\Pennant\Contracts\Driver;
class RedisDriver implements Driver {
public function isEnabled($name) { ... }
public function set($name, $value) { ... }
// ...
}
Pennant::extend('redis', function () {
return new RedisDriver();
});
Route::get('/flags/{name}', function ($name) {
return Pennant::get($name);
});
Route::post('/flags', function (Request $request) {
Pennant::set($request->name, $request->value);
return response()->json(['status' => 'updated']);
});
Scope Mismatches:
// Fails silently if scope is missing
Pennant::isEnabledForUser('flag', null); // Avoid!
defaultScope():
Pennant::setDefaultScope('user');
Pennant::isEnabled('flag'); // Uses 'user' scope
Cache Invalidation:
flushCache() after bulk operations or use cache tags:
Pennant::setMany([...]);
Cache::tags(['features'])->flush();
Type Safety:
int, bool) failing scope validation.nameMap in decorators:
Pennant::decorate('flag', function ($feature) {
return (bool) $feature->value; // Force boolean
});
Migration Conflicts:
'table' => 'app_feature_flags',
Middleware Order:
EnsureFeatureIsActive after auth middleware:
Route::middleware(['auth', EnsureFeatureIsActive::class => 'dashboard'])->group(...);
Blade Directive Scope:
@feature not respecting the current scope.@feature('flag', 'user', auth()->user())
Zero Values:
0 as false (e.g., from DB).Pennant::get('flag') to inspect raw values:
$value = Pennant::get('flag'); // Returns 0 or 1
if ($value === 1) { ... }
Log Flag Checks:
Pennant::enableLogging();
// Logs all flag evaluations to storage/logs/pennant.log
Inspect Raw Data:
// Dump all flags for a scope
dd(Pennant::getAllForScope('user', auth()->user()));
Check Cache:
// Clear cache and
How can I help you explore Laravel packages today?