laravel/pennant
Laravel Pennant is a simple, lightweight feature flag library for Laravel. Define and evaluate feature toggles, control rollouts, and experiment safely across environments. Official docs available at laravel.com/docs/pennant.
## Getting Started
### Minimal Setup
1. **Installation**:
```bash
composer require laravel/pennant
php artisan pennant:install
This publishes the migration and config file, and creates a default features table.
Configuration:
Review config/pennant.php to adjust:
database, cache, or custom).First Feature Flag:
Define a feature in a migration or via the pennant:create command:
php artisan pennant:create new-feature --value=true
Or programmatically:
use Laravel\Pennant\Feature;
Feature::create('new-feature', true);
Usage in Code: Check a feature flag:
if (Feature::get('new-feature')) {
// Feature is active
}
Or use the helper:
if (pennant('new-feature')) {
// Feature is active
}
Feature Flag Management:
Feature::create('name', $value, $scope = null) or the pennant:create Artisan command.Feature::update('name', $value) or bulk updates with Feature::updateMany([...]).Feature::delete('name') or Feature::purge() for all features.Scoped Features: Define features for specific scopes (e.g., user roles, environments):
Feature::create('beta-feature', true, 'admin');
Retrieve scoped features:
Feature::get('beta-feature', 'admin'); // Returns true
Feature::get('beta-feature'); // Returns null (no default scope)
Middleware Integration: Protect routes with feature flags:
Route::middleware(['feature:new-feature'])->group(function () {
// Routes only accessible if 'new-feature' is active
});
Or use the EnsureFeaturesAreActive middleware:
Route::middleware(['feature:new-feature|beta-feature'])->group(...);
Blade Directives: Toggle UI elements dynamically:
@feature('new-feature')
<button>Enable New Feature</button>
@endfeature
Or use @featureany for multiple flags:
@featureany('flag1|flag2')
<div>At least one feature is active</div>
@endfeatureany
Events and Hooks: Listen for feature updates:
use Laravel\Pennant\Events\FeatureUpdated;
FeatureUpdated::listen(function ($event) {
Log::info("Feature {$event->feature->name} updated to {$event->value}");
});
Use before hooks for pre-flight checks:
Feature::create('premium-feature', false, 'premium')
->before(function ($feature) {
return auth()->user()->isPremium();
});
Bulk Operations: Load all features at once (e.g., for caching):
$allFeatures = Feature::loadAll();
Update multiple features efficiently:
Feature::updateMany([
'feature1' => true,
'feature2' => false,
]);
Custom Drivers:
Extend the Driver interface to support non-database backends (e.g., Redis, DynamoDB):
use Laravel\Pennant\Contracts\Driver;
class RedisDriver implements Driver {
public function get($name, $scope = null) { ... }
public function set($name, $value, $scope = null) { ... }
// ... other methods
}
Register in config/pennant.php:
'driver' => \App\Drivers\RedisDriver::class,
Testing:
Use the Feature facade in tests to mock feature flags:
public function test_feature_flag()
{
Feature::shouldReceive('get')
->with('test-feature')
->andReturn(true);
$this->assertTrue(pennant('test-feature'));
}
Or use the FeatureTestCase trait for cleaner assertions:
use Laravel\Pennant\Testing\FeatureTestCase;
class MyTest extends FeatureTestCase {
public function test_feature()
{
$this->assertFeatureActive('test-feature');
}
}
Performance:
Feature::loadAll() to preload flags if checking multiple in a single request.'cache_ttl' => 0 in config).Database Optimization:
insertAll method (v1.8.1+) to batch inserts.updated_at column name in migrations if needed.Scope Mismatches:
Feature::get('admin-feature'); // Returns null if no default scope is set
config/pennant.php:
'default_scope' => 'user',
Cache Staleness:
Feature::flushCache();
Or configure automatic cache flushing in config/pennant.php:
'cache_flush_on_update' => true,
Middleware Misconfiguration:
EnsureFeaturesAreActive middleware failing silently.app/Http/Kernel.php:
protected $routeMiddleware = [
'feature' => \Laravel\Pennant\Http\Middleware\EnsureFeaturesAreActive::class,
];
feature alias:
Route::middleware(['feature:flag1,flag2'])->group(...);
Type Hints and Scopes:
bool, int) failing scope validation.Feature::create('numeric-feature', (int)100, 'scope');
Migration Conflicts:
pennant:install is run multiple times.--force:
php artisan migrate --force
Blade Directive Scope:
@feature directives not working as expected.AppServiceProvider@boot():
if (! Blade::directive('feature')) {
Blade::directive('feature', function ($expression) {
return "<?php if (\\Laravel\\Pennant\\Feature::get({$expression})): ?>";
});
}
Log Feature Checks: Add a temporary log to debug feature retrieval:
\Log::debug('Feature check', [
'name' => 'test-feature',
'scope' => 'user',
'value' => Feature::get('test-feature', 'user'),
]);
Check Database Values: Verify feature values in the database:
php artisan tinker
>>> \DB::table('features')->where('name', 'test-feature')->get();
Validate Config:
Ensure config/pennant.php matches your environment:
return [
'driver' => env('PENNANT_DRIVER', 'database'),
'table' => env('PENNANT_TABLE', 'features'),
'default_scope' => null,
'cache_ttl' => env('PENNANT_CACHE_TTL', 60),
];
Custom Feature Classes:
Extend the Feature class to add domain-specific logic:
class CustomFeature extends \Laravel\Pennant\Feature {
public static function isPremium($name) {
return self::get($name, 'premium') ?? false;
}
}
Driver Decorators: Wrap the
How can I help you explore Laravel packages today?