中间件

中间件一般用于拦截请求或者响应。例如执行控制器前统一验证用户身份,如用户未登录时跳转到登录页面,例如响应中增加某个header头。例如统计某个uri请求占比等等。

中间件洋葱模型

  1. ┌──────────────────────────────────────────────────────┐
  2. middleware1
  3. ┌──────────────────────────────────────────┐
  4. middleware2
  5. ┌──────────────────────────────┐
  6. middleware3
  7. ┌──────────────────┐
  8.  ── 请求 ───────────────────────> 控制器 响应 ───────────────────────────> 客户端
  9. └──────────────────┘
  10. └──────────────────────────────┘
  11. └──────────────────────────────────────────┘
  12. └──────────────────────────────────────────────────────┘

中间件和控制器组成了一个经典的洋葱模型,中间件类似一层一层的洋葱表皮,控制器是洋葱芯。如果所示请求像箭一样穿越中间件1、2、3到达控制器,控制器返回了一个响应,然后响应又以3、2、1的顺序穿出中间件最终返回给客户端。也就是说在每个中间件里我们既可以拿到请求,也可以获得响应。

请求拦截

有时候我们不想某个请求到达控制器层,例如我们在某个身份验证中间件发现当前用户并没有登录,则我们可以直接拦截请求并返回一个登录响应。那么这个流程类似下面这样

  1. ┌───────────────────────────────────────────────────────┐
  2. middleware1
  3. ┌───────────────────────────────────────────┐
  4.     身份验证中间件
  5. ┌──────────────────────────────┐
  6. middleware3
  7. ┌──────────────────┐
  8.  ── 请求 ───────────┐ 控制器
  9. 响应 │
  10. <─────────────────┘ └──────────────────┘
  11. └──────────────────────────────┘
  12. └───────────────────────────────────────────┘
  13. └───────────────────────────────────────────────────────┘

如图所示请求到达身份验证中间件后生成了一个登录响应,响应从身份验证中间穿越回中间件1然后返回给浏览器。

中间件接口

中间件必须实现Webman\MiddlewareInterface接口。

  1. interface MiddlewareInterface
  2. {
  3. /**
  4. * Process an incoming server request.
  5. *
  6. * Processes an incoming server request in order to produce a response.
  7. * If unable to produce the response itself, it may delegate to the provided
  8. * request handler to do so.
  9. */
  10. public function process(Request $request, callable $handler): Response;
  11. }

也就是必须实现process方法,process方法必须返回一个support\Response对象,默认这个对象由$handler($request)生成(请求将继续向洋葱芯穿越),也可以可以是response() json() xml() redirect()等助手函数生成的响应(请求停止继续向洋葱芯穿越)。

中间件中获取请求及响应

在中间件中我们可以获得请求,也可以获得执行控制器后的响应,所以中间件内部分为三个部分。

  1. 请求穿越阶段,也就是请求处理前的阶段
  2. 控制器处理请求阶段,也就是请求处理阶段
  3. 响应穿出阶段,也就是请求处理后的阶段

三个阶段在中间件里的体现如下

  1. <?php
  2. namespace app\middleware;
  3. use Webman\MiddlewareInterface;
  4. use Webman\Http\Response;
  5. use Webman\Http\Request;
  6. class Test implements MiddlewareInterface
  7. {
  8. public function process(Request $request, callable $handler) : Response
  9. {
  10. echo '请求穿越阶段,也就是请求处理前';
  11. $response = $handler($request); // 继续向洋葱芯穿越,直至执行控制器得到响应
  12. echo '响应穿出阶段,也就是请求处理后';
  13. return $response;
  14. }
  15. }

示例:身份验证中间件

创建文件app/middleware/AuthCheckTest.php (如目录不存在请自行创建) 如下:

  1. <?php
  2. namespace app\middleware;
  3. use Webman\MiddlewareInterface;
  4. use Webman\Http\Response;
  5. use Webman\Http\Request;
  6. class AuthCheckTest implements MiddlewareInterface
  7. {
  8. public function process(Request $request, callable $handler) : Response
  9. {
  10. $session = $request->session();
  11. // 用户未登录
  12. if (!$session->get('userinfo')) {
  13. // 拦截请求,返回一个重定向响应,请求停止向洋葱芯穿越
  14. return redirect('/user/login');
  15. }
  16. // 请求继续向洋葱芯穿越
  17. return $handler($request);
  18. }
  19. }

config/middleware.php 中添加全局中间件如下:

  1. return [
  2. // 全局中间件
  3. '' => [
  4. // ... 这里省略其它中间件
  5. app\middleware\AuthCheckTest::class,
  6. ]
  7. ];

有了身份验证中间件,我们就可以在控制器层专心的写业务代码,不用就用户是否登录而担心。

示例:跨域请求中间件

创建文件app/middleware/AccessControlTest.php (如目录不存在请自行创建) 如下:

  1. <?php
  2. namespace app\middleware;
  3. use Webman\MiddlewareInterface;
  4. use Webman\Http\Response;
  5. use Webman\Http\Request;
  6. class AccessControlTest implements MiddlewareInterface
  7. {
  8. public function process(Request $request, callable $handler) : Response
  9. {
  10. // 如果是opitons请求则返回一个空的响应,否则继续向洋葱芯穿越,并得到一个响应
  11. $response = $request->method() == 'OPTIONS' ? response('') : $handler($request);
  12. // 给响应添加跨域相关的http头
  13. $response->withHeaders([
  14. 'Access-Control-Allow-Credentials' => 'true',
  15. 'Access-Control-Allow-Origin' => $request->header('Origin', '*'),
  16. 'Access-Control-Allow-Methods' => '*',
  17. 'Access-Control-Allow-Headers' => '*',
  18. ]);
  19. return $response;
  20. }
  21. }

提示 跨域可能会产生OPTIONS请求,我们不想OPTIONS请求进入到控制器,所以我们为OPTIONS请求直接返回了一个空的响应(response(''))实现请求拦截。 如果你的接口需要设置路由,请使用Route::any(..) 或者 Route::add(['POST', 'OPTIONS'], ..)设置。

config/middleware.php 中添加全局中间件如下:

  1. return [
  2. // 全局中间件
  3. '' => [
  4. // ... 这里省略其它中间件
  5. app\middleware\AccessControlTest::class,
  6. ]
  7. ];

注意 如果ajax请求自定义了header头,需要在中间件里 Access-Control-Allow-Headers 字段加入这个自定义header头,否则会报Request header field XXXX is not allowed by Access-Control-Allow-Headers in preflight response.

说明

  • 中间件分为全局中间件、应用中间件(应用中间件仅在多应用模式下有效,参见多应用)、路由中间件
  • 目前不支持单个控制器的中间件(但可以在中间件中通过判断$request->controller来实现类似控制器中间件功能)
  • 中间件配置文件位置在 config/middleware.php
  • 全局中间件配置在key ''
  • 应用中间件配置在具体的应用名下,例如
  1. return [
  2. // 全局中间件
  3. '' => [
  4. app\middleware\AuthCheckTest::class,
  5. app\middleware\AccessControlTest::class,
  6. ],
  7. // api应用中间件(应用中间件仅在多应用模式下有效)
  8. 'api' => [
  9. app\middleware\ApiOnly::class,
  10. ]
  11. ];

路由中间件

注意:需要 workerman/webman-framework 版本 >= 1.0.12

我们可以给某个一个或某一组路由设置中间件。 例如在config/route.php中添加如下配置:

  1. Route::any('/admin', [app\admin\controller\Index::class, 'index'])->middleware([
  2. app\middleware\MiddlewareA::class,
  3. app\middleware\MiddlewareB::class,
  4. ]);
  5. Route::group('/blog', function () {
  6. Route::any('/create', function () {return response('create');});
  7. Route::any('/edit', function () {return response('edit');});
  8. Route::any('/view/{id}', function ($r, $id) {response("view $id");});
  9. })->middleware([
  10. app\middleware\MiddlewareA::class,
  11. app\middleware\MiddlewareB::class,
  12. ]);

中间件执行顺序

  • 中间件执行顺序为全局中间件->应用中间件->路由中间件
  • 有多个全局中间件时,按照中间件实际配置顺序执行(应用中间件、路由中间件同理)。
  • 404请求不会触发任何中间件,包括全局中间件

中间件向控制器传参

有时候控制器需要使用中间件里产生的数据,这时我们可以通过给$request对象添加属性的方式向控制器传参。例如:

中间件:

  1. <?php
  2. namespace app\middleware;
  3. use Webman\MiddlewareInterface;
  4. use Webman\Http\Response;
  5. use Webman\Http\Request;
  6. class Hello implements MiddlewareInterface
  7. {
  8. public function process(Request $request, callable $handler) : Response
  9. {
  10. $request->data = 'some value';
  11. return $handler($request);
  12. }
  13. }

控制器:

  1. <?php
  2. namespace app\controller;
  3. use support\Request;
  4. class Foo
  5. {
  6. public function index(Request $request)
  7. {
  8. return response($request->data);
  9. }
  10. }

中间件获取当前请求路由信息

注意 需要 webman-framework >= 1.3.2

我们可以使用 $request->route 获取路由对象,通过调用对应的方法获取相应信息。

路由配置

  1. <?php
  2. use support\Request;
  3. use Webman\Route;
  4. Route::any('/user/{uid}', [app\controller\User::class, 'view']);

中间件:

  1. <?php
  2. namespace app\middleware;
  3. use Webman\MiddlewareInterface;
  4. use Webman\Http\Response;
  5. use Webman\Http\Request;
  6. class Hello implements MiddlewareInterface
  7. {
  8. public function process(Request $request, callable $handler) : Response
  9. {
  10. $route = $request->route;
  11. // 如果请求没有匹配任何路由(默认路由除外),则 $request->route 为 null
  12. // 假设浏览器访问地址 /user/111,则会打印如下信息
  13. if ($route) {
  14. var_export($route->getPath()); // /user/{uid}
  15. var_export($route->getMethods()); // ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD','OPTIONS']
  16. var_export($route->getName()); // user_view
  17. var_export($route->getMiddleware()); // []
  18. var_export($route->getCallback()); // ['app\\controller\\User', 'view']
  19. var_export($route->param()); // ['uid'=>111]
  20. var_export($route->param('uid')); // 111
  21. }
  22. return $handler($request);
  23. }
  24. }

注意 $route->param()方法需要 webman-framework >= 1.3.16

中间件获取异常

注意 需要 webman-framework >= 1.3.15

业务处理过程中可能会产生异常,在中间件里使用 $response->exception() 获取异常。

路由配置

  1. <?php
  2. use support\Request;
  3. use Webman\Route;
  4. Route::any('/user/{uid}', function (Request $request, $uid) {
  5. throw new \Exception('exception test');
  6. });

中间件:

  1. <?php
  2. namespace app\middleware;
  3. use Webman\MiddlewareInterface;
  4. use Webman\Http\Response;
  5. use Webman\Http\Request;
  6. class Hello implements MiddlewareInterface
  7. {
  8. public function process(Request $request, callable $handler) : Response
  9. {
  10. $response = $handler($request);
  11. $exception = $response->exception();
  12. if ($exception) {
  13. echo $exception->getMessage();
  14. }
  15. return $response;
  16. }
  17. }