Skip to content

Middleware ​

Middleware provide a mechanism to filter and inspect HTTP requests as they enter your application, and responses before they are sent to the client. They are perfect for authentication, logging, CORS handling, and more.

How Middleware Works ​

Middleware wraps around the request/response cycle. Each middleware can:

  • Inspect or modify the incoming request
  • Pass the request to the next handler in the chain
  • Inspect or modify the outgoing response
  • Short-circuit the chain and return a response immediately
Request → Middleware A → Middleware B → Controller → Middleware B → Middleware A → Response

Creating Middleware ​

All middleware must implement the MiddlewareInterface:

php
<?php declare(strict_types=1);

namespace App\Middleware;

use Lalaz\Web\Http\Contracts\MiddlewareInterface;
use Lalaz\Web\Http\Contracts\RequestInterface;
use Lalaz\Web\Http\Contracts\ResponseInterface;

class AuthMiddleware implements MiddlewareInterface
{
    public function handle(
        RequestInterface $request,
        ResponseInterface $response,
        callable $next
    ): mixed {
        // Check authentication
        if (!$request->header('Authorization')) {
            $response->status(401)->json(['error' => 'Unauthorized']);
            return null;
        }

        // Pass to next handler
        return $next($request, $response);
    }
}

The MiddlewareInterface ​

php
interface MiddlewareInterface
{
    public function handle(
        RequestInterface $request,
        ResponseInterface $response,
        callable $next
    ): mixed;
}

Key points:

  • Call $next($request, $response) to pass control to the next handler
  • Return the value from $next() to enable the return-based response pattern
  • Return early (without calling $next) to short-circuit the chain

Registering Middleware ​

Per-Route Middleware ​

Apply middleware to individual routes:

php
use App\Middleware\AuthMiddleware;
use App\Middleware\RateLimitMiddleware;

$router->get('/profile', UserController::class . '@profile', [
    AuthMiddleware::class,
]);

$router->post('/api/data', DataController::class . '@store', [
    AuthMiddleware::class,
    RateLimitMiddleware::class,
]);

Group Middleware ​

Apply middleware to a group of routes:

php
use App\Middleware\AuthMiddleware;
use App\Middleware\AdminMiddleware;

// Using array attributes
$router->group([
    'prefix' => '/admin',
    'middleware' => [AuthMiddleware::class, AdminMiddleware::class]
], function ($router) {
    $router->get('/dashboard', AdminController::class . '@dashboard');
    $router->get('/users', AdminController::class . '@users');
    $router->post('/settings', AdminController::class . '@settings');
});

Fluent Middleware ​

Apply middleware after defining a group:

php
$router->group('/api', function ($router) {
    $router->get('/users', UserController::class . '@index');
    $router->post('/users', UserController::class . '@store');
})->middleware(AuthMiddleware::class)
  ->middleware(RateLimitMiddleware::class);

Or apply multiple at once:

php
$router->group('/api', function ($router) {
    $router->get('/users', UserController::class . '@index');
})->middlewares([
    AuthMiddleware::class,
    RateLimitMiddleware::class,
    CorsMiddleware::class,
]);

Built-in Middleware ​

RequestLoggingMiddleware ​

Logs HTTP requests with timing, memory usage, and status information:

php
use Lalaz\Web\Http\Middlewares\RequestLoggingMiddleware;

$middleware = new RequestLoggingMiddleware(
    excludePaths: ['/health', '/metrics'],  // Paths to skip logging
    logHeaders: false,                       // Include request headers in logs
    logBody: false,                          // Include request body in logs
    slowThreshold: 1000,                     // Milliseconds for slow request warning
);

Log output example:

[db:mysql][write] 12.34ms select SELECT * FROM users WHERE id = ?

Features:

  • Request duration tracking
  • Memory usage tracking
  • Configurable log levels by status code
  • Path exclusion support
  • Header/body logging (debug mode only)

MethodSpoofingMiddleware ​

Allows HTML forms to use HTTP methods other than GET and POST:

php
use Lalaz\Web\Http\Middlewares\MethodSpoofingMiddleware;

// Register globally or per-route
$router->post('/users/{id}', UserController::class . '@update', [
    MethodSpoofingMiddleware::class,
]);

Usage in HTML forms:

html
<form method="POST" action="/users/5">
    <input type="hidden" name="_method" value="DELETE">
    <button>Delete User</button>
</form>

Or using the Twig helper:

twig
<form method="POST" action="/users/5">
    {{ methodField('DELETE') | raw }}
    <button>Delete User</button>
</form>

Supported methods: PUT, PATCH, DELETE

Common Middleware Examples ​

Authentication Middleware ​

php
<?php declare(strict_types=1);

namespace App\Middleware;

use Lalaz\Web\Http\Contracts\MiddlewareInterface;
use Lalaz\Web\Http\Contracts\RequestInterface;
use Lalaz\Web\Http\Contracts\ResponseInterface;

class AuthMiddleware implements MiddlewareInterface
{
    public function handle(
        RequestInterface $request,
        ResponseInterface $response,
        callable $next
    ): mixed {
        $token = $request->header('Authorization');

        if (!$token || !$this->validateToken($token)) {
            return $response->status(401)->json([
                'error' => 'Unauthorized',
                'message' => 'Valid authentication token required',
            ]);
        }

        // Optionally add user info to request
        // $request->setAttribute('user', $this->getUserFromToken($token));

        return $next($request, $response);
    }

    private function validateToken(string $token): bool
    {
        // Your token validation logic
        return str_starts_with($token, 'Bearer ');
    }
}

CORS Middleware ​

php
<?php declare(strict_types=1);

namespace App\Middleware;

use Lalaz\Web\Http\Contracts\MiddlewareInterface;
use Lalaz\Web\Http\Contracts\RequestInterface;
use Lalaz\Web\Http\Contracts\ResponseInterface;

class CorsMiddleware implements MiddlewareInterface
{
    private array $allowedOrigins = ['https://example.com'];
    private array $allowedMethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
    private array $allowedHeaders = ['Content-Type', 'Authorization'];

    public function handle(
        RequestInterface $request,
        ResponseInterface $response,
        callable $next
    ): mixed {
        $origin = $request->header('Origin');

        // Handle preflight requests
        if ($request->method() === 'OPTIONS') {
            return $this->handlePreflight($response, $origin);
        }

        // Process the request
        $result = $next($request, $response);

        // Add CORS headers to response
        if ($this->isAllowedOrigin($origin)) {
            $response->header('Access-Control-Allow-Origin', $origin);
            $response->header('Access-Control-Allow-Credentials', 'true');
        }

        return $result;
    }

    private function handlePreflight(ResponseInterface $response, ?string $origin): mixed
    {
        if ($this->isAllowedOrigin($origin)) {
            $response->header('Access-Control-Allow-Origin', $origin);
            $response->header('Access-Control-Allow-Methods', implode(', ', $this->allowedMethods));
            $response->header('Access-Control-Allow-Headers', implode(', ', $this->allowedHeaders));
            $response->header('Access-Control-Max-Age', '86400');
        }

        return $response->status(204);
    }

    private function isAllowedOrigin(?string $origin): bool
    {
        return $origin !== null && in_array($origin, $this->allowedOrigins, true);
    }
}

Rate Limiting Middleware ​

php
<?php declare(strict_types=1);

namespace App\Middleware;

use Lalaz\Web\Http\Contracts\MiddlewareInterface;
use Lalaz\Web\Http\Contracts\RequestInterface;
use Lalaz\Web\Http\Contracts\ResponseInterface;

class RateLimitMiddleware implements MiddlewareInterface
{
    private int $maxRequests = 60;
    private int $windowSeconds = 60;

    public function handle(
        RequestInterface $request,
        ResponseInterface $response,
        callable $next
    ): mixed {
        $key = $this->resolveKey($request);
        $remaining = $this->getRemainingRequests($key);

        if ($remaining <= 0) {
            return $response->status(429)->json([
                'error' => 'Too Many Requests',
                'message' => 'Rate limit exceeded. Try again later.',
            ]);
        }

        $this->incrementCounter($key);

        // Add rate limit headers
        $response->header('X-RateLimit-Limit', (string) $this->maxRequests);
        $response->header('X-RateLimit-Remaining', (string) ($remaining - 1));

        return $next($request, $response);
    }

    private function resolveKey(RequestInterface $request): string
    {
        // Use IP or authenticated user ID
        return 'rate_limit:' . ($request->ip() ?? 'unknown');
    }

    private function getRemainingRequests(string $key): int
    {
        // Implement using cache or database
        return $this->maxRequests;
    }

    private function incrementCounter(string $key): void
    {
        // Implement counter logic
    }
}

Maintenance Mode Middleware ​

php
<?php declare(strict_types=1);

namespace App\Middleware;

use Lalaz\Web\Http\Contracts\MiddlewareInterface;
use Lalaz\Web\Http\Contracts\RequestInterface;
use Lalaz\Web\Http\Contracts\ResponseInterface;

class MaintenanceMiddleware implements MiddlewareInterface
{
    private array $allowedIps = ['127.0.0.1'];

    public function handle(
        RequestInterface $request,
        ResponseInterface $response,
        callable $next
    ): mixed {
        if ($this->isInMaintenance() && !$this->isAllowed($request)) {
            return $response->status(503)->json([
                'error' => 'Service Unavailable',
                'message' => 'The application is under maintenance.',
            ]);
        }

        return $next($request, $response);
    }

    private function isInMaintenance(): bool
    {
        return env('APP_MAINTENANCE', false);
    }

    private function isAllowed(RequestInterface $request): bool
    {
        return in_array($request->ip(), $this->allowedIps, true);
    }
}

Middleware Execution Order ​

Middleware execute in the order they are registered. For groups:

php
$router->group([
    'prefix' => '/api',
    'middleware' => [LoggingMiddleware::class, AuthMiddleware::class]
], function ($router) {
    $router->get('/users', UserController::class . '@index', [
        CacheMiddleware::class,
    ]);
});

Execution order for GET /api/users:

  1. LoggingMiddleware (group)
  2. AuthMiddleware (group)
  3. CacheMiddleware (route)
  4. Controller action
  5. CacheMiddleware (response)
  6. AuthMiddleware (response)
  7. LoggingMiddleware (response)

Middleware Best Practices ​

Keep Middleware Focused ​

Each middleware should do one thing well:

php
// ✅ Good: Single responsibility
class AuthMiddleware implements MiddlewareInterface { /* auth only */ }
class LoggingMiddleware implements MiddlewareInterface { /* logging only */ }

// ❌ Bad: Multiple responsibilities
class DoEverythingMiddleware implements MiddlewareInterface {
    public function handle($req, $res, $next): mixed
    {
        // Auth + logging + rate limiting + CORS...
    }
}

Use Dependency Injection ​

Inject dependencies through the constructor:

php
class AuthMiddleware implements MiddlewareInterface
{
    public function __construct(
        private TokenValidator $validator,
        private UserRepository $users,
    ) {}

    public function handle($req, $res, $next): mixed
    {
        $user = $this->validator->validate($req->header('Authorization'));
        // ...
    }
}

Handle Exceptions Gracefully ​

Wrap potentially failing operations:

php
public function handle($req, $res, $next): mixed
{
    try {
        // Your middleware logic
        return $next($req, $res);
    } catch (AuthenticationException $e) {
        return $res->status(401)->json(['error' => $e->getMessage()]);
    } catch (\Throwable $e) {
        // Log and re-throw or handle gracefully
        throw $e;
    }
}

Use Route Attributes ​

For controller-level middleware, use the #[Route] attribute:

php
use Lalaz\Web\Routing\Attribute\Route;

class AdminController
{
    #[Route(path: '/admin/dashboard', method: 'GET', middlewares: [AdminMiddleware::class])]
    public function dashboard(): array
    {
        return ['admin' => 'dashboard'];
    }
}

Released under the MIT License.