Web 程序,简明扼要地说,就是个吃网络请求,并吐出响应的东西。 MVC 结构的框架,使用控制器来处理请求,并生成响应。 而 Laravel 框架,还使用流水线(Pipeline)来处理 传入控制器前的请求,和控制器返回的响应。 流水线上的工人,是一个个中间件(Middleware)。 它们就像乐高积木,负责单一的行为。 或负责控制 Cookie,管理 Session; 或负责控制用户的登录验证;又或者控制访问的频率。 通过灵活的中间件注册机制,我们可方便地对这些中间件组装和复用。

运行机制

   如果我们马上打开 Pipeline.php 一定会一头雾水,满脑子退堂鼓声。 所以我们得先抛开具体的实现,从一个典型的中间件上,窥一窥流水线的运行机制。

class MyMiddleware
{
	public function handle( \Illuminate\Http\Request$request, callable$next )
	:\Illuminate\Http\Request
	{
		// do something to $request
		
		$response= $next( $request );
		
		// do something to $response
		
		return $response;
	}
}

这是一个典型的中间件类,其 handle 方法接收一个请求和一个叫“下一步”的函数, 并返回一个响应。

Laravel 允许我们的中间件返回 mixed,但笔者不推荐这么做。 建议的做法是只允许其返回响应对象,并且是“下一步”所返回的那个, 而不要新建响应对象。遵循这样的规范,可避免诸如 header 丢失这样的意外发生。

这个“下一步”函数,即执行流水线上的下一步,可能是执行下一个中间件, 也可能是执行最终的控制器。总之也是个吃请求,吐响应的东西。 从代码的结构看,流水线的上下游,是嵌套的关系。可以形象地表示为:

<Middleware-0>
	<Middleware-1>
		<Middleware-2>
			<Middleware-3>
				<Controller />
			</Middleware-3>
		</Middleware-2>
	</Middleware-1>
</Middleware-0>

一层层的中间件,就像一层层的门卫。 越上游的中间件,就可以越早地接触请求,也能越后手地处理响应,总之权利越大。 这就是流水线的运行机制,层层向内,再层层向外。

中间件的注册

   注册中间件有三种方式,其零是全局注册,其一是在路由上注册,其二是在控制器中注册。 不同的情况,应当采取不同的方式。全局注册,显而易见,适合那些统揽全局的中间件。 例如 Laravel 默认的系统维护中间件,当系统处于维护状态时,它将拦截所有请求。 再如强制 HTTPS 的中间件,拦截所有 HTTP 请求,跳转到对应的 HTTPS 请求。 而那些作用于一定范围的中间件,则应当在路由上注册。 例如登录判断中间件,应当注册于一个路由组上,这个路由组包含了所有用户私有的路由。 而不需要登录即可访问的路由,例如登录页面,应放在此组之外。 而那些应用于具体个例的中间件,则要具体分析。属于路由的,要在路由上注册。 耦合于控制器的,则应在控制器上注册。我们可以如此假设: 若该控制器同时注册在多个路由上,此中间件是否都必须要注册。 例如一个上传头像的控制器,上面要注册一个内容读取中间件, 以读取上传的内容,并弥合 PHP 对 POST 和 PUT 请求的差异; 还要注册一个访问频率限制中间件,来避免频繁上传耗费过多服务器带宽。 我们假设它同时注册在用户和管理员的路由组下。 对于内容读取中间件,不论在哪上传,必然都需要,因此要在控制器中注册。 而对于频率限制中间件,则两处可能不同,对管理员的限制可能更宽松。 因此要注册在路由上。总地来说,只有少部分中间件,应注册在全局或控制器上, 而大部分都适合注册在路由上。

控制中心

   每个 Laravel 项目中都有一个 \App\Http\Kernel 类, 这便是中间件的控制中心。 全局中间件在这里注册;中间件权利顺序由这里掌管; 在这里,你能为中间件起一个简短的别名, 还能将若干中间件打包成一个组,方便一起注册。

namespace App\Http;

class Kernel extends \Illuminate\Foundation\Http\Kernel
{
	protected $middleware= [
		// 在此注册全局中间件
	];
	
	protected $middlewarePriority= [
		// 在此定中间件的义优先级,越靠前的中间件权利越大
	];
	
	protected $routeMiddleware= [
		// 在此定义中间件的别名($routeMiddleware 这个名字不太合适,别名可不仅在路由中使用)
	];
	
	protected $middlewareGroups= [
		// 在此定义中间件组
		'group_name'=> [
			// 组内中间件
		],
	];
}

一个初始化的 Laravel 项目中,$routeMiddleware 里, 官方为我们准备了一些常用的中间件。我们大可奉行拿来主义。 而在 $middlewareGroups 中,则预设了 webapi 两个组。 它们在 \App\Providers\RouteServiceProvider 中使用, 分别被注册在各自的路由组上。这只是官方提供的一个范式, 根据项目结构不同,我们大可大刀阔斧地改动这个文件, 而不必拘泥于 web 与 api 二分天下的固有结构。

小推荐

   末了,推荐一个包 Laroute, 是一个用于 Laravel 的路由工具,同时也是一种路由的语言。可以大幅降低路由文件的语法噪音。 作者即是“王婆”本人了。让我们感受一下 Laroute 带来的清爽:

// Routing with PHP
Route::group( [ 'middleware'=>[ 'middleware0', 'middleware1:param', ], ],function(){
	Route::get( '/', 'Home@home' )
		->middleware( 'middleware2' )
		->middleware( 'middleware3:1,60' )
		->name( 'home' )
	;
} );
# Routing with Laroute
: >middleware0 >middleware1:param
	GET / home
		>middleware2
		>middleware3:1,60
		>>Home@home