Advanced
Middleware
Create and use custom middleware
Build custom middleware to handle cross-cutting concerns like authentication, logging, and request transformation.
What is Middleware?
Middleware sits between the request and your controller, allowing you to:
- Authenticate requests
- Log requests/responses
- Transform data
- Add headers
- Rate limit
- Cache responses
Creating Middleware
Implement the RouteMiddleware interface:
namespace App\Middleware;
use Glueful\Routing\RouteMiddleware;
use Symfony\Component\HttpFoundation\Request;
class TimingMiddleware implements RouteMiddleware
{
public function handle(Request $request, callable $next): mixed
{
$start = microtime(true);
$response = $next($request);
$duration = round((microtime(true) - $start) * 1000, 2);
$response->headers->set('X-Response-Time', $duration . 'ms');
return $response;
}
}
Applying Middleware
Route-Level
// Single middleware
$router->get('/profile', [ProfileController::class, 'show'])
->middleware('auth');
// Multiple middleware
$router->post('/posts', [PostController::class, 'store'])
->middleware(['auth', 'csrf']);
Group Middleware
$router->group(['middleware' => ['auth']], function($router) {
$router->get('/dashboard', [DashboardController::class, 'index']);
$router->get('/profile', [ProfileController::class, 'show']);
$router->post('/posts', [PostController::class, 'store']);
});
Global Middleware
Apply to all routes:
$router->group(['middleware' => ['security_headers', 'request_logging']], function($router) {
require __DIR__ . '/../routes/api.php';
});
Common Middleware Examples
Authentication
class AuthMiddleware implements RouteMiddleware
{
public function handle(Request $request, callable $next): mixed
{
$token = $request->header('Authorization');
if (!$token || !$this->validateToken($token)) {
return Response::unauthorized('Invalid token');
}
// Attach user to request
$request->attributes->set('user', $this->getUserFromToken($token));
return $next($request);
}
private function validateToken(string $token): bool
{
// Validate JWT token
return true;
}
private function getUserFromToken(string $token): object
{
// Decode and return user
return (object) ['id' => 123, 'name' => 'John'];
}
}
Logging
class RequestLoggingMiddleware implements RouteMiddleware
{
public function handle(Request $request, callable $next): mixed
{
logger()->info('Request started', [
'method' => $request->getMethod(),
'uri' => $request->getRequestUri(),
'ip' => $request->getClientIp(),
]);
$response = $next($request);
logger()->info('Request completed', [
'status' => $response->getStatusCode(),
]);
return $response;
}
}
CORS
class CorsMiddleware implements RouteMiddleware
{
public function handle(Request $request, callable $next): mixed
{
// Handle preflight
if ($request->getMethod() === 'OPTIONS') {
return Response::noContent()
->header('Access-Control-Allow-Origin', '*')
->header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE')
->header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
}
$response = $next($request);
// Add CORS headers to response
$response->headers->set('Access-Control-Allow-Origin', '*');
return $response;
}
}
Rate Limiting
class RateLimitMiddleware implements RouteMiddleware
{
public function __construct(
private int $maxAttempts = 60,
private int $windowSeconds = 60
) {}
public function handle(Request $request, callable $next, mixed ...$params): mixed
{
$key = 'rate_limit:' . $request->getClientIp();
$attempts = Cache::get($key, 0);
if ($attempts >= $this->maxAttempts) {
// Prefer the built-in 'rate_limit' middleware for production-ready behavior.
return Response::error('Too many requests', 429);
}
Cache::set($key, $attempts + 1, $this->windowSeconds);
$response = $next($request);
$response->headers->set('X-RateLimit-Limit', (string) $this->maxAttempts);
$response->headers->set('X-RateLimit-Remaining', (string) ($this->maxAttempts - $attempts - 1));
return $response;
}
}
JSON Response
class JsonResponseMiddleware implements RouteMiddleware
{
public function handle(Request $request, callable $next): mixed
{
$response = $next($request);
// Ensure response is JSON
if (!$response->headers->has('Content-Type')) {
$response->headers->set('Content-Type', 'application/json');
}
return $response;
}
}
Middleware with Parameters
Accept parameters in middleware:
class RoleMiddleware implements RouteMiddleware
{
public function handle(Request $request, callable $next, mixed ...$params): mixed
{
$requiredRole = $params[0] ?? 'user';
$user = $request->attributes->get('user');
if (!$user || $user->role !== $requiredRole) {
return Response::forbidden('Insufficient permissions');
}
return $next($request);
}
}
Use with parameters:
// Require admin role
$router->delete('/users/{id}', [UserController::class, 'destroy'])
->middleware('role:admin');
// Require moderator role
$router->put('/posts/{id}/publish', [PostController::class, 'publish'])
->middleware('role:moderator');
Middleware Ordering
Order matters! Middleware executes in the order defined:
// ✅ Good order
$router->group(['middleware' => [
'cors', // Handle CORS first
'rate_limit', // Rate limit early
'auth', // Then authenticate
'role:admin', // Check permissions
'request_logging', // Log after checks
]], function($router) {
// Routes
});
// ❌ Bad order
$router->group(['middleware' => [
'request_logging', // Logs unauthorized requests
'role:admin', // Checks role before auth
'auth', // Authenticates too late
'rate_limit', // Rate limits after expensive work
]], function($router) {
// Routes
});
Conditional Middleware
Apply middleware conditionally:
class MaintenanceModeMiddleware implements RouteMiddleware
{
public function handle(Request $request, callable $next): mixed
{
if (config('app.maintenance_mode') && !$this->isWhitelisted($request)) {
return Response::error('Service under maintenance', 503);
}
return $next($request);
}
private function isWhitelisted(Request $request): bool
{
$whitelistedIps = config('app.maintenance_whitelist', []);
return in_array($request->getClientIp(), $whitelistedIps);
}
}
## Built-in Middleware Aliases
Registered aliases you can use directly in routes/groups:
- `auth` — Authentication middleware
- `rate_limit` — Rate limiter (with params: `max,window[,type]`)
- `csrf` — CSRF protection
- `security_headers` — Security headers
- `allow_ip` — IP allowlist checks
- `admin` — Admin permission checks
- `request_logging` — Request/response logging
- `metrics` — Metrics collection
- `tracing` — Distributed tracing
- `async` — Inject FiberScheduler for per-route concurrency
- `lockdown` — Emergency lockdown
- `gate_permissions` — Gate-based permission checks
- `auth_to_request` — Attach auth context to request attributes
Note: Aliases are registered in providers (see CoreProvider) and may vary if customized. Use strings like `'rate_limit:60,60'` or mix with class instances.
## PSR-15 Middleware
Glueful includes a built‑in PSR‑15 bridge. You can attach any PSR‑15 middleware directly, and the router will auto‑wrap it when `http.psr15.enabled` and `http.psr15.auto_detect` are true (default).
```php
use Middlewares\Cors; // example third‑party PSR‑15 middleware
$psrMiddleware = new Cors();
$router->get('/data', [ApiController::class, 'index'])
->middleware([$psrMiddleware]); // router detects and wraps PSR‑15
Configuration (config/http.php):
'psr15' => [
'enabled' => env('PSR15_ENABLED', true),
'auto_detect' => env('PSR15_AUTO_DETECT', true),
// Optional factory provider for PSR‑17 factories; auto‑detected if omitted
'factory_provider' => null,
'throw_on_missing_bridge' => env('PSR15_STRICT', true),
],
How it works (for reference):
- Router auto-detects PSR‑15 middleware and wraps it via a resolver trait: src/Routing/Internal/Psr15MiddlewareResolverTrait.php
- The bridge that exposes Glueful middleware as PSR‑15 and performs conversions lives at: src/Http/Bridge/Psr15/MiddlewareAsPsr15.php
- Toggle behavior and factory provisioning in: config/http.php
Further Reading
- Middleware Cookbook: RouteMiddleware interface, parameters, and advanced patterns — see /cookbook/middleware
## Terminating Middleware
Run code after response is sent:
```php
class MetricsMiddleware implements RouteMiddleware
{
public function handle(Request $request, callable $next): mixed
{
$start = microtime(true);
$response = $next($request);
// This runs after response is sent to client
register_shutdown_function(function() use ($request, $response, $start) {
$duration = microtime(true) - $start;
// Send metrics to external service
$this->sendMetrics([
'route' => $request->attributes->get('_route'),
'method' => $request->getMethod(),
'status' => $response->getStatusCode(),
'duration' => $duration,
]);
});
return $response;
}
}
Testing Middleware
class TimingMiddlewareTest extends TestCase
{
public function test_adds_timing_header()
{
$middleware = new TimingMiddleware();
$request = Request::create('/test', 'GET');
$response = $middleware->handle($request, function($req) {
return Response::success(['ok' => true]);
});
$this->assertTrue($response->headers->has('X-Response-Time'));
$this->assertMatchesRegularExpression('/\d+\.\d+ms/', $response->headers->get('X-Response-Time'));
}
}
Best Practices
Keep Middleware Focused
// ✅ Good - single responsibility
class AuthMiddleware { /* auth only */ }
class LoggingMiddleware { /* logging only */ }
// ❌ Bad - too many responsibilities
class RequestMiddleware {
public function handle($request, $next) {
// Auth
// Logging
// Rate limiting
// Validation
// ...
}
}
Early Returns
// ✅ Good - fail fast
public function handle($request, $next)
{
if (!$this->isAuthenticated($request)) {
return Response::unauthorized();
}
return $next($request);
}
// ❌ Bad - unnecessary nesting
public function handle($request, $next)
{
if ($this->isAuthenticated($request)) {
return $next($request);
} else {
return Response::unauthorized();
}
}
Use Dependency Injection
class CacheMiddleware implements RouteMiddleware
{
public function __construct(
private CacheManager $cache,
private int $ttl = 3600
) {}
public function handle(Request $request, callable $next): mixed
{
$key = $this->getCacheKey($request);
if ($cached = $this->cache->get($key)) {
return Response::success($cached);
}
$response = $next($request);
$this->cache->set($key, $response->getContent(), $this->ttl);
return $response;
}
}
Next Steps
- Testing - Test your middleware
- Configuration - Configure middleware
- Performance - Optimize middleware