Route Middleware
Toggl provides middleware to protect routes based on feature activation status. Use these to ensure certain features are active (or inactive) before allowing access to routes.
Available Middleware
Section titled “Available Middleware”EnsureFeaturesAreActive
Section titled “EnsureFeaturesAreActive”Aborts with 400 if any required features are inactive.
use Cline\Cline\Toggl\Http\Middleware\EnsureFeaturesAreActive;
// Using static constructorRoute::get('/dashboard', DashboardController::class) ->middleware(EnsureFeaturesAreActive::using('new-dashboard'));
// Multiple features (all must be active)Route::get('/analytics', AnalyticsController::class) ->middleware(EnsureFeaturesAreActive::using('analytics', 'reporting'));
// Middleware alias (register in bootstrap/app.php or Kernel)Route::get('/dashboard', DashboardController::class) ->middleware('feature:new-dashboard,analytics');EnsureFeaturesAreInactive
Section titled “EnsureFeaturesAreInactive”Aborts with 400 if any specified features are active. Useful for legacy routes that should only be accessible when new features are disabled.
use Cline\Cline\Toggl\Http\Middleware\EnsureFeaturesAreInactive;
// Legacy endpoint only available when new dashboard is offRoute::get('/old-dashboard', LegacyDashboardController::class) ->middleware(EnsureFeaturesAreInactive::using('new-dashboard'));
// Multiple features (all must be inactive)Route::get('/legacy-api', LegacyApiController::class) ->middleware(EnsureFeaturesAreInactive::using('api-v2', 'api-v3'));Registering Middleware Aliases
Section titled “Registering Middleware Aliases”In bootstrap/app.php (Laravel 11+):
->withMiddleware(function (Middleware $middleware) { $middleware->alias([ 'feature' => \Cline\Toggl\Http\Middleware\EnsureFeaturesAreActive::class, 'feature.inactive' => \Cline\Toggl\Http\Middleware\EnsureFeaturesAreInactive::class, ]);})Or in app/Http/Kernel.php (Laravel 10):
protected $middlewareAliases = [ // ... 'feature' => \Cline\Toggl\Http\Middleware\EnsureFeaturesAreActive::class, 'feature.inactive' => \Cline\Toggl\Http\Middleware\EnsureFeaturesAreInactive::class,];Custom Response Handling
Section titled “Custom Response Handling”By default, middleware aborts with a 400 status. Customize this behavior:
For Active Checks
Section titled “For Active Checks”use Cline\Cline\Toggl\Http\Middleware\EnsureFeaturesAreActive;
// In a service provider boot methodEnsureFeaturesAreActive::whenInactive(function ($request, $features) { // Redirect to upgrade page return redirect('/upgrade')->with('required_features', $features);});
// Or return a custom responseEnsureFeaturesAreActive::whenInactive(function ($request, $features) { return response()->json([ 'error' => 'Feature not available', 'required_features' => $features, ], 403);});
// Reset to default behaviorEnsureFeaturesAreActive::whenInactive(null);For Inactive Checks
Section titled “For Inactive Checks”use Cline\Cline\Toggl\Http\Middleware\EnsureFeaturesAreInactive;
// Redirect when trying to access legacy route with new features enabledEnsureFeaturesAreInactive::whenActive(function ($request, $features) { return redirect('/dashboard')->with('message', 'You have been upgraded!');});
// Reset to default behaviorEnsureFeaturesAreInactive::whenActive(null);Route Group Examples
Section titled “Route Group Examples”Feature-Gated Section
Section titled “Feature-Gated Section”// All routes require premium featureRoute::middleware(EnsureFeaturesAreActive::using('premium'))->group(function () { Route::get('/premium/dashboard', PremiumDashboardController::class); Route::get('/premium/analytics', PremiumAnalyticsController::class); Route::get('/premium/reports', PremiumReportsController::class);});Beta Features
Section titled “Beta Features”// Beta routes only accessible when beta is enabledRoute::prefix('beta') ->middleware(EnsureFeaturesAreActive::using('beta-program')) ->group(function () { Route::get('/new-editor', BetaEditorController::class); Route::get('/ai-assistant', BetaAiController::class); });Legacy Routes During Migration
Section titled “Legacy Routes During Migration”// Old routes only available when new features are offRoute::middleware(EnsureFeaturesAreInactive::using('checkout-v2'))->group(function () { Route::get('/checkout', LegacyCheckoutController::class); Route::post('/checkout/process', LegacyCheckoutProcessController::class);});
// New routes require new featuresRoute::middleware(EnsureFeaturesAreActive::using('checkout-v2'))->group(function () { Route::get('/checkout', NewCheckoutController::class); Route::post('/checkout/process', NewCheckoutProcessController::class);});API Versioning
Section titled “API Versioning”// API v1 - legacy (only when v2 is not enabled)Route::prefix('api/v1') ->middleware(EnsureFeaturesAreInactive::using('api-v2')) ->group(function () { Route::apiResource('users', Api\V1\UserController::class); });
// API v2 - new versionRoute::prefix('api/v2') ->middleware(EnsureFeaturesAreActive::using('api-v2')) ->group(function () { Route::apiResource('users', Api\V2\UserController::class); });Debug Mode
Section titled “Debug Mode”In debug mode (APP_DEBUG=true), error messages include the feature names:
Required features [analytics, reporting] are not active.Features [legacy-api] must be inactive.In production, generic error messages are shown for security.
Testing Routes with Middleware
Section titled “Testing Routes with Middleware”use Cline\Toggl\Toggl;use App\Models\User;
test('premium route requires premium feature', function () { // Arrange $user = User::factory()->create(); Toggl::for($user)->deactivate('premium');
// Act & Assert $this->actingAs($user) ->get('/premium/dashboard') ->assertStatus(400);});
test('premium route accessible with feature active', function () { // Arrange $user = User::factory()->create(); Toggl::for($user)->activate('premium');
// Act & Assert $this->actingAs($user) ->get('/premium/dashboard') ->assertOk();});
test('legacy route inaccessible when new feature is active', function () { // Arrange $user = User::factory()->create(); Toggl::for($user)->activate('new-dashboard');
// Act & Assert $this->actingAs($user) ->get('/old-dashboard') ->assertStatus(400);});
test('legacy route accessible when new feature is inactive', function () { // Arrange $user = User::factory()->create(); Toggl::for($user)->deactivate('new-dashboard');
// Act & Assert $this->actingAs($user) ->get('/old-dashboard') ->assertOk();});
test('different users can have different feature states', function () { // Arrange $premiumUser = User::factory()->create(); $freeUser = User::factory()->create();
Toggl::for($premiumUser)->activate('premium'); Toggl::for($freeUser)->deactivate('premium');
// Act & Assert $this->actingAs($premiumUser) ->get('/premium/dashboard') ->assertOk();
$this->actingAs($freeUser) ->get('/premium/dashboard') ->assertStatus(400);});Guest Context
Section titled “Guest Context”For unauthenticated requests, the middleware uses a guest context (TogglContext::simple('guest', 'guest')). You can activate features globally for guests:
// Activate globally for everyone including guestsToggl::activateForEveryone('public-feature');
// Or define with a default valueToggl::define('public-feature', true);