Feature flags enable gradual rollouts and A/B testing without deployments. Laravel Pennant provides first-party feature flag management. At ZIRA Software, feature flags reduce deployment risk and enable data-driven decisions.
Installation
composer require laravel/pennant
php artisan vendor:publish --provider="Laravel\Pennant\PennantServiceProvider"
php artisan migrate
Defining Features
// app/Providers/AppServiceProvider.php
use Laravel\Pennant\Feature;
use App\Models\User;
public function boot(): void
{
// Simple boolean feature
Feature::define('new-dashboard', fn () => true);
// User-based feature
Feature::define('beta-features', fn (User $user) => $user->is_beta_tester);
// Percentage rollout
Feature::define('new-checkout', fn (User $user) => match (true) {
$user->is_internal => true,
$user->created_at->isAfter('2022-01-01') => true,
default => false,
});
// Lottery-based (10% of users)
Feature::define('experiment-a', fn () => Lottery::odds(1, 10)->choose());
}
Feature Class Definition
// app/Features/NewOnboarding.php
namespace App\Features;
use App\Models\User;
use Illuminate\Support\Lottery;
class NewOnboarding
{
public function resolve(User $user): bool
{
// Internal users always get new features
if ($user->hasRole('internal')) {
return true;
}
// Beta testers
if ($user->is_beta_tester) {
return true;
}
// 25% rollout to remaining users
return Lottery::odds(1, 4)->choose();
}
}
// Register in AppServiceProvider
Feature::define('new-onboarding', NewOnboarding::class);
Checking Features
// In controllers
use Laravel\Pennant\Feature;
class DashboardController extends Controller
{
public function index()
{
if (Feature::active('new-dashboard')) {
return view('dashboard.new');
}
return view('dashboard.classic');
}
}
// For specific user
if (Feature::for($user)->active('beta-features')) {
// Show beta features
}
// Check multiple features
if (Feature::allAreActive(['feature-a', 'feature-b'])) {
// Both features active
}
if (Feature::someAreActive(['feature-a', 'feature-b'])) {
// At least one active
}
Blade Directives
@feature('new-dashboard')
<x-new-dashboard-component />
@else
<x-classic-dashboard-component />
@endfeature
@feature('premium-features')
<div class="premium-badge">Premium</div>
@endfeature
Feature Values (A/B Testing)
// Define feature with values
Feature::define('checkout-button-color', fn () => Arr::random([
'blue',
'green',
'orange',
]));
// In controller
$buttonColor = Feature::value('checkout-button-color');
return view('checkout', [
'buttonColor' => $buttonColor,
]);
<!-- In view -->
<button class="btn btn-{{ Feature::value('checkout-button-color') }}">
Complete Purchase
</button>
Storing Feature Decisions
// Features are stored per-scope (user) by default
// Force activate for user
Feature::for($user)->activate('new-dashboard');
// Force deactivate
Feature::for($user)->deactivate('new-dashboard');
// Forget stored value (re-evaluate)
Feature::for($user)->forget('new-dashboard');
// Activate with value
Feature::for($user)->activate('button-color', 'green');
Middleware for Feature Gates
// routes/web.php
Route::middleware('feature:new-dashboard')->group(function () {
Route::get('/dashboard/v2', [DashboardController::class, 'newDashboard']);
});
// Custom response for inactive feature
Route::get('/beta', [BetaController::class, 'index'])
->middleware('feature:beta-features,redirect:/waitlist');
Rich Feature Configuration
// app/Features/PricingExperiment.php
class PricingExperiment
{
public function resolve(User $user): array
{
$variant = $this->determineVariant($user);
return [
'variant' => $variant,
'prices' => $this->getPricesForVariant($variant),
'layout' => $this->getLayoutForVariant($variant),
];
}
private function determineVariant(User $user): string
{
// Consistent variant per user
$hash = crc32($user->id . 'pricing-experiment');
$bucket = $hash % 100;
return match (true) {
$bucket < 33 => 'control',
$bucket < 66 => 'variant-a',
default => 'variant-b',
};
}
}
Event Tracking
// Track feature exposure for analytics
Feature::when('new-checkout',
fn () => $this->trackExperimentExposure('new-checkout', 'treatment'),
fn () => $this->trackExperimentExposure('new-checkout', 'control'),
);
// Custom tracking service
class FeatureTrackingService
{
public function trackExposure(User $user, string $feature, mixed $value): void
{
FeatureExposure::create([
'user_id' => $user->id,
'feature' => $feature,
'value' => $value,
'exposed_at' => now(),
]);
}
}
Purging Old Features
# Remove stored values for removed features
php artisan pennant:purge new-dashboard
# Remove all stored values
php artisan pennant:clear
Conclusion
Laravel Pennant provides robust feature flag management for gradual rollouts and experimentation. First-party integration ensures seamless Laravel development experience.
Need feature flag implementation? Contact ZIRA Software for controlled deployments.