Laravel Pennant provides feature flags for Laravel applications. A feature flag lets you release code without making it available to every user immediately. You can test a feature with an internal team, enable it for selected accounts, run a gradual rollout, and disable it quickly if a problem appears.
This tutorial follows Laravel 13 and the current Pennant package. Pennant stores resolved feature values so each user receives a consistent result across requests.
Install Laravel Pennant
Install the package with Composer, publish its configuration and migration, and create the database table used by the default driver.
Example:
# Install the official feature flag package.
composer require laravel/pennant
# Publish config/pennant.php and the database migration.
php artisan vendor:publish --provider="Laravel\Pennant\PennantServiceProvider"
# Create the features table.
php artisan migrate
The default database driver stores resolved values persistently. The array driver stores values in memory and is useful during automated tests.
Define a Feature Flag
Define simple flags in a service provider with the Feature facade. The resolver receives the current scope, which is usually the authenticated user.
Example:
<?php
namespace App\Providers;
use App\Models\User;
use Illuminate\Support\ServiceProvider;
use Laravel\Pennant\Feature;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Feature::define('new-dashboard', function (User $user): bool {
// Give staff members early access.
return $user->is_staff;
});
}
}
When Pennant checks this flag for a user for the first time, it resolves and stores the result. Later checks reuse that stored value instead of running the resolver again.
Check a Feature in Application Code
Use Feature::active() when the flag should use the default scope.
Example:
use Laravel\Pennant\Feature;
if (Feature::active('new-dashboard')) {
// Return the new experience for eligible users.
return view('dashboard.new');
}
return view('dashboard.current');
Use Feature::for() when you need to check the flag for a specific user or another scope.
Example:
$enabled = Feature::for($user)->active('new-dashboard');
// The result belongs to the supplied user scope.
if ($enabled) {
$user->notify(new DashboardPreviewAvailable());
}
Use Feature Flags in Blade
The @feature directive keeps conditional interface code readable.
Example:
@feature('new-dashboard')
{{-- Render the new navigation for enabled users. --}}
@include('dashboard.partials.new-navigation')
@else
@include('dashboard.partials.current-navigation')
@endfeature
Create a Class-Based Feature
Class-based flags are useful when rollout rules need several conditions or injected dependencies. Generate a feature class with Artisan.
Example:
# Create app/Features/InvoiceExport.php.
php artisan pennant:feature InvoiceExport
Add a resolve() method that returns the initial value.
Example:
<?php
namespace App\Features;
use App\Models\User;
use Illuminate\Support\Lottery;
class InvoiceExport
{
public function resolve(User $user): bool|Lottery
{
// Administrators always receive the feature.
if ($user->is_admin) {
return true;
}
// Roll it out consistently to approximately 10% of users.
return Lottery::odds(1, 10);
}
}
Check a class-based flag by passing its class name.
Example:
use App\Features\InvoiceExport;
use Laravel\Pennant\Feature;
if (Feature::active(InvoiceExport::class)) {
// Display the export control.
}
Store Rich Feature Values
A flag can return more than true or false. Rich values work well for controlled interface variants or configuration choices.
Example:
Feature::define('checkout-layout', function (User $user): string {
// Assign a stable layout according to the user's account group.
return $user->account_group === 'business' ? 'compact' : 'guided';
});
$layout = Feature::value('checkout-layout');
Keep rich values small and predictable. Store large or sensitive configuration in the application rather than inside a feature value.
Activate and Deactivate Features
You can override stored values for a specific scope without changing the feature definition.
Example:
// Enable the feature for Priya.
Feature::for($priya)->activate('new-dashboard');
// Disable it for Oliver while an issue is investigated.
Feature::for($oliver)->deactivate('new-dashboard');
// Remove the stored decision so Pennant resolves it again.
Feature::for($oliver)->forget('new-dashboard');
For a completed rollout, update the definition and then use the bulk activation or purge operations deliberately. A temporary flag should not remain in the codebase forever.
Avoid Repeated Database Queries
Pennant caches checked values in memory for the current request. When processing a collection, eager-load the required flags to avoid repeated lookups.
Example:
// Load the flag values before iterating over users.
Feature::for($users)->load(['new-dashboard']);
foreach ($users as $user) {
if (Feature::for($user)->active('new-dashboard')) {
// Perform work only for enabled users.
}
}
Test Code Behind a Feature Flag
Redefine the flag in a test so the enabled and disabled paths remain deterministic.
Example:
use Laravel\Pennant\Feature;
public function test_staff_can_open_the_new_dashboard(): void
{
// Force the flag on for this test.
Feature::define('new-dashboard', true);
$response = $this->actingAs(User::factory()->create())
->get('/dashboard');
$response->assertOk()
->assertViewIs('dashboard.new');
}
Tip: Test both flag states. A safe rollout needs confidence in the current path as well as the new path.
Feature Flag Best Practices
- Give each flag a clear, stable name that describes the capability.
- Define who owns the rollout and when the flag should be removed.
- Keep authorization checks separate; a feature flag is not a security boundary.
- Monitor errors and business results while increasing exposure.
- Avoid deeply nested flags that make application behavior difficult to predict.
- Remove the old branch and purge stored values after the rollout is complete.
Conclusion
Laravel Pennant gives Laravel applications a consistent way to define, check, store, and test feature flags. Start with a small resolver, scope it to the correct user or account, test both outcomes, and remove temporary flags after rollout. This keeps releases controlled without turning flags into permanent complexity.