Table Of Contents
- Introduction
- What Are Feature Flags and Why Use Them in Laravel?
- Basic Feature Flag Implementation in Laravel
- Advanced Feature Flag Implementation
- Popular Laravel Feature Flag Packages
- Performance Optimization for Feature Flags
- Testing Feature Flags in Laravel
- Real-World Use Cases and Best Practices
- Troubleshooting Common Issues
- Frequently Asked Questions
- Conclusion
Introduction
Feature flags (also known as feature toggles) have become an essential tool for modern Laravel developers who want to deploy code safely, test new features with specific user groups, and maintain better control over their application's functionality. If you've ever wished you could release features gradually, perform A/B tests seamlessly, or quickly disable problematic features without deploying new code, then implementing feature flags in your Laravel application is the solution you need.
The challenge many Laravel developers face is knowing where to start with feature flag implementation. Should you build a custom solution, use a third-party service, or leverage existing Laravel packages? How do you ensure your feature flags don't negatively impact performance, and what are the best practices for managing flags across different environments?
In this comprehensive guide, you'll learn everything you need to know about implementing feature flags in Laravel, from basic boolean toggles to advanced percentage-based rollouts and user segmentation. We'll cover multiple implementation approaches, performance optimization techniques, testing strategies, and real-world use cases that will help you make informed decisions for your Laravel projects.
What Are Feature Flags and Why Use Them in Laravel?
Feature flags are conditional statements in your code that allow you to enable or disable functionality without deploying new code. They act as runtime switches that give you complete control over which features are active for which users at any given time.
Key Benefits of Feature Flags in Laravel Applications
Safer Deployments and Reduced Risk Feature flags allow you to deploy code to production with new features disabled, then gradually enable them once you're confident everything works correctly. This separation of deployment from feature release significantly reduces the risk of breaking your application.
A/B Testing and Experimentation With feature flags, you can easily test different versions of features with specific user segments, gathering valuable data about user behavior and feature performance before rolling out changes to everyone.
Improved Developer Workflow Multiple developers can work on different features simultaneously without worrying about incomplete code affecting the main application. Features can be developed behind flags and merged into the main branch even before they're complete.
Emergency Feature Control When issues arise, you can instantly disable problematic features without emergency deployments, maintaining application stability and user experience.
Basic Feature Flag Implementation in Laravel
Let's start with the simplest approach to implementing feature flags using Laravel's built-in configuration system.
Method 1: Configuration-Based Feature Flags
Create a dedicated configuration file for your feature flags:
// config/features.php
<?php
return [
'new_dashboard' => env('FEATURE_NEW_DASHBOARD', false),
'payment_gateway_v2' => env('FEATURE_PAYMENT_V2', false),
'advanced_search' => env('FEATURE_ADVANCED_SEARCH', true),
'beta_features' => env('FEATURE_BETA_ACCESS', false),
];
Add the corresponding environment variables to your .env
file:
FEATURE_NEW_DASHBOARD=false
FEATURE_PAYMENT_V2=true
FEATURE_ADVANCED_SEARCH=true
FEATURE_BETA_ACCESS=false
Create a helper service to manage feature flags:
// app/Services/FeatureFlagService.php
<?php
namespace App\Services;
class FeatureFlagService
{
public function isEnabled(string $feature): bool
{
return config("features.{$feature}", false);
}
public function isDisabled(string $feature): bool
{
return !$this->isEnabled($feature);
}
public function getEnabledFeatures(): array
{
return array_filter(config('features', []), function ($value) {
return $value === true;
});
}
}
Register the service in your AppServiceProvider
:
// app/Providers/AppServiceProvider.php
public function register()
{
$this->app->singleton(FeatureFlagService::class);
}
Using Feature Flags in Controllers
Here's how to use feature flags in your Laravel controllers:
// app/Http/Controllers/DashboardController.php
<?php
namespace App\Http\Controllers;
use App\Services\FeatureFlagService;
class DashboardController extends Controller
{
private $featureFlags;
public function __construct(FeatureFlagService $featureFlags)
{
$this->featureFlags = $featureFlags;
}
public function index()
{
if ($this->featureFlags->isEnabled('new_dashboard')) {
return view('dashboard.new');
}
return view('dashboard.legacy');
}
}
Feature Flags in Blade Templates
Create a Blade directive for easy template usage:
// app/Providers/AppServiceProvider.php
use Illuminate\Support\Facades\Blade;
use App\Services\FeatureFlagService;
public function boot()
{
Blade::directive('feature', function ($expression) {
return "<?php if(app(App\Services\FeatureFlagService::class)->isEnabled({$expression})): ?>";
});
Blade::directive('endfeature', function () {
return '<?php endif; ?>';
});
}
Use it in your Blade templates:
@feature('new_dashboard')
<div class="new-dashboard-widget">
<!-- New dashboard content -->
</div>
@endfeature
@if(!app(App\Services\FeatureFlagService::class)->isEnabled('new_dashboard'))
<div class="legacy-dashboard">
<!-- Legacy dashboard content -->
</div>
@endif
Advanced Feature Flag Implementation
For more sophisticated feature flag requirements, let's implement a database-driven solution with user segmentation and percentage-based rollouts.
Database-Driven Feature Flags
Create the necessary migrations:
// database/migrations/create_feature_flags_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateFeatureFlagsTable extends Migration
{
public function up()
{
Schema::create('feature_flags', function (Blueprint $table) {
$table->id();
$table->string('key')->unique();
$table->string('name');
$table->text('description')->nullable();
$table->boolean('is_active')->default(false);
$table->integer('rollout_percentage')->default(0);
$table->json('targeting_rules')->nullable();
$table->timestamps();
});
Schema::create('feature_flag_users', function (Blueprint $table) {
$table->id();
$table->foreignId('feature_flag_id')->constrained()->onDelete('cascade');
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->boolean('is_enabled');
$table->timestamps();
$table->unique(['feature_flag_id', 'user_id']);
});
}
public function down()
{
Schema::dropIfExists('feature_flag_users');
Schema::dropIfExists('feature_flags');
}
}
Create the Feature Flag model:
// app/Models/FeatureFlag.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class FeatureFlag extends Model
{
protected $fillable = [
'key',
'name',
'description',
'is_active',
'rollout_percentage',
'targeting_rules',
];
protected $casts = [
'is_active' => 'boolean',
'targeting_rules' => 'array',
];
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class, 'feature_flag_users')
->withPivot('is_enabled')
->withTimestamps();
}
}
Advanced Feature Flag Service
Create a more sophisticated service with user targeting and percentage rollouts:
// app/Services/AdvancedFeatureFlagService.php
<?php
namespace App\Services;
use App\Models\FeatureFlag;
use App\Models\User;
use Illuminate\Support\Facades\Cache;
class AdvancedFeatureFlagService
{
private const CACHE_TTL = 300; // 5 minutes
public function isEnabledForUser(string $flagKey, ?User $user = null): bool
{
$flag = $this->getFeatureFlag($flagKey);
if (!$flag || !$flag->is_active) {
return false;
}
// Check if user has explicit flag assignment
if ($user && $this->hasExplicitUserAssignment($flag, $user)) {
return $this->getExplicitUserAssignment($flag, $user);
}
// Check targeting rules
if ($user && $this->matchesTargetingRules($flag, $user)) {
return true;
}
// Check percentage rollout
return $this->isInPercentageRollout($flag, $user);
}
private function getFeatureFlag(string $key): ?FeatureFlag
{
return Cache::remember(
"feature_flag_{$key}",
self::CACHE_TTL,
fn() => FeatureFlag::where('key', $key)->first()
);
}
private function hasExplicitUserAssignment(FeatureFlag $flag, User $user): bool
{
return $flag->users()->where('user_id', $user->id)->exists();
}
private function getExplicitUserAssignment(FeatureFlag $flag, User $user): bool
{
$pivot = $flag->users()->where('user_id', $user->id)->first()?->pivot;
return $pivot ? $pivot->is_enabled : false;
}
private function matchesTargetingRules(FeatureFlag $flag, User $user): bool
{
$rules = $flag->targeting_rules ?? [];
foreach ($rules as $rule) {
switch ($rule['type']) {
case 'user_attribute':
if ($this->matchesUserAttribute($rule, $user)) {
return true;
}
break;
case 'user_segment':
if ($this->matchesUserSegment($rule, $user)) {
return true;
}
break;
}
}
return false;
}
private function matchesUserAttribute(array $rule, User $user): bool
{
$attribute = $rule['attribute'];
$operator = $rule['operator'];
$value = $rule['value'];
$userValue = $user->{$attribute} ?? null;
return match ($operator) {
'equals' => $userValue == $value,
'not_equals' => $userValue != $value,
'contains' => str_contains($userValue, $value),
'greater_than' => $userValue > $value,
'less_than' => $userValue < $value,
default => false,
};
}
private function matchesUserSegment(array $rule, User $user): bool
{
$segment = $rule['segment'];
return match ($segment) {
'premium_users' => $user->isPremium(),
'beta_testers' => $user->isBetaTester(),
'admin_users' => $user->isAdmin(),
default => false,
};
}
private function isInPercentageRollout(FeatureFlag $flag, ?User $user): bool
{
if ($flag->rollout_percentage >= 100) {
return true;
}
if ($flag->rollout_percentage <= 0) {
return false;
}
// Use consistent hashing for stable rollout
$identifier = $user ? $user->id : request()->ip();
$hash = crc32($flag->key . $identifier);
$percentage = abs($hash) % 100;
return $percentage < $flag->rollout_percentage;
}
public function enableForUser(string $flagKey, User $user): void
{
$flag = $this->getFeatureFlag($flagKey);
if ($flag) {
$flag->users()->syncWithoutDetaching([
$user->id => ['is_enabled' => true]
]);
}
}
public function disableForUser(string $flagKey, User $user): void
{
$flag = $this->getFeatureFlag($flagKey);
if ($flag) {
$flag->users()->syncWithoutDetaching([
$user->id => ['is_enabled' => false]
]);
}
}
}
Popular Laravel Feature Flag Packages
While building custom solutions gives you full control, several excellent packages can accelerate your implementation.
Laravel Pennant
Laravel Pennant is the official first-party package for feature flags:
composer require laravel/pennant
php artisan vendor:publish --provider="Laravel\Pennant\PennantServiceProvider"
php artisan migrate
Define feature flags:
// app/Providers/AppServiceProvider.php
use Laravel\Pennant\Feature;
public function boot()
{
Feature::define('new-api', fn (User $user) => match (true) {
$user->team->plan === 'enterprise' => true,
$user->isInternalTeamMember() => true,
$user->isEarlyAdopter() => lottery([1, 10]),
default => false,
});
}
LaravelFlag Package
Another popular option with database storage:
composer require spatie/laravel-feature-flags
php artisan migrate
Usage example:
use Spatie\LaravelFeatureFlags\Models\FeatureFlag;
// Create a feature flag
FeatureFlag::create([
'name' => 'new-dashboard',
'is_active' => true,
]);
// Check if feature is enabled
if (feature('new-dashboard')) {
// Feature code here
}
Performance Optimization for Feature Flags
Feature flags can impact performance if not implemented carefully. Here are optimization strategies:
Caching Strategies
Implement multi-level caching to minimize database queries:
// app/Services/CachedFeatureFlagService.php
<?php
namespace App\Services;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Redis;
class CachedFeatureFlagService extends AdvancedFeatureFlagService
{
private array $runtimeCache = [];
public function isEnabledForUser(string $flagKey, ?User $user = null): bool
{
$cacheKey = $this->generateUserCacheKey($flagKey, $user);
// Check runtime cache first
if (isset($this->runtimeCache[$cacheKey])) {
return $this->runtimeCache[$cacheKey];
}
// Check Redis cache
$result = Cache::remember($cacheKey, 300, function () use ($flagKey, $user) {
return parent::isEnabledForUser($flagKey, $user);
});
// Store in runtime cache
$this->runtimeCache[$cacheKey] = $result;
return $result;
}
private function generateUserCacheKey(string $flagKey, ?User $user): string
{
$userId = $user ? $user->id : 'anonymous';
return "feature_flag_{$flagKey}_user_{$userId}";
}
public function clearCache(string $flagKey = null): void
{
if ($flagKey) {
Cache::forget("feature_flag_{$flagKey}");
// Clear user-specific caches
$pattern = "feature_flag_{$flagKey}_user_*";
$this->clearCachePattern($pattern);
} else {
$this->clearCachePattern('feature_flag_*');
}
$this->runtimeCache = [];
}
private function clearCachePattern(string $pattern): void
{
$keys = Redis::keys($pattern);
if (!empty($keys)) {
Redis::del($keys);
}
}
}
Middleware for Feature Flag Resolution
Create middleware to resolve feature flags early in the request lifecycle:
// app/Http/Middleware/ResolveFeatureFlags.php
<?php
namespace App\Http\Middleware;
use App\Services\AdvancedFeatureFlagService;
use Closure;
use Illuminate\Http\Request;
class ResolveFeatureFlags
{
private $featureFlagService;
public function __construct(AdvancedFeatureFlagService $featureFlagService)
{
$this->featureFlagService = $featureFlagService;
}
public function handle(Request $request, Closure $next)
{
$user = $request->user();
$flags = ['new_dashboard', 'payment_v2', 'advanced_search'];
$resolvedFlags = [];
foreach ($flags as $flag) {
$resolvedFlags[$flag] = $this->featureFlagService->isEnabledForUser($flag, $user);
}
// Make flags available throughout the request
$request->attributes->set('feature_flags', $resolvedFlags);
view()->share('featureFlags', $resolvedFlags);
return $next($request);
}
}
Testing Feature Flags in Laravel
Proper testing ensures your feature flags work correctly across different scenarios.
Unit Testing Feature Flag Logic
// tests/Unit/FeatureFlagServiceTest.php
<?php
namespace Tests\Unit;
use App\Models\User;
use App\Models\FeatureFlag;
use App\Services\AdvancedFeatureFlagService;
use Tests\TestCase;
class FeatureFlagServiceTest extends TestCase
{
private $service;
protected function setUp(): void
{
parent::setUp();
$this->service = new AdvancedFeatureFlagService();
}
public function test_inactive_flag_returns_false()
{
FeatureFlag::factory()->create([
'key' => 'test_feature',
'is_active' => false,
]);
$user = User::factory()->create();
$this->assertFalse($this->service->isEnabledForUser('test_feature', $user));
}
public function test_percentage_rollout_works_correctly()
{
FeatureFlag::factory()->create([
'key' => 'test_feature',
'is_active' => true,
'rollout_percentage' => 50,
]);
$users = User::factory()->count(100)->create();
$enabledCount = 0;
foreach ($users as $user) {
if ($this->service->isEnabledForUser('test_feature', $user)) {
$enabledCount++;
}
}
// Should be approximately 50% (allow for some variance)
$this->assertGreaterThan(40, $enabledCount);
$this->assertLessThan(60, $enabledCount);
}
public function test_explicit_user_assignment_overrides_percentage()
{
$flag = FeatureFlag::factory()->create([
'key' => 'test_feature',
'is_active' => true,
'rollout_percentage' => 0, // 0% rollout
]);
$user = User::factory()->create();
// Explicitly enable for this user
$flag->users()->attach($user->id, ['is_enabled' => true]);
$this->assertTrue($this->service->isEnabledForUser('test_feature', $user));
}
}
Feature Testing with Different Flag States
// tests/Feature/DashboardTest.php
<?php
namespace Tests\Feature;
use App\Models\User;
use App\Models\FeatureFlag;
use Tests\TestCase;
class DashboardTest extends TestCase
{
public function test_new_dashboard_shown_when_flag_enabled()
{
FeatureFlag::factory()->create([
'key' => 'new_dashboard',
'is_active' => true,
'rollout_percentage' => 100,
]);
$user = User::factory()->create();
$response = $this->actingAs($user)->get('/dashboard');
$response->assertSee('new-dashboard-widget');
$response->assertDontSee('legacy-dashboard');
}
public function test_legacy_dashboard_shown_when_flag_disabled()
{
FeatureFlag::factory()->create([
'key' => 'new_dashboard',
'is_active' => false,
]);
$user = User::factory()->create();
$response = $this->actingAs($user)->get('/dashboard');
$response->assertSee('legacy-dashboard');
$response->assertDontSee('new-dashboard-widget');
}
}
Real-World Use Cases and Best Practices
Gradual Feature Rollouts
Implement a systematic approach to rolling out features:
// app/Console/Commands/RolloutFeature.php
<?php
namespace App\Console\Commands;
use App\Models\FeatureFlag;
use Illuminate\Console\Command;
class RolloutFeature extends Command
{
protected $signature = 'feature:rollout {flag} {percentage}';
protected $description = 'Gradually rollout a feature to a percentage of users';
public function handle()
{
$flagKey = $this->argument('flag');
$percentage = (int) $this->argument('percentage');
$flag = FeatureFlag::where('key', $flagKey)->first();
if (!$flag) {
$this->error("Feature flag '{$flagKey}' not found.");
return 1;
}
$flag->update(['rollout_percentage' => $percentage]);
$this->info("Feature '{$flagKey}' rolled out to {$percentage}% of users.");
return 0;
}
}
A/B Testing Implementation
Create a service for managing A/B tests:
// app/Services/ABTestService.php
<?php
namespace App\Services;
use App\Models\User;
class ABTestService
{
private $featureFlagService;
public function __construct(AdvancedFeatureFlagService $featureFlagService)
{
$this->featureFlagService = $featureFlagService;
}
public function getVariant(string $testName, User $user): string
{
$variantAFlag = "{$testName}_variant_a";
$variantBFlag = "{$testName}_variant_b";
if ($this->featureFlagService->isEnabledForUser($variantAFlag, $user)) {
return 'A';
} elseif ($this->featureFlagService->isEnabledForUser($variantBFlag, $user)) {
return 'B';
}
return 'control';
}
public function trackConversion(string $testName, User $user, string $event): void
{
$variant = $this->getVariant($testName, $user);
// Log conversion event for analytics
logger()->info('AB Test Conversion', [
'test' => $testName,
'variant' => $variant,
'user_id' => $user->id,
'event' => $event,
'timestamp' => now(),
]);
}
}
Feature Flag Management Dashboard
Create routes and controllers for managing feature flags:
// routes/web.php
Route::middleware(['auth', 'admin'])->prefix('admin')->group(function () {
Route::get('/feature-flags', [FeatureFlagController::class, 'index']);
Route::post('/feature-flags/{flag}/toggle', [FeatureFlagController::class, 'toggle']);
Route::put('/feature-flags/{flag}/rollout', [FeatureFlagController::class, 'updateRollout']);
});
// app/Http/Controllers/FeatureFlagController.php
<?php
namespace App\Http\Controllers;
use App\Models\FeatureFlag;
use Illuminate\Http\Request;
class FeatureFlagController extends Controller
{
public function index()
{
$flags = FeatureFlag::with('users')->get();
return view('admin.feature-flags.index', compact('flags'));
}
public function toggle(FeatureFlag $flag)
{
$flag->update(['is_active' => !$flag->is_active]);
return redirect()->back()->with('success',
"Feature '{$flag->name}' " . ($flag->is_active ? 'enabled' : 'disabled'));
}
public function updateRollout(Request $request, FeatureFlag $flag)
{
$request->validate([
'rollout_percentage' => 'required|integer|min:0|max:100',
]);
$flag->update(['rollout_percentage' => $request->rollout_percentage]);
return redirect()->back()->with('success',
"Rollout percentage updated to {$request->rollout_percentage}%");
}
}
Troubleshooting Common Issues
Cache Invalidation Problems
When feature flags don't update immediately, it's usually a caching issue:
// Create a cache invalidation listener
// app/Listeners/ClearFeatureFlagCache.php
<?php
namespace App\Listeners;
use App\Services\CachedFeatureFlagService;
class ClearFeatureFlagCache
{
private $featureFlagService;
public function __construct(CachedFeatureFlagService $featureFlagService)
{
$this->featureFlagService = $featureFlagService;
}
public function handle($event)
{
if (isset($event->flag)) {
$this->featureFlagService->clearCache($event->flag->key);
} else {
$this->featureFlagService->clearCache();
}
}
}
Database Performance Issues
Monitor and optimize feature flag queries:
// Add database indexes
Schema::table('feature_flags', function (Blueprint $table) {
$table->index(['key', 'is_active']);
$table->index('rollout_percentage');
});
Schema::table('feature_flag_users', function (Blueprint $table) {
$table->index(['user_id', 'is_enabled']);
});
Memory Usage with Large User Bases
Implement pagination for user-specific flag operations:
public function bulkEnableForUsers(string $flagKey, array $userIds): void
{
$flag = $this->getFeatureFlag($flagKey);
if (!$flag) return;
// Process in chunks to avoid memory issues
collect($userIds)->chunk(1000)->each(function ($chunk) use ($flag) {
$data = $chunk->mapWithKeys(fn($userId) => [$userId => ['is_enabled' => true]])->toArray();
$flag->users()->syncWithoutDetaching($data);
});
}
Frequently Asked Questions
Q: Should I use database-driven or configuration-based feature flags? Configuration-based flags are simpler and faster but require deployments to change. Database-driven flags offer more flexibility and real-time control but add complexity and potential performance overhead. Use configuration for simple on/off toggles and database for dynamic flags requiring percentage rollouts or user targeting.
Q: How do feature flags impact application performance? Properly implemented feature flags have minimal performance impact. The key is effective caching strategies and avoiding database queries on every flag check. Runtime caching, Redis caching, and resolving flags early in the request lifecycle can keep performance overhead under 1-2ms per request.
Q: What's the best way to clean up old feature flags? Establish a feature flag lifecycle process: create flags with expiration dates, regularly audit active flags, and remove flags once features are fully rolled out or permanently disabled. Consider creating a command that identifies unused flags and generates cleanup reports for your team.
Q: How do I handle feature flags in different environments (staging, production)? Use environment-specific configuration files or database seeders to set appropriate flag states. Staging should typically have all flags enabled for testing, while production uses controlled rollouts. Consider environment-aware flag defaults and deployment scripts that sync flag states appropriately.
Q: Can feature flags cause security issues? Feature flags themselves don't create security vulnerabilities, but they can expose sensitive features if not properly controlled. Always validate user permissions before checking flags, never rely solely on flags for security, and audit flag access regularly. Treat feature flag management as a privileged operation requiring appropriate access controls.
Q: How do I test applications with multiple feature flag combinations? Use automated testing with feature flag fixtures or factories to test different flag combinations systematically. Create test cases for each significant flag combination rather than testing all possible permutations. Consider using property-based testing tools to generate flag combination test cases automatically.
Conclusion
Implementing feature flags in Laravel applications provides significant benefits for deployment safety, user experience optimization, and development workflow improvement. Whether you choose a simple configuration-based approach for basic needs or a sophisticated database-driven solution with user targeting and percentage rollouts, the key is to start simple and evolve your implementation as your requirements grow.
The most important takeaways from this guide are: establish clear naming conventions and lifecycle processes for your flags, implement proper caching strategies to maintain performance, thoroughly test flag combinations to avoid unexpected behavior, and regularly audit and clean up obsolete flags to prevent technical debt accumulation.
Remember that feature flags are tools for enabling better software delivery practices, not permanent architectural components. The best feature flag implementations are invisible to users and transparent to developers, providing safety and flexibility without adding complexity to your daily development workflow.
Ready to implement feature flags in your Laravel application? Start with the basic configuration approach outlined in this guide, then gradually add more sophisticated features as your needs evolve. Share your experience with feature flag implementation in the comments below, and subscribe to our newsletter for more Laravel development tips and best practices.
Add Comment
No comments yet. Be the first to comment!