Writing Middleware
This page is about writing your own middleware: the RouteMiddleware contract, runtime parameters, registering custom aliases, and applying middleware.
RouteMiddleware Interface
The core middleware contract provides flexibility with parameter passing while maintaining clean semantics:
<?php
namespace Glueful\Routing;
use Symfony\Component\HttpFoundation\Request;
interface RouteMiddleware
{
/**
* Handle middleware processing
*
* @param Request $request The HTTP request being processed
* @param callable $next Next handler in the pipeline - call $next($request) to continue
* @param mixed ...$params Additional parameters from route or middleware config
* @return Response|mixed Response object or data to be normalized
*/
public function handle(Request $request, callable $next, mixed ...$params): mixed;
}
Basic Implementation Pattern
use Glueful\Routing\RouteMiddleware;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class ExampleMiddleware implements RouteMiddleware
{
public function handle(Request $request, callable $next, mixed ...$params): mixed
{
// Pre-processing: modify request, check conditions, etc.
$request->attributes->set('processed_at', time());
// Call next middleware or final handler
$response = $next($request);
// Post-processing: modify response, add headers, log, etc.
$response->headers->set('X-Processed-By', 'ExampleMiddleware');
return $response;
}
}
Creating Custom Middleware
Simple Middleware
use Glueful\Routing\RouteMiddleware;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
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;
}
}
Middleware with Dependencies
use Glueful\Routing\RouteMiddleware;
use Symfony\Component\HttpFoundation\Request;
use Psr\Log\LoggerInterface;
use App\Services\AuditService;
class AuditMiddleware implements RouteMiddleware
{
public function __construct(
private LoggerInterface $logger,
private AuditService $auditService
) {}
public function handle(Request $request, callable $next): mixed
{
// Log request
$this->logger->info('Request started', [
'method' => $request->getMethod(),
'uri' => $request->getRequestUri(),
'ip' => $request->getClientIp()
]);
$response = $next($request);
// Audit the action
$this->auditService->recordAction([
'user_id' => $request->attributes->get('user_id'),
'action' => $request->attributes->get('_route'),
'status' => $response->getStatusCode()
]);
return $response;
}
}
Middleware with Conditional Logic
class MaintenanceMiddleware implements RouteMiddleware
{
public function __construct(
private string $maintenanceFile = '/tmp/maintenance'
) {}
public function handle(Request $request, callable $next): mixed
{
if ($this->isInMaintenanceMode() && !$this->isExemptRoute($request)) {
return new Response('Service temporarily unavailable', 503, [
'Retry-After' => 3600,
'Content-Type' => 'application/json'
]);
}
return $next($request);
}
private function isInMaintenanceMode(): bool
{
return file_exists($this->maintenanceFile);
}
private function isExemptRoute(Request $request): bool
{
$exemptRoutes = ['/health', '/status'];
return in_array($request->getPathInfo(), $exemptRoutes);
}
}
Middleware Parameters
The framework supports runtime parameters for configurable middleware behavior:
Parameter Extraction
class CacheMiddleware implements RouteMiddleware
{
public function handle(Request $request, callable $next, mixed ...$params): mixed
{
// Extract parameters with defaults
$ttl = (int) ($params[0] ?? 300); // Default 5 minutes
$tags = isset($params[1]) ? explode(',', $params[1]) : ['default'];
$varyBy = (string) ($params[2] ?? 'path');
$cacheKey = $this->generateCacheKey($request, $varyBy);
// Check cache
$cache = app($context, Glueful\Cache\CacheStore::class);
if ($cached = $cache->get($cacheKey)) {
return new Response($cached);
}
$response = $next($request);
// Store in cache with TTL (add tags if your driver supports them)
$cache->set($cacheKey, (string) $response->getContent(), $ttl);
// Optionally: $cache->addTags($cacheKey, $tags);
return $response;
}
}
// Usage with parameters
$router->get('/api/expensive-data', [DataController::class, 'expensive'])
->middleware('cache:600,data,user'); // 10min TTL, 'data' tag, vary by user
Parameter Parsing Patterns
class FlexibleMiddleware implements RouteMiddleware
{
public function handle(Request $request, callable $next, mixed ...$params): mixed
{
$config = $this->parseParams($params);
// Use parsed configuration
if ($config['strict_mode']) {
// Strict validation
}
return $next($request);
}
private function parseParams(array $params): array
{
$config = [
'timeout' => 30,
'strict_mode' => false,
'allowed_types' => ['json', 'xml']
];
foreach ($params as $param) {
if (is_numeric($param)) {
$config['timeout'] = (int) $param;
} elseif ($param === 'strict') {
$config['strict_mode'] = true;
} elseif (str_contains($param, ',')) {
$config['allowed_types'] = explode(',', $param);
}
}
return $config;
}
}
// Usage: ->middleware('flexible:60,strict,json,xml')
Applying Middleware
Container vs Direct Instantiation
The framework supports two approaches for applying middleware: using container-registered aliases (recommended for standard cases) and direct instantiation (for custom configurations).
Method 1: Container-Registered Middleware (String Aliases)
Use string aliases when framework defaults are sufficient:
// Standard authentication using framework defaults
$router->get('/profile', [UserController::class, 'show'])
->middleware('auth');
// Multiple middleware with parameters
$router->post('/admin/users', [AdminController::class, 'createUser'])
->middleware(['auth:admin', 'csrf', 'rate_limit:10,60']);
// Group middleware
$router->group(['middleware' => ['auth']], function($router) {
$router->get('/dashboard', [DashboardController::class, 'index']);
$router->get('/settings', [SettingsController::class, 'index']);
});
Method 2: Direct Instantiation with Custom Configuration
Use direct instantiation when you need custom configuration:
// Custom API authentication - API keys only, no expiration validation
$apiAuthMiddleware = new AuthMiddleware(
authManager: null,
container: null,
providerNames: ['api_key'], // Only API keys for this endpoint
options: [
'validate_expiration' => false, // Lenient for API
'enable_events' => false, // High performance
'enable_logging' => true // But track usage
]
);
$router->get('/api/public-data', [PublicApiController::class, 'getData'])
->middleware([$apiAuthMiddleware]);
// Different configuration for internal API
$internalAuthMiddleware = new AuthMiddleware(
providerNames: ['jwt'],
options: [
'validate_expiration' => true, // Strict for internal
'enable_events' => true,
'enable_logging' => true
]
);
$router->group(['middleware' => [$internalAuthMiddleware]], function($router) {
$router->get('/internal/metrics', [InternalController::class, 'metrics']);
$router->post('/internal/admin-action', [InternalController::class, 'adminAction'])
->middleware('admin'); // Can still mix with string aliases
});
Method 3: Mixed Usage (Recommended Pattern)
Combine both approaches strategically:
// Use string aliases for standard cases
$router->group(['middleware' => ['auth']], function($router) {
// Standard routes use framework defaults
$router->get('/profile', [UserController::class, 'profile']);
$router->get('/dashboard', [DashboardController::class, 'index']);
// Special routes use custom middleware instances
$router->get('/experimental-feature', [ExperimentalController::class, 'index'])
->middleware([
new App\Middleware\FeatureFlagMiddleware('experimental_ui'),
new App\Middleware\ABTestMiddleware()
]);
});
Container Registration (custom aliases)
To add your own string alias for middleware, load a binding into the container (e.g., during bootstrap or in a provider):
$container = container($context);
$container->load([
// String alias => middleware instance (or factory/definition)
'custom_rate_limit' => new App\Middleware\CustomRateLimitMiddleware(1000, 3600),
]);
// Then use it in routes
$router->get('/api/data', [ApiController::class, 'getData'])
->middleware(['custom_rate_limit', 'auth']);
Registering Custom Middleware in Container
For your own middleware that you want to use with string aliases:
// In your ServiceProvider
class AppServiceProvider implements ServiceProviderInterface
{
public function register(ContainerBuilder $container): void
{
// Register custom middleware
$container->register(App\Middleware\CustomRateLimitMiddleware::class)
->setArguments([1000, 3600]) // maxRequests, windowSeconds
->setPublic(true);
// Create string alias
$container->setAlias('custom_rate_limit', App\Middleware\CustomRateLimitMiddleware::class)
->setPublic(true);
}
}
// Then use with string:
$router->get('/api/data', [ApiController::class, 'getData'])
->middleware(['custom_rate_limit', 'auth']);
Route-Level Middleware
// Single middleware
$router->get('/protected', [SecureController::class, 'data'])
->middleware('auth');
// Multiple middleware (executed in order)
$router->post('/api/upload', [UploadController::class, 'store'])
->middleware(['auth', 'rate_limit:10,60', 'csrf']);
// Middleware with parameters
$router->get('/cached-data', [DataController::class, 'expensive'])
->middleware(['cache:300,data', 'rate_limit:100,60']);
Group-Level Middleware
// Apply middleware to entire groups
$router->group(['middleware' => ['auth', 'rate_limit:1000,3600']], function($router) {
$router->get('/dashboard', [DashboardController::class, 'index']);
$router->get('/profile', [ProfileController::class, 'show']);
$router->put('/profile', [ProfileController::class, 'update']);
});
// Nested groups inherit parent middleware
$router->group(['middleware' => ['auth']], function($router) {
// User routes (inherits 'auth')
$router->get('/user/data', [UserController::class, 'data']);
// Admin routes (inherits 'auth', adds 'admin')
$router->group(['prefix' => '/admin', 'middleware' => ['admin_permission']], function($router) {
$router->get('/users', [AdminController::class, 'users']);
$router->delete('/users/{id}', [AdminController::class, 'deleteUser']);
});
});
Global Middleware
// Apply to all routes (in bootstrap or service provider)
$router->group(['middleware' => ['security_headers', 'metrics']], function($router) {
// Load all application routes
require __DIR__ . '/routes/api.php';
require __DIR__ . '/routes/web.php';
});
Best Practices
1. Middleware Ordering
Order middleware strategically for optimal performance and security:
// Recommended order:
$router->group(['middleware' => [
'security_headers', // 1. Apply security headers early
'rate_limit:100,60', // 2. Rate limiting before expensive operations
'auth', // 3. Authentication check
'csrf', // 4. CSRF validation (requires auth context)
'admin_permission', // 5. Authorization checks
'field_selection', // 6. Query optimization
'metrics', // 7. Metrics collection
'request_logging' // 8. Logging (captures full context)
]], function($router) {
// Protected routes
});
2. Parameter Configuration
Use clear, consistent parameter patterns:
// Good: Clear parameter meanings
->middleware('rate_limit:100,60,user') // max, window, type
->middleware('cache:300,api-data,user') // ttl, tag, vary-by
// Bad: Unclear parameters
->middleware('config:100,60,1,user') // What do these numbers mean?
3. Error Handling
Provide meaningful error responses:
class AuthMiddleware implements RouteMiddleware
{
public function handle(Request $request, callable $next, mixed ...$params): mixed
{
try {
$user = $this->authService->authenticate($request);
} catch (AuthenticationException $e) {
return new JsonResponse([
'error' => 'Authentication failed',
'message' => 'Please provide valid credentials',
'code' => 'AUTH_FAILED'
], 401);
}
$request->attributes->set('user', $user);
return $next($request);
}
}
4. Performance Considerations
class CachingMiddleware implements RouteMiddleware
{
private array $cache = []; // In-memory cache for single request
public function handle(Request $request, callable $next, mixed ...$params): mixed
{
$cacheKey = $this->getCacheKey($request);
// Check in-memory cache first (fastest)
if (isset($this->cache[$cacheKey])) {
return $this->cache[$cacheKey];
}
// Check distributed cache
if ($cached = $this->redis->get($cacheKey)) {
return $this->cache[$cacheKey] = unserialize($cached);
}
$response = $next($request);
// Store in both caches
$this->cache[$cacheKey] = $response;
$this->redis->setex($cacheKey, 300, serialize($response));
return $response;
}
}
5. Testing Middleware
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class RateLimitMiddlewareTest extends TestCase
{
public function test_allows_requests_within_limit(): void
{
$middleware = new RateLimiterMiddleware(maxAttempts: 5, windowSeconds: 60);
$request = Request::create('/test', 'GET');
$next = fn($req) => new Response('OK');
// First request should pass
$response = $middleware->handle($request, $next);
$this->assertEquals(200, $response->getStatusCode());
}
public function test_blocks_requests_over_limit(): void
{
$middleware = new RateLimiterMiddleware(maxAttempts: 1, windowSeconds: 60);
$request = Request::create('/test', 'GET');
$next = fn($req) => new Response('OK');
// First request passes
$middleware->handle($request, $next);
// Second request should be blocked
$response = $middleware->handle($request, $next);
$this->assertEquals(429, $response->getStatusCode());
}
}