Middleware

Introduction

Middleware provides a convenient mechanism for inspecting and filtering HTTP requests entering your application. For example, Slenix includes a middleware that verifies the user of your application is authenticated. If the user is not authenticated, the middleware will redirect the user to your application's login screen. However, if the user is authenticated, the middleware will allow the request to proceed further into the application.

Additional middleware can be written to perform a variety of tasks besides authentication. For example, a CORS middleware might be responsible for adding the proper headers to all responses leaving your application. A rate limiting middleware might restrict the number of requests a given IP address can make within a time window.

Slenix includes several built-in middleware aliases for common tasks such as authentication, CORS, JWT validation, and rate limiting. All of your application's custom middleware is typically stored in the app/Middlewares/ directory.


Defining Middleware

To create a new middleware, use the make:middleware Celestial command:

bash
php celestial make:middleware EnsureTokenIsValid

This command will place a new EnsureTokenIsValidMiddleware class within your app/Middlewares/ directory. In this middleware, we will only allow access to the route if a supplied token matches a known value. Otherwise, we will redirect the user back to the home page:

php
namespace App\Middlewares;

use Slenix\Http\Request;
use Slenix\Http\Response;
use Slenix\Http\Middlewares\Middleware;

class EnsureTokenIsValidMiddleware implements Middleware
{
    public function handle(Request $request, Response $response, callable $next): mixed
    {
        if ($request->input('token') !== 'my-secret-token') {
            return redirect('/');
        }

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

As you can see, if the given token does not match our secret token, the middleware will return an HTTP redirect to the client. Otherwise, the request will be passed further into the application by calling $next($request, $response).

It is best to envision middleware as a series of "layers" that HTTP requests must pass through before they hit your application. Each layer can examine the request and even reject it entirely.

Tip: All middleware classes must implement the Slenix\Http\Middlewares\Middleware interface and define the handle method.

The Middleware Interface

Every middleware in Slenix must implement the following contract:

php
namespace Slenix\Http\Middlewares;

use Slenix\Http\Request;
use Slenix\Http\Response;

interface Middleware
{
    public function handle(Request $request, Response $response, callable $next): mixed;
}

The handle method receives three arguments:

ArgumentTypeDescription
$requestRequestThe incoming HTTP request
$responseResponseThe outgoing HTTP response
$nextcallableThe next middleware or route handler in the pipeline

Before and After Middleware

Whether a middleware runs before or after a request is handled depends on the middleware itself. For example, the following middleware would perform some task before the request is handled by the application:

php
public function handle(Request $request, Response $response, callable $next): mixed
{
    // Perform action before the request is handled
    // e.g. validate a token, check a session, log the request

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

However, this middleware would perform its task after the request is handled by the application:

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

    // Perform action after the request is handled
    // e.g. add a response header, log the duration

    return $result;
}

Registering Middleware

Route Middleware

If you would like to assign middleware to specific routes, you may chain the middleware method when defining the route:

php
use Slenix\Http\Routing\Router;

Router::get('/profile', [ProfileController::class, 'index'])
    ->middleware('auth');

You may assign multiple middleware to a route by passing an array:

php
Router::get('/settings', [SettingsController::class, 'index'])
    ->middleware(['auth', 'throttle']);

You may also use the full class name instead of an alias:

php
Router::get('/profile', [ProfileController::class, 'index'])
    ->middleware(\App\Middlewares\AuthMiddleware::class);

Global Middleware

If you want a middleware to run during every HTTP request to your application, you may register it as a global middleware. Global middleware is prepended to every route automatically, regardless of group or individual route definitions:

php
Router::globalMiddleware([
    \App\Middlewares\CorsMiddleware::class,
    \App\Middlewares\ThrottleMiddleware::class,
]);

Assigning Middleware to Route Groups

You may also assign middleware to a group of routes, so that every route inside the group shares the same middleware stack:

php
Router::group(['middleware' => ['auth', 'verified']], function () {
    Router::get('/dashboard', [DashboardController::class, 'index']);
    Router::get('/settings',  [SettingsController::class, 'index']);
    Router::post('/settings', [SettingsController::class, 'update']);
});

When using Router::middleware, you may assign middleware to a group without a URI prefix:

php
Router::middleware('auth', function () {
    Router::get('/profile',  [ProfileController::class, 'index']);
    Router::get('/billing',  [BillingController::class, 'index']);
});

Middleware Aliases

Slenix resolves short string aliases to their full middleware class names automatically. The following aliases are available out of the box:

AliasClass
authApp\Middlewares\AuthMiddleware
guestApp\Middlewares\GuestMiddleware
corsApp\Middlewares\CorsMiddleware
jwtApp\Middlewares\JwtMiddleware
throttleApp\Middlewares\ThrottleMiddleware

You may always pass the fully qualified class name if you prefer to be explicit:

php
Router::get('/cl-admin', [AdminController::class, 'index'])
    ->middleware(\App\Middlewares\AuthMiddleware::class);

Middleware Pipeline

Slenix executes middleware in reverse declaration order, building a nested pipeline identical to how Laravel handles the middleware stack. The last declared middleware wraps the outermost layer of the execution pipeline, and the first declared middleware is the closest to the route handler.

Given this configuration:

php
Router::get('/dashboard', [DashboardController::class, 'index'])
    ->middleware(['cors', 'throttle', 'auth']);

The execution order is:

plaintext
Request
  → CorsMiddleware::handle()
    → ThrottleMiddleware::handle()
      → AuthMiddleware::handle()
        → DashboardController::index()
      ← AuthMiddleware (after $next)
    ← ThrottleMiddleware (after $next)
  ← CorsMiddleware (after $next)
Response

To halt the pipeline — for example, when a user is not authenticated — simply do not call $next and return or redirect instead:

php
public function handle(Request $request, Response $response, callable $next): mixed
{
    if (!session()->has('user_id')) {
        // Pipeline stops here — $next is never called
        redirect('/login')->with('error', 'Please sign in to continue.');
    }

    return $next($request, $response); // Pipeline continues
}

Practical Examples

Authentication Middleware

The most common middleware pattern in any web application. Protect all routes that require a logged-in user:

php
namespace App\Middlewares;

use Slenix\Http\Request;
use Slenix\Http\Response;
use Slenix\Http\Middlewares\Middleware;

class AuthMiddleware implements Middleware
{
    public function handle(Request $request, Response $response, callable $next): mixed
    {
        if (!session()->has('user_id')) {
            redirect('/login')->with('error', 'Please sign in to continue.');
        }

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

Guest Middleware

Redirect already authenticated users away from pages they should not access, such as the login or registration pages:

php
namespace App\Middlewares;

use Slenix\Http\Request;
use Slenix\Http\Response;
use Slenix\Http\Middlewares\Middleware;

class GuestMiddleware implements Middleware
{
    public function handle(Request $request, Response $response, callable $next): mixed
    {
        if (session()->has('user_id')) {
            redirect('/dashboard');
        }

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

CORS Middleware

Add the appropriate cross-origin headers to every response leaving your application:

php
namespace App\Middlewares;

use Slenix\Http\Request;
use Slenix\Http\Response;
use Slenix\Http\Middlewares\Middleware;

class CorsMiddleware implements Middleware
{
    public function handle(Request $request, Response $response, callable $next): mixed
    {
        header('Access-Control-Allow-Origin: *');
        header('Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS');
        header('Access-Control-Allow-Headers: Content-Type, Authorization, X-CSRF-TOKEN');

        // Respond immediately to preflight OPTIONS requests
        if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
            http_response_code(204);
            exit;
        }

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

JWT Middleware

Validate a Bearer token on every API request:

php
namespace App\Middlewares;

use Slenix\Http\Request;
use Slenix\Http\Response;
use Slenix\Http\Middlewares\Middleware;
use Slenix\Supports\Security\JWT;

class JwtMiddleware implements Middleware
{
    public function handle(Request $request, Response $response, callable $next): mixed
    {
        $token = $this->extractToken();

        if (!$token || !JWT::validate($token)) {
            http_response_code(401);
            header('Content-Type: application/json');
            echo json_encode(['error' => 'Unauthorized — invalid or missing token.']);
            exit;
        }

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

    private function extractToken(): ?string
    {
        $header = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
        if (str_starts_with($header, 'Bearer ')) {
            return substr($header, 7);
        }
        return null;
    }
}

Throttle Middleware

Limit the number of requests from a single IP address within a time window:

php
namespace App\Middlewares;

use Slenix\Http\Request;
use Slenix\Http\Response;
use Slenix\Http\Middlewares\Middleware;

class ThrottleMiddleware implements Middleware
{
    private int $maxRequests;
    private int $windowSeconds;

    public function __construct(int $maxRequests = 100, int $windowSeconds = 60)
    {
        $this->maxRequests   = $maxRequests;
        $this->windowSeconds = $windowSeconds;
    }

    public function handle(Request $request, Response $response, callable $next): mixed
    {
        $ip  = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
        $key = 'throttle_' . md5($ip);

        $count  = (int) session()->get($key, 0);
        $resetAt = (int) session()->get($key . '_reset', 0);
        $now    = time();

        if ($now > $resetAt) {
            $count   = 0;
            $resetAt = $now + $this->windowSeconds;
            session()->put($key . '_reset', $resetAt);
        }

        if ($count >= $this->maxRequests) {
            http_response_code(429);
            header('Retry-After: ' . ($resetAt - $now));
            header('Content-Type: application/json');
            echo json_encode(['error' => 'Too many requests. Please slow down.']);
            exit;
        }

        session()->put($key, $count + 1);

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

Middleware and CSRF Protection

Slenix automatically validates CSRF tokens for all state-mutating requests (POST, PUT, PATCH, DELETE) at the router level — no middleware configuration is required for basic CSRF protection.

If you need to exclude specific routes from CSRF validation (such as webhook endpoints), configure the $csrfExcept array in the Kernel rather than disabling CSRF in a middleware:

php
// src/Core/Kernel.php
private array $csrfExcept = [
    '/api/*',
    '/webhook/stripe',
];

For more information, refer to the CSRF Protection documentation.


Method Reference

Middleware Interface

php
interface Middleware
{
    public function handle(Request $request, Response $response, callable $next): mixed;
}

Registration Methods

MethodWhereDescription
->middleware('alias')RouteAttach middleware to a single route
->middleware(['a', 'b'])RouteAttach multiple middleware to a single route
->middlewares(...)RouteAlias for middleware()
Router::middleware('alias', fn)GroupCreate a middleware-only group
Router::group(['middleware' => [...]], fn)GroupAttach middleware to a route group
Router::globalMiddleware([...])GlobalApply middleware to all routes