Middleware

Middleware is generally used for intercepting requests or responses. For example, validating user identity before executing the controller, redirecting to the login page if the user is not logged in, or adding a header to the response. It can also be used for tracking the percentage of requests for a specific URI, and much more.

The Onion Model of Middleware


            ┌──────────────────────────────────────────────────────┐
            │                     middleware1                      │ 
            │     ┌──────────────────────────────────────────┐     │
            │     │               middleware2                │     │
            │     │     ┌──────────────────────────────┐     │     │
            │     │     │         middleware3          │     │     │        
            │     │     │     ┌──────────────────┐     │     │     │
            │     │     │     │                  │     │     │     │
   ── Request ───────────────────> Controller ── Response ───────────────────────────> Client
            │     │     │     │                  │     │     │     │
            │     │     │     └──────────────────┘     │     │     │
            │     │     │                              │     │     │
            │     │     └──────────────────────────────┘     │     │
            │     │                                          │     │
            │     └──────────────────────────────────────────┘     │
            │                                                      │
            └──────────────────────────────────────────────────────┘

Middleware and controllers form a classic onion model, where middleware acts as layers of onion skin, and the controller is the core of the onion. As shown in the diagram, the request passes through middleware 1, 2, and 3 before reaching the controller. The controller returns a response, which then passes back through the middleware in reverse order (3, 2, 1) and ultimately returns to the client. This means that within each middleware, we can access both the request and the response.

Request Interception

Sometimes, we don't want a particular request to reach the controller layer. For example, if we find in middleware 2 that the current user is not logged in, we can intercept the request and return a login response. The flow would look something like this:


            ┌────────────────────────────────────────────────────────────┐
            │                         middleware1                        │ 
            │     ┌────────────────────────────────────────────────┐     │
            │     │                   middleware2                  │     │
            │     │          ┌──────────────────────────────┐      │     │
            │     │          │        middleware3           │      │     │       
            │     │          │    ┌──────────────────┐      │      │     │
            │     │          │    │                  │      │      │     │
   ── Request ─────────┐     │    │    Controller    │      │      │     │
            │     │ Response │    │                  │      │      │     │
   <───────────────────┘     │    └──────────────────┘      │      │     │
            │     │          │                              │      │     │
            │     │          └──────────────────────────────┘      │     │
            │     │                                                │     │
            │     └────────────────────────────────────────────────┘     │
            │                                                            │
            └────────────────────────────────────────────────────────────┘

As shown in the diagram, the request reaches middleware 2 and generates a login response, which then passes back through middleware 1 and returns to the client.

Middleware Interface

Middleware must implement the Webman\MiddlewareInterface interface.

interface MiddlewareInterface
{
    /**
     * Process an incoming server request.
     *
     * Processes an incoming server request in order to produce a response.
     * If unable to produce the response itself, it may delegate to the provided
     * request handler to do so.
     */
    public function process(Request $request, callable $handler): Response;
}

In other words, middleware must implement the process method, which must return a support\Response object. By default, this object is generated by $handler($request) (the request continues toward the onion core), but it can also be a response generated by helper functions like response(), json(), xml(), redirect(), etc. (in which case the request stops before reaching the onion core).

Accessing the Request and Response in Middleware

In middleware, we can access both the request and the response. Therefore, a middleware can be divided into three parts:

  1. Request traversal phase — the phase before request processing
  2. Controller processing phase — the phase during request processing
  3. Response traversal phase — the phase after request processing

The three phases are reflected in the middleware as follows:

<?php
namespace app\middleware;

use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;

class Test implements MiddlewareInterface
{
    public function process(Request $request, callable $handler) : Response
    {
        echo 'This is the request traversal phase, i.e. before request processing.';

        $response = $handler($request); // Continue toward the onion core until the controller returns a response

        echo 'This is the response traversal phase, i.e. after request processing.';

        return $response;
    }
}

Example: Authentication Middleware

Create file app/middleware/AuthCheckTest.php (create the directory if it does not exist) as follows:

<?php
namespace app\middleware;

use ReflectionClass;
use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;

class AuthCheckTest implements MiddlewareInterface
{
    public function process(Request $request, callable $handler) : Response
    {
        if (session('user')) {
            // Already logged in, request continues toward the onion core
            return $handler($request);
        }

        // Use reflection to get controller methods that do not require login
        $controller = new ReflectionClass($request->controller);
        $noNeedLogin = $controller->getDefaultProperties()['noNeedLogin'] ?? [];

        // The method being accessed requires login
        if (!in_array($request->action, $noNeedLogin)) {
            // Intercept the request, return a redirect response, stop the request from reaching the onion core
            return redirect('/user/login');
        }

        // Login not required, request continues toward the onion core
        return $handler($request);
    }
}

Create controller app/controller/UserController.php:

<?php
namespace app\controller;
use support\Request;

class UserController
{
    /**
     * Methods that do not require login
     */
    protected $noNeedLogin = ['login'];

    public function login(Request $request)
    {
        $request->session()->set('user', ['id' => 10, 'name' => 'webman']);
        return json(['code' => 0, 'msg' => 'login ok']);
    }

    public function info()
    {
        return json(['code' => 0, 'msg' => 'ok', 'data' => session('user')]);
    }
}

Note
The $noNeedLogin array stores the methods in the current controller that can be accessed without login.

Add the global middleware in config/middleware.php as follows:

return [
    // Global middleware
    '' => [
        // ... other middleware omitted
        app\middleware\AuthCheckTest::class,
    ]
];

With the authentication middleware in place, we can focus on writing business logic in the controller layer without worrying about whether the user is logged in.

Example: Cross-Origin Request Middleware

Create file app/middleware/AccessControlTest.php (create the directory if it does not exist) as follows:

<?php
namespace app\middleware;

use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;

class AccessControlTest implements MiddlewareInterface
{
    public function process(Request $request, callable $handler) : Response
    {
        // If it is an OPTIONS request, return an empty response; otherwise continue toward the onion core and get a response
        $response = $request->method() == 'OPTIONS' ? response('') : $handler($request);

        // Add CORS-related HTTP headers to the response
        $response->withHeaders([
            'Access-Control-Allow-Credentials' => 'true',
            'Access-Control-Allow-Origin' => $request->header('origin', '*'),
            'Access-Control-Allow-Methods' => $request->header('access-control-request-method', '*'),
            'Access-Control-Allow-Headers' => $request->header('access-control-request-headers', '*'),
        ]);

        return $response;
    }
}

Tip
Cross-origin requests may trigger OPTIONS preflight requests. We do not want OPTIONS requests to reach the controller, so we return an empty response (response('')) directly to intercept them.
If your API needs routing, use Route::any(..) or Route::add(['POST', 'OPTIONS'], ..) to configure the route.

Add the global middleware in config/middleware.php as follows:

return [
    // Global middleware
    '' => [
        // ... other middleware omitted
        app\middleware\AccessControlTest::class,
    ]
];

Note
If the AJAX request uses custom headers, add those custom headers to the Access-Control-Allow-Headers field in the middleware; otherwise you may see Request header field XXXX is not allowed by Access-Control-Allow-Headers in preflight response.

Explanation

  • Middleware is divided into global middleware, application middleware (application middleware only takes effect in multi-application mode, see Multi-Application), and route middleware
  • The middleware configuration file is located at config/middleware.php
  • Global middleware is configured under the key ''
  • Application middleware is configured under the specific application name, for example:
return [
    // Global middleware
    '' => [
        app\middleware\AuthCheckTest::class,
        app\middleware\AccessControlTest::class,
    ],
    // API application middleware (application middleware only takes effect in multi-application mode)
    'api' => [
        app\middleware\ApiOnly::class,
    ]
];

Controller Middleware and Method Middleware

Using annotations, we can assign middleware to a controller or to specific controller methods.

<?php
namespace app\controller;
use app\middleware\Controller1Middleware;
use app\middleware\Controller2Middleware;
use app\middleware\Method1Middleware;
use app\middleware\Method2Middleware;
use support\annotation\Middleware;
use support\Request;

#[Middleware(Controller1Middleware::class, Controller2Middleware::class)]
class IndexController
{
    #[Middleware(Method1Middleware::class, Method2Middleware::class)]
    public function index(Request $request): string
    {
        return 'hello';
    }
}

Route Middleware

We can assign middleware to a single route or a group of routes.

For example, add the following configuration in config/route.php:

<?php
use support\Request;
use Webman\Route;

Route::any('/admin', [app\admin\controller\IndexController::class, 'index'])->middleware([
    app\middleware\MiddlewareA::class,
    app\middleware\MiddlewareB::class,
]);

Route::group('/blog', function () {
   Route::any('/create', function () {return response('create');});
   Route::any('/edit', function () {return response('edit');});
   Route::any('/view/{id}', function ($r, $id) {response("view $id");});
})->middleware([
    app\middleware\MiddlewareA::class,
    app\middleware\MiddlewareB::class,
]);

Middleware Constructor Parameters

The configuration file supports direct instantiation of middleware, making it easy to pass parameters via the constructor.

For example, you can also configure config/middleware.php as follows:

return [
    // Global middleware
    '' => [
        new app\middleware\AuthCheckTest($param1, $param2, ...),
        new app\middleware\AccessControlTest($param1, $param2, ...)
    ],
    // API application middleware (application middleware only takes effect in multi-application mode)
    'api' => [
        app\middleware\ApiOnly::class,
    ]
];

Similarly, route middleware can pass parameters via the constructor. For example, in config/route.php:

Route::any('/admin', [app\admin\controller\IndexController::class, 'index'])->middleware([
    new app\middleware\MiddlewareA($param1, $param2, ...),
    new app\middleware\MiddlewareB($param1, $param2, ...),
]);

Example of using parameters in middleware:

<?php
namespace app\middleware;

use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;

class MiddlewareA implements MiddlewareInterface
{
    protected $param1;

    protected $param2;

    public function __construct($param1, $param2)
    {
        $this->param1 = $param1;
        $this->param2 = $param2;
    }

    public function process(Request $request, callable $handler) : Response
    {
        var_dump($this->param1, $this->param2);
        return $handler($request);
    }
}

Middleware Execution Order

  • Middleware runs in the order: global middleware -> application middleware -> controller middleware -> route middleware -> method middleware
  • When there are multiple middleware at the same level, they run in the order in which they are configured
  • 404 requests do not trigger any middleware by default (you can still add middleware via Route::fallback(function(){})->middleware())

Passing Parameters from Routes to Middleware (route->setParams)

Route configuration config/route.php

<?php
use support\Request;
use Webman\Route;

Route::any('/test', [app\controller\IndexController::class, 'index'])->setParams(['some_key' =>'some value']);

Middleware (assuming it is global middleware)

<?php
namespace app\middleware;

use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;

class Hello implements MiddlewareInterface
{
    public function process(Request $request, callable $handler) : Response
    {
        // By default $request->route is null, so we need to check if it is empty
        if ($route = $request->route) {
            $value = $route->param('some_key');
            var_export($value);
        }
        return $handler($request);
    }
}

Passing Parameters from Middleware to Controller

Sometimes the controller needs to use data produced in the middleware. In that case, we can pass parameters to the controller by adding properties to the $request object. For example:

Middleware

<?php
namespace app\middleware;

use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;

class Hello implements MiddlewareInterface
{
    public function process(Request $request, callable $handler) : Response
    {
        $request->data = 'some value';
        return $handler($request);
    }
}

Controller

<?php
namespace app\controller;

use support\Request;

class FooController
{
    public function index(Request $request)
    {
        return response($request->data);
    }
}

Getting Current Route Information in Middleware

We can use $request->route to get the route object and call its methods to retrieve the relevant information.

Route configuration

<?php
use support\Request;
use Webman\Route;

Route::any('/user/{uid}', [app\controller\UserController::class, 'view']);

Middleware

<?php
namespace app\middleware;

use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;

class Hello implements MiddlewareInterface
{
    public function process(Request $request, callable $handler) : Response
    {
        $route = $request->route;
        // If the request does not match any route (except the default route), $request->route will be null
        // If the browser visits /user/111, the following information will be printed
        if ($route) {
            var_export($route->getPath());       // /user/{uid}
            var_export($route->getMethods());    // ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD','OPTIONS']
            var_export($route->getName());       // user_view
            var_export($route->getMiddleware()); // []
            var_export($route->getCallback());   // ['app\\controller\\UserController', 'view']
            var_export($route->param());         // ['uid'=>111]
            var_export($route->param('uid'));    // 111 
        }
        return $handler($request);
    }
}

Getting Exceptions in Middleware

Exceptions may occur during business processing. Use $response->exception() in the middleware to retrieve them.

Route configuration

<?php
use support\Request;
use Webman\Route;

Route::any('/user/{uid}', function (Request $request, $uid) {
    throw new \Exception('exception test');
});

Middleware

<?php
namespace app\middleware;

use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;

class Hello implements MiddlewareInterface
{
    public function process(Request $request, callable $handler) : Response
    {
        $response = $handler($request);
        $exception = $response->exception();
        if ($exception) {
            echo $exception->getMessage();
        }
        return $response;
    }
}

Super Global Middleware

Global middleware in the main project only affects the main project and does not affect application plugins. If you want middleware that affects everything, including all plugins, use super global middleware.

Configure the following in config/middleware.php:

return [
    '@' => [ // Add global middleware to the main project and all plugins
        app\middleware\MiddlewareGlobl::class,
    ], 
    '' => [], // Add global middleware to the main project only
];

Tip
The @ super global middleware can be configured not only in the main project but also in a plugin. For example, configuring @ super global middleware in plugin/ai/config/middleware.php will also affect the main project and all plugins.

Adding Middleware to a Plugin

Sometimes we want to add middleware to an application plugin without modifying the plugin code (which would be overwritten on update). In that case, we can configure middleware for it in the main project.

Configure the following in config/middleware.php:

return [
    'plugin.ai' => [], // Add middleware to the ai plugin
    'plugin.ai.admin' => [], // Add middleware to the admin module of the ai plugin (plugin\ai\app\admin directory)
];

Tip
You can also add similar configuration in a plugin to affect other plugins. For example, adding the above configuration in plugin/foo/config/middleware.php will affect the ai plugin.