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 ResponseEach middleware can:
- Pass the request further (call
$next($request)) - 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
| Group | Includes | When to Use |
|---|---|---|
default | ban.check, throttle, maintenance | Automatically applied to ALL routes |
web | csrf, throttle | For regular pages with forms |
api | throttle, ban.check | For API endpoints |
Individual Middleware
| Middleware | What It Does | Usage Example |
|---|---|---|
auth | Allows only authorized users | #[Middleware(['auth'])] |
guest | Allows only guests | Login page |
csrf | Checks CSRF token | Form protection |
can:permission | Checks specific permission | #[Middleware(['can:manage_users'])] |
throttle | Limits request rate | Spam protection |
token | Checks API token | API authorization |
htmx | Special HTMX handling | For 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 names —
check.subscriptionis better thanmw1 - Document parameters — if middleware accepts parameters, describe them