Middleware

Middleware provides a mechanism for filtering HTTP requests entering your application. They can inspect, modify, or reject requests before they reach your controllers.

Creating Middleware

Generate a new middleware using Craftsman:

php craftsman make:middleware AuthMiddleware

This creates a file in app/Middleware/AuthMiddleware.php:

<?php

namespace App\Middleware;

use ZephyrPHP\Core\Http\Request;
use ZephyrPHP\Core\Http\Response;

class AuthMiddleware
{
    public function handle(Request $request, callable $next): Response
    {
        // Your middleware logic here

        return $next($request);
    }
}

Middleware Structure

Every middleware must have a handle method that:

  • Receives the current Request
  • Receives a $next callable to pass control to the next middleware/controller
  • Returns a Response

Before vs After Middleware

Before Middleware

Execute logic before the request is handled:

public function handle(Request $request, callable $next): Response
{
    // This runs BEFORE the controller
    if (!$this->isAuthorized($request)) {
        return Response::redirect('/login');
    }

    return $next($request);
}

After Middleware

Execute logic after the response is created:

public function handle(Request $request, callable $next): Response
{
    $response = $next($request);

    // This runs AFTER the controller
    $response->withHeader('X-Custom-Header', 'value');

    return $response;
}

Common Middleware Examples

Authentication Middleware

<?php

namespace App\Middleware;

use ZephyrPHP\Core\Http\Request;
use ZephyrPHP\Core\Http\Response;

class AuthMiddleware
{
    public function handle(Request $request, callable $next): Response
    {
        if (!session('user_id')) {
            // Store intended URL for redirect after login
            session()->set('intended_url', $request->getPath());
            return Response::redirect('/login');
        }

        return $next($request);
    }
}

Guest Middleware

<?php

namespace App\Middleware;

use ZephyrPHP\Core\Http\Request;
use ZephyrPHP\Core\Http\Response;

class GuestMiddleware
{
    public function handle(Request $request, callable $next): Response
    {
        // Redirect authenticated users away from guest-only pages
        if (session('user_id')) {
            return Response::redirect('/dashboard');
        }

        return $next($request);
    }
}

CORS Middleware

<?php

namespace App\Middleware;

use ZephyrPHP\Core\Http\Request;
use ZephyrPHP\Core\Http\Response;

class CorsMiddleware
{
    private array $allowedOrigins = [
        'http://localhost:3000',
        'https://app.example.com',
    ];

    public function handle(Request $request, callable $next): Response
    {
        // Handle preflight OPTIONS request
        if ($request->getMethod() === 'OPTIONS') {
            $response = new Response('', 204);
            return $this->addCorsHeaders($response, $request);
        }

        $response = $next($request);
        return $this->addCorsHeaders($response, $request);
    }

    private function addCorsHeaders(Response $response, Request $request): Response
    {
        $origin = $request->getHeader('Origin');

        if ($origin && in_array($origin, $this->allowedOrigins)) {
            $response->withHeader('Access-Control-Allow-Origin', $origin);
        }

        $response->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
        $response->withHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
        $response->withHeader('Access-Control-Allow-Credentials', 'true');

        return $response;
    }
}

Rate Limiting Middleware

<?php

namespace App\Middleware;

use ZephyrPHP\Core\Http\Request;
use ZephyrPHP\Core\Http\Response;

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

    public function handle(Request $request, callable $next): Response
    {
        $key = 'rate_limit:' . md5($request->getClientIp());
        $data = session()->get($key, ['count' => 0, 'expires' => 0]);

        if ($data['expires'] < time()) {
            $data = ['count' => 0, 'expires' => time() + $this->windowSeconds];
        }

        if ($data['count'] >= $this->maxRequests) {
            return Response::make('Too Many Requests', 429)
                ->withHeader('Retry-After', (string) $this->windowSeconds);
        }

        $data['count']++;
        session()->set($key, $data);

        $response = $next($request);
        $response->withHeader('X-RateLimit-Remaining', (string) ($this->maxRequests - $data['count']));

        return $response;
    }
}

Using Middleware with Routes

Single Route

use App\Middleware\AuthMiddleware;

Route::get('/dashboard', [new DashboardController(), 'index'])
    ->middleware(new AuthMiddleware());

Route Groups

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

Route::group('/admin', function() {
    Route::get('/dashboard', [new AdminController(), 'dashboard']);
    Route::get('/users', [new AdminController(), 'users']);
    Route::post('/users', [new AdminController(), 'createUser']);
})->middleware([
    new AuthMiddleware(),
    new AdminMiddleware(),
]);

Multiple Middleware

Route::get('/api/data', [new ApiController(), 'data'])
    ->middleware([
        new CorsMiddleware(),
        new AuthMiddleware(),
        new RateLimitMiddleware(),
    ]);

Middleware with Parameters

Pass configuration to middleware via the constructor:

<?php

namespace App\Middleware;

class RoleMiddleware
{
    private array $allowedRoles;

    public function __construct(array $roles)
    {
        $this->allowedRoles = $roles;
    }

    public function handle(Request $request, callable $next): Response
    {
        $userRole = session('user_role');

        if (!in_array($userRole, $this->allowedRoles)) {
            abort(403, 'Access denied');
        }

        return $next($request);
    }
}

// Usage
Route::get('/admin', [new AdminController(), 'index'])
    ->middleware(new RoleMiddleware(['admin', 'superadmin']));

Terminating Middleware

Execute code after the response has been sent:

<?php

namespace App\Middleware;

class LoggingMiddleware
{
    public function handle(Request $request, callable $next): Response
    {
        $startTime = microtime(true);

        $response = $next($request);

        // Log after response
        $duration = microtime(true) - $startTime;
        logger()->info('Request completed', [
            'path' => $request->getPath(),
            'method' => $request->getMethod(),
            'duration_ms' => round($duration * 1000, 2),
            'status' => $response->getStatusCode(),
        ]);

        return $response;
    }
}

Best Practices

  • Keep middleware focused - Each middleware should do one thing well
  • Order matters - Middleware runs in the order they're registered
  • Return early - Stop the chain when conditions aren't met
  • Don't modify request data - Let controllers handle business logic
  • Use dependency injection - Pass configuration via constructors

Built-in Middleware Concepts

ZephyrPHP handles some common middleware tasks automatically:

Feature How It's Handled
CSRF Protection Use validateCSRF() in controllers
Security Headers Applied automatically via Headers::apply()
Session Start Started automatically in Application bootstrap
Related

See Routing for applying middleware to routes, Security for built-in security features, and CSRF Protection.