Mastering Feature Flags with Laravel Pennant
In modern development environments, deploying code to production frequently (CI/CD) is the standard. But how do you merge new feature code into the main branch without affecting users when that feature isn't ready? Or how do you enable a new feature only for a team of Beta Testers?
The answer is Feature Flags.
Laravel Pennant is a first-party package that makes managing Feature Flags extremely simple, lightweight, and "Laravel-way".
Why Feature Flags?
- Trunk-based Development: Developers merge code daily, hiding unfinished code behind a
FALSEflag. - Canary Releases: Enable new features for 1% of users to monitor for errors before releasing to 100%.
- A/B Testing: Show UI A to this group, B to that group to measure effectiveness.
- Kill Switch: Instantly turn off a buggy feature without needing to rollback code.
Installing Pennant
composer require laravel/pennant
php artisan vendor:publish --provider="Laravel\Pennant\PennantServiceProvider"
php artisan migrate
By default, Pennant stores flags in the database. It also supports redis or array (for testing).
Defining Features
Typically, you define features in AppServiceProvider:
use Laravel\Pennant\Feature;
use App\Models\User;
public function boot(): void
{
// Simple flag: on or off for everyone
Feature::define('new-dashboard', fn (User $user) => match ($user->role) {
'admin', 'beta-tester' => true,
default => false,
});
// Lottery: Randomly for 10% of users
Feature::define('site-redesign', fn (User $user) => \Laravel\Pennant\Feature::lottery(1 in: 10));
}
These features can also be defined using separate Classes if the logic is complex.
Usage in Code
In Blade View
The @feature helper makes view code very clean:
@feature('new-dashboard')
<!-- New Interface -->
<x-dashboard-v2 />
@else
<!-- Old Interface -->
<x-dashboard-v1 />
@endfeature
In Controller
public function index()
{
if (Feature::active('new-dashboard')) {
return view('dashboard.v2');
}
return view('dashboard.v1');
}
Middleware
Pennant provides middleware to protect routes:
Route::get('/analytics', AnalyticsController::class)
->middleware('features:view-analytics');
Class Based Features
For large features, it's better to use Classes to manage state.
php artisan pennant:feature NewBillingSystem
This Class will have a resolve method. The return value doesn't strictly have to be boolean. It can be string, number, or object (Rich Feature).
// app/Features/NewBillingSystem.php
public function resolve(User $user)
{
return $user->isPremium() ? 'stripe' : 'paypal';
}
Usage:
$gateway = Feature::value(NewBillingSystem::class); // returns 'stripe' or 'paypal'
Testing with Pennant
A strong point of official packages is comprehensive test support.
public function test_it_shows_new_dashboard_when_enabled()
{
Feature::activate('new-dashboard');
$response = $this->get('/dashboard');
$response->assertSee('Welcome to Dashboard V2');
}
Rollout Strategy
Pennant is extremely powerful when combined with business logic.
For example, you want to enable the AI Chat feature for:
- Internal Admins.
- "Enterprise" customers.
- Old users registered before 2024.
Instead of writing messy if/else checks everywhere, you gather it all in one place within Feature::define. Your codebase remains clean, maintainable, and most importantly, you can toggle that entire logic with a single line of code (or via UI if using Database Driver).
Conclusion
Laravel Pennant transforms Feature Flags management from a manual, error-prone task into a standardized process. If you are building a product with real users, consider adopting Pennant immediately to make your release process more confident.