Swoft2 框架精华教程:Middlewares 优先级

详细说明

swoft2 中的中间件相当于 java 的请求拦截器,用来拦截服务器的每个请求,所以拦截器的顺序很重要,因为有些校验必须按照一定的顺序才有意义。比如,要先验证用户是否登录,然后在验证接口是否有访问权限。如果反过来,未验证登录的时候,就不存在验证接口权限一说。所以,通过以下分析,希望能够加强对swoft 中间件(拦截器)的理解和使用。

中间件的主要顺序

配置在 bean.php 文件中的全局的中间件,有三种类型:

  • preMiddlewares
  • middlewares
  • afterMiddlewares

这三个中间件执行的的顺序为 preMiddlewares => middlewares => afterMiddlewares。

通过 MiddlewareMiddlewares配置的自定义的中间件,是通过 afterMiddleware()方法。此方法中默认有一个 UserMiddlewares::class,此中间件用来收集注解标注的中间件类,将其动态的插入到已经合并后的 middlewares 属性中,插入的位置就是下一个要执行的中间件(即 UserMiddleware::class后)。

举例说明:

假如用户请求 /user/login 接口,UserController 上有个Middleware 注解:X::class,那么此中间件执行位置如下:

php 复制
// bean.php 中配置的中间件如下:
return [
    'httpDispatcher'     => [
        'preMiddlewares' => [
            A::class
        ],
        // Add global http middleware
        'middlewares'      => [
            B::class,
        ],
        'afterMiddlewares' => [
            // UserMiddleware::class 此处相当于有此中间件
            // 实际如果访问路径有中间价注解,会动态放到此位置执行 <=
            C::class
        ]
    ],
];
// requestMiddlewares: [A::class, B::class, UserMiddleware::class, C::class]
// 每次请求时候,会将 requestMiddlewares 重新覆盖 middlewares,达到动态中间件的结果.

先执行 A::class 对应的实例,然后执行 B::class 的实例,然后是 UserMiddleware::class 的实例。 执行 UserMiddleware::class 实例,本质上,是将动态收集到当前执行方法上的中间件注解类(X::class),插到UserMiddleware::class 的位置之后。这样此时会执行 X::class 的实例,最后,才执行 C::class 的实例。由于每次请求进来后,会通过 initialize()方法,将 requestMiddlewares 重新放置于 middlewares 之中,这就会造成每个请求会动态的加载注解标注的中间件的效果。

注意:UserMiddleware::class 中间件,会根据当前访问的路径,判断是否有动态的中间件注解,没有注解就不会动态加,这样可以提高执行的效率。

源码依据

src/HttpDispatcher.php

php 复制
/**
 * Init 初始化中间件代码
 * 注意:array_merge 合并两个数字索引数组,会按照两个数组的顺序进行合并(参数靠前的最终靠前)
 */
public function init(): void
{
    // 前置中间件,优先合并通过 pre 方法的中间件,然后是通过 preMiddlewares 属性配置的中间件
    $this->preMiddlewares     = array_merge($this->preMiddleware(), $this->preMiddlewares);
    // 后置中间件,同样合并时候通过方法获取到的优先级要高于通过属性配置进去的(Bean定义方式)
    $this->afterMiddlewares   = array_merge($this->afterMiddleware(), $this->afterMiddlewares);
    // 最后请求通过的所有中间件,先是 pre 然后是 middlewares,然后是 after
    $this->requestMiddlewares = array_merge($this->preMiddlewares, $this->middlewares, $this->afterMiddlewares);
}
/**
 * Dispatch http request
 *
 * @param mixed ...$params
 */
public function dispatch(...$params): void
{
    /**
     * @var Request  $request
     * @var Response $response
     */
    [$request, $response] = $params;

    /* @var RequestHandler $requestHandler */
    $requestHandler = Swoft::getBean(RequestHandler::class);

    try {
        // 注意这一句,会将中间件重置
        $requestHandler->initialize($this->requestMiddlewares, $this->defaultMiddleware);

        // Before request
        $this->beforeRequest($request, $response);

        // Trigger before handle event
        Swoft::trigger(HttpServerEvent::BEFORE_REQUEST, null, $request, $response);

        // Match router and handle
        $request  = $this->matchRouter($request);
        $response = $requestHandler->handle($request);
    } catch (Throwable $e) {
        /** @var HttpErrorDispatcher $errDispatcher */
        $errDispatcher = Swoft::getSingleton(HttpErrorDispatcher::class);

        // Handle request error
        $response = $errDispatcher->run($e, $response);
    }

    try {
        // Format response content type
        $response = $this->acceptFormatter->format($response);

        // Trigger after request
        Swoft::trigger(HttpServerEvent::AFTER_REQUEST, null, $response);

        // After request
        $this->afterRequest($response);
    } catch (Throwable $e) {
        Error::log('response error=%s(%d) at %s:%d', $e->getMessage(), $e->getCode(), $e->getFile(), $e->getLine());
    }
}
/**
 * @return array
 */
public function afterMiddleware(): array
{
    return [
        UserMiddleware::class
    ];
}

src/Middleware/UserMiddleware.php

此中间件的执行过程,会校验当前访问的路径上是否有通过注解标注的中间件(拦截器),如果有,则会自动加入到 UserMiddleware::class之后。

php 复制
class UserMiddleware implements MiddlewareInterface
{
    /**
     * @param ServerRequestInterface  $request
     * @param RequestHandlerInterface $handler
     *
     * @return ResponseInterface
     * @throws HttpServerException
     */
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        // Route data
        $routeData = $request->getAttribute(Request::ROUTER_ATTRIBUTE);

        /* @var Route $route */
        [$status, , $route] = $routeData;

        if ($status !== Router::FOUND) {
            return $handler->handle($request);
        }

        // Controller and method
        $handlerId = $route->getHandler();
        [$className, $method] = explode('@', $handlerId);

        $middlewares = MiddlewareRegister::getMiddlewares($className, $method);
        if (!empty($middlewares) && $handler instanceof RequestHandler) {
            $handler->insertMiddlewares($middlewares);
        }

        return $handler->handle($request);
    }
}