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) (continuing the request through the onion layers), but it can also be a response generated by helper functions like response(), json(), xml(), redirect(), etc. (stopping the request from continuing through the onion layers).

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 - before request processing
  2. Controller processing phase - request processing
  3. Response traversal 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, before request processing.';

        // Continue through the onion layers until a response is obtained from the controller
        $response = $handler($request);

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

        return $response;
    }
}

Example: Authentication Middleware

Create file app/middleware/AuthCheckTest.php (create 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, continue with the request
            return $handler($request);
        }

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

        // Method requires login
        if (!in_array($request->action, $noNeedLogin)) {
            // Intercept the request and return a redirect response, stopping the request from continuing
            return redirect('/user/login');
        }

        // Method does not require login, continue with the request
        return $handler($request);
    }
}

Create a controller app/controller/UserController.php as follows:

<?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 controller that do not require 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, we can focus on writing business code 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 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 with the request 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;
    }
}

Note
Cross-origin requests may trigger an OPTIONS request. We do not want OPTIONS requests to reach the controller, so we directly return an empty response (response('')) to intercept the request.
If your API needs routing, please 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 has custom headers, you need to add the custom headers to the Access-Control-Allow-Headers field in the middleware, otherwise it will report Request header field XXXX is not allowed by Access-Control-Allow-Headers in preflight response.

Explanation

  • Middleware is divided into global middleware, application middleware (only effective in multi-application mode, see Multi-Application), and route middleware.
  • Currently, single controller middleware is not supported (however, you can use $request->controller in the middleware to achieve similar functionality to controller 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,
    ],
    // Application middleware (only effective in multi-application mode)
    'api' => [
        app\middleware\ApiOnly::class,
    ]
];

Route Middleware

We can set middleware for 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

Note
This feature requires webman-framework >= 1.4.8

Starting from version 1.4.8, the configuration file supports instantiating middleware directly or using anonymous functions, making it easy to pass parameters to the middleware through the constructor.
For example, you can also configure the config/middleware.php as follows:

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

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

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

Middleware Execution Order

  • The execution order of middleware is global middleware -> application middleware -> route middleware.
  • When there are multiple global middleware, they are executed in the order specified in the middleware configuration (the same applies to application middleware and route middleware).
  • 404 requests will not trigger any middleware, including global 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
    {
        // $request->route is null by default, so we need to check $request->route for null
        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 generated in the middleware. In this case, we can pass parameters to the controller by adding attributes 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);
    }
}

Middleware Gets Current Route Information

Note
Requires webman-framework >= 1.3.2

We can use $request->route to get the route object and retrieve corresponding information by calling the respective methods.

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
        // Assuming the browser accesses the URL /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);
    }
}

Note
$route->param() method requires webman-framework >= 1.3.16

Middleware Gets Exceptions

Note
Requires webman-framework >= 1.3.15

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

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

Note
This feature requires webman-framework >= 1.5.16

Global middleware in the main project only affects the main project and does not affect application plugins. Sometimes we want to add a middleware that affects all global plugins, including all plugins. In this case, we can use the 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
];

Note
@ super global middleware can be configured not only in the main project but also in a specific plugin. For example, if you configure @ super-global middleware in plugin/ai/config/middleware.php, it will also affect the main project and all plugins.

Adding Middleware to a Plugin

Note
This feature requires webman-framework >= 1.5.16

Sometimes we want to add middleware to a application plugin without modifying the plugin's code (because it will be overwritten during an update). In this 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
];

Note
Of course, similar configurations can be added in a plugin to affect other plugins. For example, if you add the above configuration in plugin/foo/config/middleware.php, it will affect the ai plugin.