Skip to content

Middleware

drawfig edited this page Aug 26, 2026 · 4 revisions

The goal of mouse-php's middleware system is to give developers full power to validate and control incoming requests using clean, reusable classes, without the architectural complexity of traditional middleware stacks. Instead of treating middleware as nested layers wrapped around each other and the controller (the "onion model"), mouse-php treats the middleware layer as a linear pipeline positioned squarely between routing and controller execution.

To understand why we chose a pipeline over an onion architecture, it helps to define how mouse-php views the responsibilities of middleware:

  1. Primary Job (Guarding): Evaluate the incoming request to determine if it should reach the controller layer. If validation fails, terminate the request immediately.

  2. Secondary Job (Enrichment): Inject relevant state (such as authenticated user objects, session claims, or validated schemas) into the $request_data payload for use inside the controller.

  3. Non-Job (Response Wrapping): Middleware should not be responsible for intercepting, modifying, or wrapping outgoing responses.

Middleware Modules

A mouse-php middleware module is a standalone, single-purpose class designed to perform a specific validation or enrichment task.

Unlike traditional middleware frameworks that rely on deeply nested function calls and next() callbacks, a mouse-php module adheres to a strict, simple contract:

  1. Evaluate: Inspect the incoming request state.

  2. Respond: Return a bool or a structured array indicating whether the pipeline should proceed or abort.

  3. Inject (Optional): Pass data back to the middleware engine to automatically enrich the request payload for subsequent modules and the controller.

The Module Contract

We recommend using the squeak CLI to scaffold new middleware modules, first start squeak if you don't have it running:

php squeak

When you have the squeak shell running then run the add-middleware command:

> add-middleware

If you prefer to create modules manually, place the class file in ./core/middleware/modules/ using the following structure:

namespace middleware\modules;

class middleware_template {
    private $DB;

    public function __construct($db, $sqlite) {
        $this->DB = $db;
    }
    
    /**
     * Executes the middleware check.
     * 
     * @param array $route_data   Matched route configuration
     * @param array &$request_data Pass-by-reference request payload
     * @param array $vars         Dynamic URL variables
     * @return bool|array         Boolean OR ["status" => bool, "data" => [...]]
     */

    public function run($route_data, $request_data, $vars) {
        // Simple success check with no injected data
        return true;
    }

}

Return Payload Types

The run() method serves as the entry point executed by the Middleware Engine. The engine expects one of two return types:

Option A: Standard Boolean (bool)

Used when a module only needs to pass or fail a request without injecting state.

return true;  // Pipeline continues
return false; // Pipeline halts immediately, and throws a 400 error

Option B: Structured Response Array (array)

Used when passing contextual data forward on success, or when returning specific error metadata on failure:

// Success with Data Injection
return [
    "status" => true,
    "data"   => [
        "user_id" => 42,
        "role"    => "admin"
    ]
];

// Failure with Status Code & Error Metadata
return [
    "status" => false,
    "data"   => [
        "error"   => 401,
        "message" => "Unauthorized access: Invalid or expired token"
    ]
];

Automatic Request Data Injection (middleware_data)

A key advantage of the linear pipeline is safe, forward-only data enrichment.

When a module returns "status" => true alongside a "data" array, the Middleware Engine automatically injects that data into $request_data['middleware_data'], using the class name of the module as the associative key.

Because $request_data is passed by reference throughout the lifecycle, subsequent middleware modules and the target Controller gain immediate access to this state without re-querying databases or re-parsing headers:

namespace controllers;

class User_Controller
{
public $REQUEST_DATA;

    public function __construct($db, $sqlite, $request_data) 
    {
        $this->REQUEST_DATA = $request_data;
    }

    public function get_profile() 
    {
        // Access user data injected automatically under the module's class key
        $auth_data = $this->REQUEST_DATA['middleware_data']['User_Authentication'];

        echo json_encode([
            'status' => 'success',
            'user'   => $auth_data
        ]);
    }
}

Early Abort Mechanics

If any middleware module returns false or ["status" => false]:

  1. Pipeline Instantly Halts: Remaining middleware modules in the route pipeline are neither instantiated nor executed.

  2. Zero Controller Instantiation: The controller class assigned to the route is never loaded into memory.

Structured Error Emission: If error metadata (e.g., 401 status code and error message) is provided in the return array, it is handed directly to the core error handler to emit the HTTP response before execution terminates immediately.

This guarantees that unauthenticated, rate-limited, or malformed requests consume the absolute minimum CPU cycles and RAM possible.

Middleware Wiring & Configuration

Now that you know how to write a standalone Middleware Module, the next step is wiring it into your application's request pipeline.

mouse-php manages middleware configuration through two central files located in ./core/middleware/:

  • Middleware_Module_Groups.php - Defines which middleware modules execute globally or as named groups.

  • Middleware_Route_Groups.php - Defines which routes run named groups, and which routes bypass global middleware.

This decoupled structure keeps your route definitions (Api_Routes.php) clean while giving you centralized control over security and validation pipelines across your entire API.

Configuration Files Overview

  1. Middleware_Module_Groups.php

Defines lists of module class names as either Global (runs everywhere) or Grouped (runs on specific mapped routes):

<?php
namespace middleware;

class Middleware_Module_Groups 
{
    // Modules that execute on ALL incoming requests (unless bypassed)
    public $GLOBAL_MIDDLEWARE = [
        "Authenticate",
        "Format_Validation",
        "Rate_Limiter"
    ];

    // Named bundles of modules for targeted routes
    public $GROUP_MIDDLEWARE = [
        "admin_auth" => [
            "Session_Check",
            "Admin_Role_Guard"
        ],
        "rate_limited_api" => [
            "Strict_Rate_Limiter"
        ],
    ];
}
  1. Middleware_Route_Groups.php

Maps route URIs to the named module groups defined above, and specifies any routes that should bypass global checks:

<?php
namespace middleware;

class Middleware_Route_Groups 
{
    // Maps route URIs to named group keys in $GROUP_MIDDLEWARE
    public $GROUPS = [
        "admin_auth" => [
            "/api/admin/dashboard",
            "/api/admin/users"
        ],
        "rate_limited_api" => [
            "/api/v1/export"
        ],
    ];

    // Routes that skip the $GLOBAL_MIDDLEWARE stack entirely
    public $GLOBAL_BYPASS_ROUTES = [
        "/api/health",
        "/api/auth/login"
    ];
}

Global Middleware vs. Named Group Middleware

mouse-php provides two ways to attach middleware to request pipelines:

Global Middleware ($GLOBAL_MIDDLEWARE)

Any module listed in $GLOBAL_MIDDLEWARE runs automatically on every incoming request in the exact array order listed.

  • Global Bypassing: Endpoints such as health checks (/api/health) or login routes (/api/auth/login) often don't need token verification or global session checks. Adding a URI to $GLOBAL_BYPASS_ROUTES completely skips the $GLOBAL_MIDDLEWARE stack for that endpoint, guaranteeing zero wasted CPU cycles.

Group Middleware ($GROUP_MIDDLEWARE & $GROUPS)

Group middleware allows you to bundle related modules together under a shared key (e.g., "admin_auth") and assign that bundle to multiple route URIs simultaneously.

┌─────────────────────────────────────────────────────────────┐
│                 LINKING MODULES TO ROUTES                   │
├─────────────────────────────────────────────────────────────┤
│  Middleware_Module_Groups.php                               │
│  $GROUP_MIDDLEWARE['admin_auth'] = ['Session_Check', ...]   │
│                          ▲                                  │
│                          │ Shared Group Key                 │
│                          ▼                                  │
│  Middleware_Route_Groups.php                                │
│  $GROUPS['admin_auth'] = ['/api/admin/dashboard', ...]      │
└─────────────────────────────────────────────────────────────┘

When a request arrives, the Middleware Engine checks if the URI matches any key in $GROUPS. If a match is found, the engine appends those group modules to the route's execution pipeline immediately after global middleware checks pass.

Securing Routes with Middleware

To secure a route with an authentication module, add the route to the security group in Middleware_Route_Groups.php using squeak:

> add-group-route

Then follow prompts to add the route to the security group.

Execution Order & Pipeline Summary

When a route is requested, the Middleware Engine evaluates execution in this exact order:

  1. Bypass Check: Is the URI in $GLOBAL_BYPASS_ROUTES?
  • If Yes: Skip $GLOBAL_MIDDLEWARE entirely.

  • If No: Execute $GLOBAL_MIDDLEWARE modules sequentially.

  1. Group Check: Is the URI mapped to any group in $GROUPS?
  • If Yes: Execute the matching $GROUP_MIDDLEWARE modules sequentially.
  1. Controller Execution: If all middleware modules return true (or valid status arrays), instantiate the controller and execute the handler method.

Clone this wiki locally