Middleware
Middleware provides a convenient mechanism for filtering HTTP requests entering your application. PivotPHP implements the PSR-15 middleware standard, ensuring compatibility with the broader PHP ecosystem.
Understanding Middleware
Think of middleware as layers of an onion. Each request passes through these layers on its way in, and the response passes through them in reverse order on its way out.
Request → Middleware 1 → Middleware 2 → Middleware 3 → Route Handler
↓
Response ← Middleware 1 ← Middleware 2 ← Middleware 3 ← Response
Creating Middleware
Closure Middleware
The simplest way to create middleware is using a closure:
$app->middleware(function($request, $handler) {
// Before request handling
echo "Request incoming!\n";
// Pass to next middleware/handler
$response = $handler->handle($request);
// After request handling
echo "Response outgoing!\n";
return $response;
});
Class-Based Middleware
For more complex middleware, create a class implementing MiddlewareInterface
:
namespace App\Middleware;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class AuthMiddleware implements MiddlewareInterface
{
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler
): ResponseInterface {
// Check if user is authenticated
$token = $request->getHeaderLine('Authorization');
if (!$this->isValidToken($token)) {
// Return early with error response
return new JsonResponse([
'error' => 'Unauthorized'
], 401);
}
// Add user to request
$request = $request->withAttribute('user', $this->getUserFromToken($token));
// Continue to next middleware
return $handler->handle($request);
}
private function isValidToken(string $token): bool
{
// Token validation logic
return !empty($token);
}
private function getUserFromToken(string $token)
{
// Decode token and return user
return ['id' => 1, 'name' => 'John Doe'];
}
}
Registering Middleware
Global Middleware
Register middleware that runs on every request:
// Using a closure
$app->middleware(function($req, $handler) {
// Runs on every request
return $handler->handle($req);
});
// Using a class
$app->middleware(new CorsMiddleware());
$app->middleware(new LoggingMiddleware());
// Using class name (will be resolved from container)
$app->middleware(RateLimitMiddleware::class);
Route Middleware
Apply middleware to specific routes:
// Single route
$app->get('/admin', function($req, $res) {
return $res->json(['admin' => true]);
})->middleware('auth');
// Multiple middleware
$app->post('/api/users', function($req, $res) {
// Create user
})->middleware(['auth', 'throttle:5,1']);
// Middleware with parameters
$app->get('/api/data', function($req, $res) {
// Get data
})->middleware('cache:300'); // Cache for 300 seconds
Group Middleware
Apply middleware to route groups:
$app->group('/api', function($group) {
$group->get('/users', [UserController::class, 'index']);
$group->post('/users', [UserController::class, 'store']);
})->middleware(['auth', 'throttle:60,1']);
// Or within the group
$app->group('/admin', function($group) {
$group->middleware(['auth', 'admin']);
$group->get('/dashboard', function($req, $res) {
// Admin dashboard
});
});
Built-in Middleware
PivotPHP includes several built-in middleware classes:
CORS Middleware
use PivotPHP\Middleware\CorsMiddleware;
$app->middleware(new CorsMiddleware([
'allowed_origins' => ['https://example.com'],
'allowed_methods' => ['GET', 'POST', 'PUT', 'DELETE'],
'allowed_headers' => ['Content-Type', 'Authorization'],
'exposed_headers' => ['X-Total-Count'],
'max_age' => 3600,
'credentials' => true
]));
Rate Limiting
use PivotPHP\Middleware\RateLimitMiddleware;
$app->middleware(new RateLimitMiddleware([
'max_attempts' => 60,
'decay_minutes' => 1,
'response_headers' => true // Add X-RateLimit-* headers
]));
// Or use the shorthand
$app->middleware('throttle:60,1');
CSRF Protection
use PivotPHP\Middleware\CsrfMiddleware;
$app->middleware(new CsrfMiddleware([
'except' => ['/webhooks/*'], // Exclude paths
'token_name' => '_token',
'header_name' => 'X-CSRF-TOKEN'
]));
Request Logging
use PivotPHP\Middleware\LoggingMiddleware;
$app->middleware(new LoggingMiddleware([
'logger' => $logger, // PSR-3 logger
'level' => 'info',
'format' => '{method} {uri} {status} {response_time}ms'
]));
Middleware Parameters
Pass parameters to middleware using a colon syntax:
// In route definition
$app->get('/cached', function($req, $res) {
return $res->json(['time' => time()]);
})->middleware('cache:300,public');
// In middleware class
class CacheMiddleware implements MiddlewareInterface
{
private int $duration;
private string $visibility;
public function __construct(int $duration = 60, string $visibility = 'private')
{
$this->duration = $duration;
$this->visibility = $visibility;
}
public function process($request, $handler): ResponseInterface
{
$response = $handler->handle($request);
return $response->withHeader(
'Cache-Control',
"{$this->visibility}, max-age={$this->duration}"
);
}
}
Middleware Priority
Middleware is executed in the order it’s registered:
// Execution order: 1 → 2 → 3 → Route → 3 → 2 → 1
$app->middleware(new Middleware1()); // 1
$app->middleware(new Middleware2()); // 2
$app->middleware(new Middleware3()); // 3
For route-specific middleware:
// Global middleware runs first, then route middleware
$app->middleware(new GlobalMiddleware());
$app->get('/test', function($req, $res) {
// Handler
})->middleware(new RouteMiddleware());
// Order: GlobalMiddleware → RouteMiddleware → Handler
Conditional Middleware
Apply middleware based on conditions:
class ConditionalMiddleware implements MiddlewareInterface
{
public function process($request, $handler): ResponseInterface
{
// Only apply to API routes
if (str_starts_with($request->getUri()->getPath(), '/api/')) {
// Apply middleware logic
$request = $request->withHeader('X-API-Request', 'true');
}
return $handler->handle($request);
}
}
Middleware Best Practices
- Keep middleware focused: Each middleware should have a single responsibility
- Order matters: Register middleware in the correct order
- Early returns: Return early for error conditions to avoid unnecessary processing
- Immutable requests: Always create new request instances instead of modifying
- Use attributes: Store middleware data in request attributes
- Handle exceptions: Catch and handle exceptions appropriately
Example: Custom Auth Middleware
Here’s a complete example of JWT authentication middleware:
namespace App\Middleware;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class JwtAuthMiddleware implements MiddlewareInterface
{
private string $secret;
private array $except = [];
public function __construct(string $secret, array $except = [])
{
$this->secret = $secret;
$this->except = $except;
}
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler
): ResponseInterface {
// Check if route is excluded
$path = $request->getUri()->getPath();
foreach ($this->except as $pattern) {
if (fnmatch($pattern, $path)) {
return $handler->handle($request);
}
}
// Extract token
$token = $this->extractToken($request);
if (!$token) {
return $this->unauthorizedResponse('Token not provided');
}
try {
// Decode token
$decoded = JWT::decode($token, new Key($this->secret, 'HS256'));
// Add user to request
$request = $request->withAttribute('user', $decoded->user);
$request = $request->withAttribute('token', $decoded);
return $handler->handle($request);
} catch (\Exception $e) {
return $this->unauthorizedResponse('Invalid token');
}
}
private function extractToken(ServerRequestInterface $request): ?string
{
$header = $request->getHeaderLine('Authorization');
if (preg_match('/Bearer\s+(.+)/', $header, $matches)) {
return $matches[1];
}
return null;
}
private function unauthorizedResponse(string $message): ResponseInterface
{
return new JsonResponse([
'error' => $message
], 401);
}
}
// Usage
$app->middleware(new JwtAuthMiddleware($_ENV['JWT_SECRET'], [
'/login',
'/register',
'/public/*'
]));