中间件一般用于拦截请求或者响应。例如执行控制器前统一验证用户身份,如用户未登录时跳转到登录页面,例如响应中增加某个header头。例如统计某个uri请求占比等等。
┌──────────────────────────────────────────────────────┐
│ middleware1 │
│ ┌──────────────────────────────────────────┐ │
│ │ middleware2 │ │
│ │ ┌──────────────────────────────┐ │ │
│ │ │ middleware3 │ │ │
│ │ │ ┌──────────────────┐ │ │ │
│ │ │ │ │ │ │ │
──Reqeust───────────────────>Controller──Response───────────────────────────>Client
│ │ │ │ │ │ │ │
│ │ │ └──────────────────┘ │ │ │
│ │ │ │ │ │
│ │ └──────────────────────────────┘ │ │
│ │ │ │
│ └──────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────┘
中间件和控制器组成了一个经典的洋葱模型,中间件类似一层一层的洋葱表皮,控制器是洋葱芯。
如图所示请求像箭一样穿越中间件1、2、3到达控制器,控制器返回了一个响应,然后响应又以3、2、1的顺序穿出中间件最终返回给客户端。也就是说在每个中间件里我们既可以拿到请求,也可以获得响应。
虽然这些简短的解释显然很容易理解,但让我用一个小例子来说明这一点:
$input = [];
$output = $this->action($input);
在此示例中,中间件层能够:
中间件层由一堆中间件可调用对象组成。这些可以是简单的闭包、可调用的类或类上的方法。
每个中间件可调用对象都围绕着核心作。它就像一个洋葱,或者一个带有多层包装纸的礼物。输入被注入到第一个中间件(或最外层)。中间件可以对输入执行某些作,然后将该(更改的)输入传递给下一个中间件,下一个和下一个,直到它到达最终作(核心)。到这个时候,输入可能会发生一些或很大的变化。
然后执行核心作并将其输出返回给最后一个中间件,最后一个中间件将其输出返回到前一个中间件,一直返回到第一个中间件。在返回的途中,每个中间件现在都可以在返回输出之前更改输出。
虽然中间件的概念在它们之间几乎相同,但实际上有两种常见的中间件类型:单通道中间件和双通道中间件。
最常见的中间件类型是单通道中间件。使用这种类型,每个中间件可调用对象都会收到两个参数:
可调用对象,用于调用链中的下一个中间件,该中间件也接收一个参数:输入。“注意: 在某些约定中,调用可调用参数
$next以指示下一个中间件的调用。
这种类型的中间件可以更改输入并将修改后的版本转发到下一个中间件。影响输出;它将首先调用下一个中间件并检索其输出。然后,它可以修改该输出并返回此输出。
这是一个小的中间件示例来说明完整的行为。
function ($input, $next) {
// Receive the input and change it.
$input = $this->changeInput($input);
// Call the next middleware or final action with the changed input, and receive the output.
$output = $next($input);
// Change the output, and return it to the previous middleware or final consumer.
return$this->changeOutput($output);
}
第二种中间件是双传递中间件。对于这种类型,每个中间件也会收到默认的output对象作为参数。因此,“double”部分是指中间件将输入和输出传递到下一个中间件/最终作。
为什么这有用?在单通道类型中,中间件必须:
$next而是返回不同的结果)。$next中间件来检索输出对象,并更改它。根据输出类型,使中间件依赖于服务或工厂来创建该输出类型可能会很麻烦,甚至是不可取的。调用(并可能实例化)所有其他中间件和最终作,只是丢弃它们所做的一切,这同样是不可取的;并完全更改输出对象。
因此,使用双通道时,预先创建并传递默认输出对象。这样,中间件已经有一个正确类型的输出对象,它可以在想要短路时使用。
这是另一个小示例来说明完整的行为。
function ($input, $output, $next) {
// Quickly return the output object, instead of passing it along.
if ($this->hasSomeReason($input)) {
return $output;
}
// Call the next middleware and return that output instead of the default output.
return $next($input, $output);
}
在深入研究一些代码,并创建一个非常基本的Single Pass中间件实现。在这个例子中,我们将使用单个中间件类在字符串前面添加一些值并附加(重新)。
“注意: 可以通过多种方式构建中间件实现。这个例子没有得到很好的优化,还有很大的改进空间。但这个例子实际上是为了尽可能清楚地展示中间件的内部工作原理。
正如我们所见,我们首先需要一个核心作(或函数)来包装我们的中间件。我们的作将简单地接收并返回一个字符串。
$action = fn(string $input): string => $input;
现在我们将创建一个可调用的中间件类,我们可以多次使用不同的值来添加和赋值。
class ValueMiddleware
{
public function __construct(string $value) {}
public function __invoke(string $input, callable $next): string
{
// Prepend value to the input.
$output = $next($this->value . $input);
// Append value to the output.
return$output . $this->value;
}
}
中间件类使用$value实例化。这个实例将是我们的中间件可调用对象,因为它是一个可调用的类。让我们创建其中的 3 个:
$middlewares = [
new ValueMiddleware('1'),
new ValueMiddleware('2'),
new ValueMiddleware('3'),
];
现在,我们将所有这些中间件作为$action周围的层添加,然后我们将检查代码。
foreach ($middlewares as $middleware) {
$action = fn(string $input): string => $middleware($input, $action);
}
让我们看看这个循环中发生了什么。
“注意:正如我们所看到的,next从未直接引用中间件。它不能,因为中间件方法签名需要 2 个参数,而next只接收一个参数:输入。
就是这样。我们的中间件实现已经完成。我们现在需要做的就是运行带有值的$action,并查看响应。
echo $action('value');
使用中间件的最常见位置之一是在转换请求的框架中 object 转换为Response对象。PSR-15:PHP 标准建议 (PSR) 中的 HTTP 服务器请求处理程序是关于 (PSR-7)请求如何 对象应该被处理并转换为Response对象。
此建议还包含Psr\Http\Server\MiddlewareInterface.此接口倾向于在中间件类上使用进程方法,但原理是相同的。它接收一个输入(Request对象),对其进行修改,并将其传递给RequestHandler,后者将触发下一个中间件或最终作。
中间件可用于在请求期间执行各种作和检查,因此主作(控制器)可以专注于手头的任务。让我们看几个有用的中间件示例。
为了防止CSRF攻击,框架可以选择向请求添加特定的验证。此验证需要在触发主作之前进行。在这种情况下,中间件可以检查请求对象。如果它认为请求有效,它可以将其传递给下一个处理程序。但如果验证失败,中间件可以立即短路请求,并返回一个403 Forbidden响应。
<?php
namespaceapp\middleware;
useWebman\MiddlewareInterface;
useWebman\Http\Response;
useWebman\Http\Request;
class AccessControlTest implements MiddlewareInterface
{
publicfunction process(Request $request, callable $handler) : Response
{
// 如果是options请求则返回一个空响应,否则继续向洋葱芯穿越,并得到一个响应
$response = $request->method() == 'OPTIONS' ? response('') : $handler($request);
// 给响应添加跨域相关的http头
$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;
}
}
有时候我们不想某个请求到达控制器层,例如我们在middleware2发现当前用户并没有登录,则我们可以直接拦截请求并返回一个登录响应。那么这个流程类似下面这样
┌────────────────────────────────────────────────────────────┐
│ middleware1 │
│ ┌────────────────────────────────────────────────┐ │
│ │ middleware2 │ │
│ │ ┌──────────────────────────────┐ │ │
│ │ │ middleware3 │ │ │
│ │ │ ┌──────────────────┐ │ │ │
│ │ │ │ │ │ │ │
──Reqeust─────────┐ │ │ Controller │ │ │ │
│ │Response│ │ │ │ │ │
<───────────────────┘ │ └──────────────────┘ │ │ │
│ │ │ │ │ │
│ │ └──────────────────────────────┘ │ │
│ │ │ │
│ └────────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────┘
如图所示请求到达middleware2后生成了一个登录响应,响应从middleware2穿越回中间件1然后返回给客户端。