Middleware
Middleware is generally used to intercept requests or responses. For example, it can be used to verify user identity before executing the controller, redirecting users to the login page if they are not logged in, or adding a certain header in the response. It can also be used for statistics, such as counting the proportion of requests for a specific URI.
Middleware Onion Model
┌──────────────────────────────────────────────────────┐
│ middleware1 │
│ ┌──────────────────────────────────────────┐ │
│ │ middleware2 │ │
│ │ ┌──────────────────────────────┐ │ │
│ │ │ middleware3 │ │ │
│ │ │ ┌──────────────────┐ │ │ │
│ │ │ │ │ │ │ │
── Reqeust ───────────────────> Controller ── Response ───────────────────────────> Client
│ │ │ │ │ │ │ │
│ │ │ └──────────────────┘ │ │ │
│ │ │ │ │ │
│ │ └──────────────────────────────┘ │ │
│ │ │ │
│ └──────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────┘
Middleware and controllers form a classic onion model. Middleware is similar to layers of onion skin, while the controller is the core of the onion. As shown in the figure, the request passes through middleware 1, 2, 3 like an arrow to reach the controller. The controller returns a response, which then leaves the middleware in the order of 3, 2, 1 and ultimately returns to the client. This means that we can obtain both the request and the response within each middleware.
Request Interception
Sometimes we do not want a specific request to reach the controller layer. For example, if we find that the current user is not logged in while in middleware2, we can directly intercept the request and return a login response. This process is similar to the following:
┌────────────────────────────────────────────────────────────┐
│ middleware1 │
│ ┌────────────────────────────────────────────────┐ │
│ │ middleware2 │ │
│ │ ┌──────────────────────────────┐ │ │
│ │ │ middleware3 │ │ │
│ │ │ ┌──────────────────┐ │ │ │
│ │ │ │ │ │ │ │
── Reqeust ─────────┐ │ │ Controller │ │ │ │
│ │ Response │ │ │ │ │ │
<───────────────────┘ │ └──────────────────┘ │ │ │
│ │ │ │ │ │
│ │ └──────────────────────────────┘ │ │
│ │ │ │
│ └────────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────┘
As shown in the figure, after the request reaches middleware2, a login response is generated, and the response passes back through middleware1 and returns to the client.
Middleware Interface
Middleware must implement the Webman\MiddlewareInterface
.
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;
}
This means that the process
method must be implemented, and the process
method must return a support\Response
object. By default, this object is generated by $handler($request)
(the request will continue to traverse to the onion core), but it can also be a response generated by helper functions like response()
, json()
, xml()
, redirect()
, etc. (the request stops traversing to the onion core).
Getting Request and Response in Middleware
In middleware, we can obtain the request as well as the response after executing the controller, so the middleware is divided into three parts:
- Request traversal phase, which is the phase before request processing
- Controller processing request phase, which is the request processing phase
- Response exit phase, which is the phase after request processing
These 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, which is before request processing.';
$response = $handler($request); // Continue to traverse to the onion core until the controller executes and obtains the response
echo 'This is the response exit phase, which is after request processing.';
return $response;
}
}
Example: Authentication Middleware
Create the 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, continue to traverse to the onion core
return $handler($request);
}
// Use reflection to get which methods of the controller do not require login
$controller = new ReflectionClass($request->controller);
$noNeedLogin = $controller->getDefaultProperties()['noNeedLogin'] ?? [];
// The accessed method requires login
if (!in_array($request->action, $noNeedLogin)) {
// Intercept the request, return a redirect response, stop the request from traversing to the onion core
return redirect('/user/login');
}
// No need to log in, continue to traverse to the onion core
return $handler($request);
}
}
Create the 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
variable keeps track of the methods in the current controller that can be accessed without logging in.
In config/middleware.php
, add the global middleware as follows:
return [
// Global middleware
'' => [
// ... Other middleware omitted here
app\middleware\AuthCheckTest::class,
]
];
With the authentication middleware, we can focus on writing business logic at the controller level without worrying about whether the user is logged in.
Example: Cross-Origin Request Middleware
Create the 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 the request is an options request, return an empty response; otherwise, continue to traverse to the onion core and get a response
$response = $request->method() == 'OPTIONS' ? response('') : $handler($request);
// Add cross-origin 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 generate OPTIONS requests, and we do not want OPTIONS requests to enter the controller; therefore, we directly return an empty response (response('')
) to achieve request interception.
If your interface needs to set routes, please useRoute::any(..)
orRoute::add(['POST', 'OPTIONS'], ..)
to set it.
In config/middleware.php
, add the global middleware as follows:
return [
// Global middleware
'' => [
// ... Other middleware omitted here
app\middleware\AccessControlTest::class,
]
];
Note
If the AJAX request customizes header fields, it is necessary to add this custom header field to theAccess-Control-Allow-Headers
in the middleware; otherwise, it will reportRequest 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 is only effective in multi-application mode, refer to Multi-application), and route middleware.
- The middleware configuration file is located in
config/middleware.php
. - Global middleware is configured under the key
''
. - Application middleware is configured under specific application names, for example:
return [ // Global middleware '' => [ app\middleware\AuthCheckTest::class, app\middleware\AccessControlTest::class, ], // API application middleware (application middleware is only effective in multi-application mode) 'api' => [ app\middleware\ApiOnly::class, ] ];
Controller Middleware and Method Middleware
Using annotations, we can set middleware for a controller or a specific method within a controller.
<?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 set middleware for a specific 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 Parameter Passing
The configuration file supports directly instantiating middleware or anonymous functions, making it convenient to pass parameters to middleware through constructors.
For example, the configuration in config/middleware.php
can also be as follows:
return [
// Global middleware
'' => [
new app\middleware\AuthCheckTest($param1, $param2, ...),
function(){
return new app\middleware\AccessControlTest($param1, $param2, ...);
},
],
// API application middleware (application middleware is only effective in multi-application mode)
'api' => [
app\middleware\ApiOnly::class,
]
];
Similarly, route middleware can also pass parameters to middleware through constructors. 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, ...);
},
]);
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
- The middleware execution order is
global middleware
->application middleware
->controller middleware
->route middleware
->method middleware
. - When there are multiple middleware at the same level, they are executed in the actual configuration order of that level.
- 404 requests do not trigger any middleware by default (but you can still add middleware using
Route::fallback(function(){})->middleware()
).
Passing Parameters from Route 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 (assumed to be 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 it needs to check if $request->route 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 generated in the middleware; in this case, we can add properties to the $request
object to pass parameters to the controller. 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 Request Route Information in Middleware
We can use $request->route
to get the route object and call the corresponding method to get 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 default route), $request->route will be null
// Suppose the browser accesses address /user/111, it will print the following information
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
Getting Exceptions in Middleware
During business processing, exceptions may occur, and you can use $response->exception()
to get exceptions in middleware.
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 of the main project only affects the main project and does not affect application plugins. Sometimes we want to add a middleware that affects globally, including all plugins, then we can use super global middleware.
Configure the following in config/middleware.php
:
return [
'@' => [ // Add global middleware for the main project and all plugins
app\middleware\MiddlewareGlobl::class,
],
'' => [], // Add global middleware only for the main project
];
Tip
The@
super global middleware can not only be configured in the main project but also in a certain plugin. For example, configuring an@
super global middleware inplugin/ai/config/middleware.php
would also affect the main project and all plugins.
Adding Middleware to a Specific Plugin
Sometimes we want to add a middleware to a specific application plugin without modifying the plugin's code (as upgrades could overwrite it). In this case, we can configure the middleware 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 (the plugin\ai\app\admin directory)
];
Tip
Of course, you can also add similar configurations in another plugin to affect other plugins. For example, adding the above configuration inplugin/foo/config/middleware.php
would impact the ai plugin.