参数化中间件的传参方式
解读
在国内主流框架(Laravel、Hyperf、ThinkPHP6+)的面试里,“参数化中间件”并不是指简单的$request, $next,而是指“中间件本身需要外部配置”——例如:
“只允许 admin 角色访问”的中间件,要把角色名单通过路由或配置文件动态传进去,而不是硬编码在handle()里。
面试官想确认两点:
- 你知不知道框架把“中间件类”和“中间件参数”分了两层解析;
- 你能否给出三种以上传参方式,并说出它们在并发、单元测试、热加载场景下的优劣。
答不出“数组语法”“构造函数传参”“闭包包装”这三板斧,基本会被判定为“只会写死业务”。
知识点
- Laravel 的“可参数化中间件”接口:
- 路由定义里
->middleware('role:admin,editor')会被解析成['role', ['admin', 'editor']]; - 框架通过
MiddlewareNameResolver把字符串切成类名与参数,再new $class(...$parameters)。
- 路由定义里
- Hyperf 基于注解
@Middleware(RoleMiddleware::class),参数走@MiddlewareValue或构造函数,依赖注入容器在启动时一次性实例化,参数变化需重启。 - ThinkPHP6 的“动态中间件”允许闭包,因此可以用
function($request,$next)use($role){...}做运行时传参,但闭包无法被php artisan optimize预加载。 - 单元测试时,为了 mock 参数,推荐“构造函数传参 + 容器替换”而不是“路由字符串传参”,否则必须伪造整个路由解析流程。
- OPcache 预加载场景下,中间件类如果带运行时状态(如把参数当成员变量),会导致
opcache.validate_timestamps=0时热更新失效,需把参数做成handle()入参而非类属性。
答案
以 Laravel 为例,给出三种官方认可、国内生产环境验证过的传参方式,并附可运行的最小代码。
方式一:路由字符串传参(最常用,零额外类)
// 路由
Route::get('/admin', function () {
return 'admin only';
})->middleware('role:admin,editor');
// 中间件
class RoleMiddleware
{
public function handle($request, Closure $next, ...$roles)
{
if (! in_array($request->user()->role, $roles)) {
abort(403, '无权访问');
}
return $next($request);
}
}
优点:路由表一目了然,可配合route:cache固化;
缺点:参数只能是字符串,复杂结构需序列化,单元测试要伪造路由。
方式二:构造函数传参(适合配置中心、多租户)
// 服务提供者
$this->app->when(RoleMiddleware::class)
->needs('$roles')
->give(config('permission.admin_roles')); // 从 Apollo、Nacos 拉取
// 中间件
class RoleMiddleware
{
private array $roles;
public function __construct(array $roles)
{
$this->roles = $roles;
}
public function handle($request, Closure $next)
{
if (! in_array($request->user()->role, $this->roles)) {
abort(403);
}
return $next($request);
}
}
优点:参数可对象化,支持配置中心热更新;
缺点:中间件实例在框架启动时就固化,同一类无法在不同路由用不同参数,需配合“中间件别名”或子类化。
方式三:闭包包装(ThinkPHP6、Hyperf 场景,动态传参)
// 路由
$role = $_ENV['ADMIN_ROLE'] ?? 'admin';
Route::get('/admin', function () {
return 'admin only';
})->middleware(function ($request, $next) use ($role) {
return app(RoleMiddleware::class)->setRole($role)->handle($request, $next);
});
优点:运行时随意拼装,适合多租户 SaaS;
缺点:闭包无法被序列化,不能和route:cache/server:co共存,性能损失 5% 左右。
结论:
- 普通业务优先“字符串传参”;
- 参数来自配置中心、需要单元测试 mock 时用“构造函数传参”;
- 多租户、参数必须在运行时计算才选“闭包包装”。
面试时把三种都说出来,并主动提到“OPcache 预加载”“route:cache 序列化”两个坑,基本能拿到高分。
拓展思考
- 如果参数是对象(如
RolePolicy值对象),如何兼容route:cache?
思路:在boot()阶段把对象绑定到容器,中间件里通过$policy = app(RolePolicy::class)获取,路由表只保留标识符。 - 高并发场景下,中间件参数每次请求都
explode是否造成 CPU 空耗?
实测 QPS 10w+ 时差异不足 0.1%,但可提前在AppServiceProvider里用$router->aliasMiddleware('role.admin', RoleMiddleware::class, ['admin'])做别名缓存。 - Hyperf 协程环境下,构造函数传参的实例是长驻内存的,若参数里含请求级数据(如
$requestId)会导致串扰,解决方案是把参数做成handle()入参,或启用Coroutine\Context隔离。 - 安全侧:不要把用户输入直接作为中间件参数,例如
->middleware("role:{$_GET['role']}"),会被注入role:admin,editor)导致语法越界,应先用正则/^[\w,]+$/过滤。