diff --git a/app/Http/Controllers/Admin/FeatureFlagController.php b/app/Http/Controllers/Admin/FeatureFlagController.php new file mode 100644 index 0000000..09542f7 --- /dev/null +++ b/app/Http/Controllers/Admin/FeatureFlagController.php @@ -0,0 +1,127 @@ +getFilenameWithoutExtension(); + + if (class_exists($className)) { + $featureName = $this->getFeatureName($className); + + // Get the current state for null scope (global) + $isActive = Feature::for(null)->active($featureName); + + $features[] = [ + 'name' => $featureName, + 'class' => $className, + 'is_active' => $isActive, + 'description' => $this->getFeatureDescription($className), + ]; + } + } + } + + // Sort features by name + usort($features, fn($a, $b) => strcmp($a['name'], $b['name'])); + + return view('admin.feature-flags.index', compact('features')); + } + + /** + * Toggle a feature flag on or off. + */ + public function toggle(Request $request) + { + $validated = $request->validate([ + 'feature' => ['required', 'string'], + 'active' => ['required', 'boolean'], + ]); + + $featureName = $validated['feature']; + + // Activate or deactivate the feature for null scope (global) + if ($validated['active']) { + Feature::for(null)->activate($featureName); + $message = "Feature '{$featureName}' has been activated."; + } else { + Feature::for(null)->deactivate($featureName); + $message = "Feature '{$featureName}' has been deactivated."; + } + + return redirect()->route('admin.feature-flags.index') + ->with('success', $message); + } + + /** + * Purge a feature flag (remove all stored values). + */ + public function purge(Request $request) + { + $validated = $request->validate([ + 'feature' => ['required', 'string'], + ]); + + $featureName = $validated['feature']; + + Feature::purge($featureName); + + return redirect()->route('admin.feature-flags.index') + ->with('success', "Feature '{$featureName}' has been purged from storage."); + } + + /** + * Get the feature name from the class. + */ + private function getFeatureName(string $className): string + { + // Check if class has Name attribute + $reflection = new \ReflectionClass($className); + $attributes = $reflection->getAttributes(\Laravel\Pennant\Attributes\Name::class); + + if (!empty($attributes)) { + $nameAttribute = $attributes[0]->newInstance(); + return $nameAttribute->name; + } + + // Default to class basename + return class_basename($className); + } + + /** + * Get the feature description from class docblock. + */ + private function getFeatureDescription(string $className): ?string + { + $reflection = new \ReflectionClass($className); + $docComment = $reflection->getDocComment(); + + if ($docComment) { + // Extract the first line of the docblock as description + preg_match('/\/\*\*\s*\n\s*\*\s*(.+?)\n/s', $docComment, $matches); + if (isset($matches[1])) { + return trim($matches[1]); + } + } + + return null; + } +} diff --git a/resources/views/admin/feature-flags/index.blade.php b/resources/views/admin/feature-flags/index.blade.php new file mode 100644 index 0000000..85dc154 --- /dev/null +++ b/resources/views/admin/feature-flags/index.blade.php @@ -0,0 +1,186 @@ +@extends('layouts.app') + +@section('content') +
+
+ +
+
+
+

Feature Flags

+

Manage application feature flags

+
+ + Back to Dashboard + +
+
+ + + @if(session('success')) +
+ {{ session('success') }} +
+ @endif + + @if(session('error')) +
+ {{ session('error') }} +
+ @endif + + +
+
+ + + +
+

+ Feature flags allow you to enable or disable features across the application without deploying new code. + Toggle flags on/off to control feature availability. +

+
+
+
+ + +
+
+ + + + + + + + + + @forelse($features as $feature) + + + + + + @empty + + + + @endforelse + +
+ Feature + + Status + + Actions +
+
+
+
+ {{ $feature['name'] }} +
+ @if($feature['description']) +
+ {{ $feature['description'] }} +
+ @endif +
+ {{ $feature['class'] }} +
+
+
+
+ @if($feature['is_active']) + + + + + Active + + @else + + + + + Inactive + + @endif + +
+ +
+ @csrf + + + +
+ + +
+ @csrf + + +
+
+
+
+ + + +

No feature flags found

+

+ Add feature flag classes to the app/Features directory +

+
+
+
+
+ + +
+

How Feature Flags Work

+
+

Activate/Deactivate: Toggle a feature on or off globally for all users.

+

Purge: Remove all stored values for a feature flag. The feature will revert to its default state defined in the code.

+

Adding Features: Create new feature flag classes in app/Features directory.

+
+
+
+
+@endsection diff --git a/resources/views/components/layout/navbar.blade.php b/resources/views/components/layout/navbar.blade.php index 68b9769..a33c170 100644 --- a/resources/views/components/layout/navbar.blade.php +++ b/resources/views/components/layout/navbar.blade.php @@ -132,6 +132,16 @@ Manage Users + +
+ + + + Feature Flags +
+
@endif @if(auth()->user()->hasAnyRole(['admin', 'moderator'])) Manage Users + + Feature Flags + @endif @if(auth()->user()->hasAnyRole(['admin', 'moderator'])) prefix('admin')->name('admin.')->group(fun Route::delete('/users/{user}', [UserManagementController::class, 'destroy'])->name('users.destroy'); Route::post('/users/{user}/roles/{role}/toggle', [UserManagementController::class, 'toggleRole'])->name('users.roles.toggle'); + // Feature Flag Management + Route::get('/feature-flags', [FeatureFlagController::class, 'index'])->name('feature-flags.index'); + Route::post('/feature-flags/toggle', [FeatureFlagController::class, 'toggle'])->name('feature-flags.toggle'); + Route::post('/feature-flags/purge', [FeatureFlagController::class, 'purge'])->name('feature-flags.purge'); + Route::get('/settings', function () { return 'Settings (Admin Only)'; })->name('settings');