背景
在Laravel框架中,解析请求生成响应之前或之后需要经过中间件的处理,包括验证、Cookie加密、开启会话、CSRF保护等。这些处理有些是在生成响应之前,有些是在生成响应之后,在实际开发过程中可能还需要添加新的处理功能,如果在不修改原有类的基础上动态地添加或减少处理功能,将使框架可扩展性大大增强,而这种需求正好可以用装饰者模式解决。
array_slice的用法
在说装饰者模式之前,我们先了解一下array_slice的用法。
函数原型:
1
|
array_reduce ( array $array , callable $callback [, mixed $initial = NULL ] ) : mixed
|
array_reduce() 将回调函数 callback 迭代地作用到 array数组中的每一个单元中,从而将数组简化为单一的值。
例1
1
2
3
4
5
6
7
8
9
10
|
function sum($carry, $item)
{
$carry += $item;
return $carry;
}
$a = array(1, 2, 3, 4);
var_dump(array_reduce($a, "sum")); // int(10)
var_dump(array_reduce($a, "sum", 10)); // int(20)
|
把sum函数迭代到$a数组中,注意:array_reduce($a, “sum”)中,由于没有设置第3个参数,所以$carry的值第一次是null,一定要做好妥善处理,这个调用的结果就是null+1+2+3+4 = 10 。而array_reduce($a, “sum”, 10)有一个初始值10,所以结果是10+1+2+3+4 = 20。
例2: 对上边的例子做一下等价换算
1
2
3
4
5
6
7
|
array_reduce([1, 2, 3, 4], "sum");
array_reduce([2, 3, 4], "sum", sum(null, 1) );
array_reduce([3, 4], "sum", sum(sum(null, 1), 2) );
array_reduce([4], "sum", sum(sum(sum(null, 1), 2), 3) );
array_reduce([], "sum", sum(sum(sum(sum(null, 1), 2), 3), 4) );
最终的函数调用:
sum(sum(sum(sum(null, 1), 2), 3), 4);
|
以上就是等价的调用过程,其中null会被转成0处理。
中间件的实现
首先,我们选用《Laravel框架关键技术解析》中的列子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
|
interface Middleware
{
public static function handle(Closure $next);
}
class VerifyCsrfToken implements Middleware
{
public static function handle(Closure $next)
{
echo "验证csrf-token\n";
$next();
}
}
class ShareErrorsFromSession implements Middleware
{
public static function handle(Closure $next)
{
echo "如果session中有‘errors’变量,则共享它\n";
$next();
}
}
class StartSession implements Middleware
{
public static function handle(Closure $next)
{
echo "开启session, 获取数据.\n";
$next();
echo "保存数据,关闭session\n";
}
}
class AddQueuedCookiesToResponse implements Middleware
{
public static function handle(Closure $next)
{
$next();
echo "添加下一次请求需要的cookie\n";
}
}
class EncryptCookies implements Middleware
{
public static function handle(Closure $next)
{
echo "对输入请求的cookie进行解密\n";
$next();
echo "对输出响应的cookie进行加密\n";
}
}
class CheckForMaintenanceMode implements Middleware
{
public static function handle(Closure $next)
{
echo "确定当前程序是否处于维护状态\n";
$next();
}
}
function getSlice()
{
return function($stack, $pipe)
{
return function() use ($stack, $pipe)
{
return $pipe::handle($stack);
};
};
}
function then()
{
$pipes = [
"CheckForMaintenanceMode",
"EncryptCookies",
"AddQueuedCookiesToResponse",
"StartSession",
"ShareErrorsFromSession",
"VerifyCsrfToken"
];
$firstSlice = function() {
echo "请求向路由器传递,返回响应.\n";
};
$pipes = array_reverse($pipes);
call_user_func(array_reduce($pipes, getSlice(), $firstSlice));
}
then();
|
运行结果:
1
2
3
4
5
6
7
8
9
|
确定当前程序是否处于维护状态
对输入请求的cookie进行解密
开启session, 获取数据.
如果session中有‘errors’变量,则共享它
验证csrf-token
请求向路由器传递,返回响应.
保存数据,关闭session
添加下一次请求需要的cookie
对输出响应的cookie进行加密
|
then函数拆解
首先,对then函数中的代码做一个简单的精简,可以看到array_reduce的第二个参数是一个函数调用getSlice(),那么我们可以直接转成一个函数f
1
2
3
4
5
6
7
|
function f($stack, $pipe)
{
return function() use ($stack, $pipe)
{
return $pipe::handle($stack);
};
}
|
那么对于的调用就变成了:
1
|
call_user_func(array_reduce($pipes, "f", $firstSlice));
|
然后把函数进一步拆解:
1
2
|
$res = array_reduce($pipes, "f", $firstSlice);
call_user_func($res);
|
好了,我们来重点分析一下array_reduce的调用关系。
核心迭代函数的拆解
我们把$pipes精简成3个,另外暂时不考虑顺序(即不考虑array_reverse)
1
2
3
4
5
6
7
8
9
|
array_reduce(["EncryptCookies","StartSession","VerifyCsrfToken"], "f", $firstSlice);
array_reduce(["StartSession","VerifyCsrfToken"], "f", f($firstSlice, "EncryptCookies"));
array_reduce(["VerifyCsrfToken"], "f", f(f($firstSlice, "EncryptCookies"),"StartSession"));
array_reduce([], "f", f(f(f($firstSlice, "EncryptCookies"),"StartSession"), "VerifyCsrfToken"));
f(f(f($firstSlice, "EncryptCookies"),"StartSession"), "VerifyCsrfToken");
|
再来拆解一下f($firstSlice, “EncryptCookies”),返回值是一个函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
function()
{
return EncryptCookies::handle($firstSlice);
}
//handle函数中的调用:
function handle(Closure $next)
{
echo "对输入请求的cookie进行解密\n";
$next(); //即调用firstSlice()
echo "对输出响应的cookie进行加密\n";
}
|
拆解f(f($firstSlice, “EncryptCookies”),“StartSession”)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
//第1步转换:
f(function(){
return EncryptCookies::handle($firstSlice);
}, "StartSession");
//第2步转换:
function() {
StartSession::handle(function(){
return EncryptCookies::handle($firstSlice);
});
}
//对应的handle函数:
function handle(Closure $next)
{
echo "开启session, 获取数据.\n";
$next(); //即调用function(){return EncryptCookies::handle($firstSlice);}
echo "保存数据,关闭session\n";
}
|
拆解f(f(f($firstSlice, “EncryptCookies”),“StartSession”), “VerifyCsrfToken”)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
//第1步转换:
f(function() {
StartSession::handle(function(){
return EncryptCookies::handle($firstSlice);
});
}, "VerifyCsrfToken");
//第2步转换:
VerifyCsrfToken::handle(function() {
StartSession::handle(function(){
return EncryptCookies::handle($firstSlice);
});
});
//对应的handle函数:
function handle(Closure $next)
{
echo "验证csrf-token\n";
$next();
}
//$next对应的函数调用为:
VerifyCsrfToken::handle(function() {
StartSession::handle(function(){
return EncryptCookies::handle($firstSlice);
});
});
|
总结
用PHP用装饰者模式实现中间件的关键点:
- 借助array_reduce实现迭代调用
- array_reduce中的第二个参数(即callback函数f)的返回值是匿名函数,即Closure类型
- 有对应的接口或者抽象类定义同名的handle函数,对应的参数是Closure类型,即可调用的匿名函数
- array_reduce的第3个参数一定要传,避免handle函数调用时报错(为null时)
参考列表