Skip to Content
ModulesMiddleware

Middleware

Middleware acts as a filter that processes requests before they reach the controller, and after the controller has generated a response.

Why Use Middleware

Typical tasks for middleware:

  • Authentication check — allow only authorized users
  • Permission check — allow only administrators
  • CSRF protection — check form token
  • Request limiting — spam and DDoS protection
  • Logging — record request information
  • Caching — return cached response

How It Works

User Request ┌─────────────────┐ │ Middleware 1 │ ← Authentication Check └────────┬────────┘ ┌─────────────────┐ │ Middleware 2 │ ← Permission Check └────────┬────────┘ ┌─────────────────┐ │ Controller │ ← Request Processing └────────┬────────┘ ┌─────────────────┐ │ Middleware 2 │ ← Can modify response └────────┬────────┘ ┌─────────────────┐ │ Middleware 1 │ ← Can modify response └────────┬────────┘ User Response

Each middleware can:

  1. Pass the request further (call $next($request))
  2. Interrupt the chain and return its own response (e.g., “Access Denied”)

Built-in Middleware

Flute CMS provides ready-made middleware for typical tasks:

Middleware Groups

GroupIncludesWhen to Use
defaultban.check, throttle, maintenanceAutomatically applied to ALL routes
webcsrf, throttleFor regular pages with forms
apithrottle, ban.checkFor API endpoints

Individual Middleware

MiddlewareWhat It DoesUsage Example
authAllows only authorized users#[Middleware(['auth'])]
guestAllows only guestsLogin page
csrfChecks CSRF tokenForm protection
can:permissionChecks specific permission#[Middleware(['can:manage_users'])]
throttleLimits request rateSpam protection
tokenChecks API tokenAPI authorization
htmxSpecial HTMX handlingFor HTMX requests

The default group is always applied. Even if you specify withoutMiddleware(['throttle']), elements from default won’t be removed.

Configuring Throttle

The throttle middleware protects against excessive requests:

// Format: throttle:strategy,limit,interval,policy // 60 requests per minute by IP #[Middleware(['throttle:ip,60,60'])] // 120 requests per minute by user #[Middleware(['throttle:user,120,60'])] // IP + User combination #[Middleware(['throttle:ip_user,100,60'])]

Parameters:

  • Strategy: ip, user, ip_user
  • Limit: number of allowed requests
  • Interval: period in seconds (or string: 1 minute)
  • Policy: fixed_window, sliding_window, token_bucket

Using Middleware

On Entire Controller

The #[Middleware] attribute on the class applies to ALL methods:

<?php namespace Flute\Modules\Blog\Http\Controllers; use Flute\Core\Router\Annotations\Get; use Flute\Core\Router\Annotations\Middleware; use Flute\Core\Support\BaseController; /** * All methods of this controller require authorization. */ #[Middleware(['auth'])] class ProfileController extends BaseController { #[Get('/profile')] public function index() { // Only authorized users will get here return response()->view('profile::index'); } #[Get('/profile/settings')] public function settings() { // And here too return response()->view('profile::settings'); } }

On Individual Method

You can add middleware only to a specific method:

#[Middleware(['web'])] class ArticleController extends BaseController { /** * View article — available to everyone. */ #[Get('/articles/{id}')] public function show(int $id) { // middleware: web } /** * Edit article — only author or admin. */ #[Get('/articles/{id}/edit')] #[Middleware(['auth', 'can:edit_articles'])] public function edit(int $id) { // middleware: web + auth + can:edit_articles } /** * Delete — only with CSRF check. */ #[Post('/articles/{id}/delete')] #[Middleware(['auth', 'csrf'])] public function delete(int $id) { // middleware: web + auth + csrf } }

In Route Files

<?php // Single route with middleware router()->get('/admin', [AdminController::class, 'index']) ->middleware(['auth', 'can:admin_access']); // Route group — middleware applies to all router()->group(['middleware' => ['auth']], function () { router()->get('/dashboard', [DashboardController::class, 'index']); router()->get('/settings', [SettingsController::class, 'index']); }); // Nested groups router()->group(['prefix' => '/api', 'middleware' => ['api']], function () { // Public endpoints router()->get('/articles', [ArticleController::class, 'index']); // Protected endpoints router()->group(['middleware' => ['auth']], function () { router()->post('/articles', [ArticleController::class, 'store']); router()->delete('/articles/{id}', [ArticleController::class, 'destroy']); }); });

Creating Custom Middleware

Basic Structure

Create a class implementing MiddlewareInterface:

<?php namespace Flute\Modules\Blog\Middleware; use Closure; use Flute\Core\Router\Contracts\MiddlewareInterface; use Flute\Core\Support\FluteRequest; use Symfony\Component\HttpFoundation\Response; class CheckArticleOwnerMiddleware implements MiddlewareInterface { /** * Handle request. * * @param FluteRequest $request Request object * @param Closure $next Next middleware in chain * @param mixed ...$args Additional parameters * @return Response */ public function handle(FluteRequest $request, Closure $next, ...$args): Response { // 1. Perform checks BEFORE controller // Get article ID from URL $articleId = $request->getAttribute('id'); // Load article $article = rep(Article::class)->findByPK($articleId); // Article not found? if (!$article) { return response()->json(['error' => 'Article not found'], 404); } // User is not author and not admin? if ($article->author_id !== user()->id && !user()->can('manage_articles')) { return response()->json(['error' => 'No permission to edit'], 403); } // 2. Can pass data to controller via request attributes $request->attributes->set('article', $article); // 3. Pass request further $response = $next($request); // 4. Can modify response AFTER controller // $response->headers->set('X-Custom-Header', 'value'); return $response; } }

Registering Middleware

Register middleware in module provider:

<?php namespace Flute\Modules\Blog\Providers; use Flute\Core\Support\ModuleServiceProvider; use Flute\Modules\Blog\Middleware\CheckArticleOwnerMiddleware; class BlogProvider extends ModuleServiceProvider { public function boot(\DI\Container $container): void { // Register middleware with short name (alias) router()->aliasMiddleware('article.owner', CheckArticleOwnerMiddleware::class); $this->bootstrapModule(); } }

Using Custom Middleware

#[Get('/articles/{id}/edit')] #[Middleware(['auth', 'article.owner'])] // Use our alias public function edit(int $id) { // Article already loaded in middleware! $article = request()->getAttribute('article'); return response()->view('blog::edit', compact('article')); }

Middleware Examples

Request Logging

Record information about every request:

<?php namespace Flute\Modules\Analytics\Middleware; use Closure; use Flute\Core\Router\Contracts\MiddlewareInterface; use Flute\Core\Support\FluteRequest; use Symfony\Component\HttpFoundation\Response; class RequestLoggerMiddleware implements MiddlewareInterface { public function handle(FluteRequest $request, Closure $next, ...$args): Response { // Start time $startTime = microtime(true); // Log incoming request logs('requests')->info('Request started', [ 'method' => $request->getMethod(), 'url' => $request->getRequestUri(), 'ip' => $request->getClientIp(), 'user_id' => user()->isLoggedIn() ? user()->id : null, ]); // Pass request further $response = $next($request); // Calculate duration $duration = round((microtime(true) - $startTime) * 1000, 2); // Log result logs('requests')->info('Request finished', [ 'status' => $response->getStatusCode(), 'duration_ms' => $duration, ]); return $response; } }

Page Caching

Return cached response for GET requests:

<?php namespace Flute\Modules\Cache\Middleware; use Closure; use Flute\Core\Router\Contracts\MiddlewareInterface; use Flute\Core\Support\FluteRequest; use Symfony\Component\HttpFoundation\Response; class PageCacheMiddleware implements MiddlewareInterface { public function handle(FluteRequest $request, Closure $next, int $ttl = 3600): Response { // Cache only GET requests if ($request->getMethod() !== 'GET') { return $next($request); } // Do not cache for authorized users (might have personalized content) if (user()->isLoggedIn()) { return $next($request); } // Generate cache key $cacheKey = 'page_' . md5($request->getRequestUri()); // Check cache $cached = cache()->get($cacheKey); if ($cached) { return $cached; // Return from cache } // Execute request $response = $next($request); // Cache only successful responses if ($response->getStatusCode() === 200) { cache()->set($cacheKey, $response, $ttl); } return $response; } }

Usage:

#[Get('/articles')] #[Middleware(['page.cache:1800'])] // Cache for 30 minutes public function index() { }

Subscription Check

Allow only users with active subscription:

<?php namespace Flute\Modules\Premium\Middleware; use Closure; use Flute\Core\Router\Contracts\MiddlewareInterface; use Flute\Core\Support\FluteRequest; use Symfony\Component\HttpFoundation\Response; class RequireSubscriptionMiddleware implements MiddlewareInterface { public function handle(FluteRequest $request, Closure $next, string $plan = null): Response { // Check authorization if (!user()->isLoggedIn()) { if ($request->expectsJson()) { return response()->json(['error' => 'Authorization required'], 401); } return redirect(route('login')); } // Check subscription $subscription = user()->getSubscription(); if (!$subscription || !$subscription->isActive()) { if ($request->expectsJson()) { return response()->json(['error' => 'Subscription required'], 403); } return redirect(route('subscription.plans')) ->with('error', 'Access requires subscription'); } // Check specific plan (if provided) if ($plan && $subscription->plan !== $plan) { return response()->json([ 'error' => "Required plan: {$plan}" ], 403); } return $next($request); } }

Usage:

#[Get('/premium/content')] #[Middleware(['subscription'])] // Any subscription public function premiumContent() { } #[Get('/vip/content')] #[Middleware(['subscription:vip'])] // Only VIP plan public function vipContent() { }

CORS for API

Allow requests from other domains:

<?php namespace Flute\Modules\Api\Middleware; use Closure; use Flute\Core\Router\Contracts\MiddlewareInterface; use Flute\Core\Support\FluteRequest; use Symfony\Component\HttpFoundation\Response; class CorsMiddleware implements MiddlewareInterface { // Allowed domains (* = all) private array $allowedOrigins = ['*']; // Allowed HTTP methods private array $allowedMethods = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS']; // Allowed headers private array $allowedHeaders = ['Content-Type', 'Authorization', 'X-Requested-With']; public function handle(FluteRequest $request, Closure $next, ...$args): Response { // Preflight request (browser checks if request is allowed) if ($request->getMethod() === 'OPTIONS') { $response = new Response('', 200); return $this->addCorsHeaders($response); } // Normal request $response = $next($request); return $this->addCorsHeaders($response); } private function addCorsHeaders(Response $response): Response { $response->headers->set( 'Access-Control-Allow-Origin', implode(', ', $this->allowedOrigins) ); $response->headers->set( 'Access-Control-Allow-Methods', implode(', ', $this->allowedMethods) ); $response->headers->set( 'Access-Control-Allow-Headers', implode(', ', $this->allowedHeaders) ); $response->headers->set('Access-Control-Max-Age', '86400'); return $response; } }

Language Detection

Automatically detect user language:

<?php namespace Flute\Modules\I18n\Middleware; use Closure; use Flute\Core\Router\Contracts\MiddlewareInterface; use Flute\Core\Support\FluteRequest; use Symfony\Component\HttpFoundation\Response; class DetectLocaleMiddleware implements MiddlewareInterface { private array $supportedLocales = ['ru', 'en', 'de']; public function handle(FluteRequest $request, Closure $next, ...$args): Response { // Language detection priority: // 1. URL parameter (?lang=en) // 2. Saved in session // 3. From browser header // 4. Default language $locale = $request->get('lang') ?? session()->get('locale') ?? $this->detectFromBrowser($request) ?? config('app.locale', 'ru'); // Check if language is supported if (!in_array($locale, $this->supportedLocales)) { $locale = 'ru'; } // Set language app()->setLocale($locale); session()->set('locale', $locale); return $next($request); } private function detectFromBrowser(FluteRequest $request): ?string { $header = $request->headers->get('Accept-Language'); if (!$header) { return null; } // Parse header: "ru-RU,ru;q=0.9,en;q=0.8" foreach (explode(',', $header) as $part) { $locale = explode(';', trim($part))[0]; $shortLocale = explode('-', $locale)[0]; if (in_array($shortLocale, $this->supportedLocales)) { return $shortLocale; } } return null; } }

Creating Middleware Groups

If multiple middlewares are often used together, create a group:

public function boot(\DI\Container $container): void { // Register individual middleware router()->aliasMiddleware('subscription', RequireSubscriptionMiddleware::class); router()->aliasMiddleware('request.log', RequestLoggerMiddleware::class); // Create group router()->middlewareGroup('premium.api', [ 'api', 'auth', 'subscription', 'request.log', 'throttle:user,100,60' ]); $this->bootstrapModule(); }

Usage:

#[Middleware(['premium.api'])] class PremiumApiController extends BaseController { // All middleware from the group are applied automatically }

Tips

Performance

  • Cache heavy checks — if checking permissions, cache the result
  • Avoid DB queries if unnecessary — check simple conditions first
  • Use lazy loading — do not load services until needed

Security

  • Always validate input data — even in middleware
  • Log suspicious activity — too many auth errors, strange requests
  • Do not trust request data — headers and parameters can be forged

Code Organization

  • One middleware — one task — don’t mix authorization check with logging
  • Give clear namescheck.subscription is better than mw1
  • Document parameters — if middleware accepts parameters, describe them