从零开始搭建自己的Swoole框架(八)路由中间件

中间件的概念

中间件就是一种系统之间互相连接的“中间的一层”。

通俗的讲类似古代的关口,西游记里唐僧每到一个国家都要取得这个国家的“通关文凭”,如果没有通关文凭就无法离开国界。边关的守卫就可以理解为“中间件”,唐僧就是请求,如果没有通过文凭(即达不到某种要求)就会被拦截在关口。

也就是说,中间件的主要功能是“拦截不符合规范的请求”。

它就是一种 if-else 条件判断结构,如果……就……

比如要设计一个活动页面,只有今天晚上 9:00 到 10:00 这个时间段才会进入活动页,如果还不到 9 点就打开这个页面就会显示“活动还未开始”,如果是 10 点之后打开这个页面,就会显示“活动已结束”。

要实现这种功能十分简单,直接用 if-else 结构就可以了。

但是这种思想属于面向过程,在框架里可以将判断条件封装为“中间件”实现自动化处理请求,满足要求的就放过,不满足要求的就拦截下来,返回失败的处理。

中间件的应用场景

中间件即拦截不符合规范的请求,因此它能用的场景非常多。

例如规定了某个时间段开放、关闭的活动页面;

表单验证、用户登录状态验证等等。

总之,凡是能用“如果……就……”描述的,几乎都可以用中间件实现,因为它本身即是一种条件判断结构。

Laravel 中的中间件

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
<?php

namespace App\Http\Middleware;

use Closure;

class AuthCheck
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
// 判断用户是否登录状态,如果已登录则进入下一步
if (auth()->check()) {
return $next($request);
}

// 如果未登录则返回提示页面的视图
$message = '用户未登录,无法操作,<a href="#">前往登录</a>。';
return $this->showErrorPage($message);
}

public functio showErrorPage($message) {
// ... 返回自定义视图页面
}
}

然后在 Kernel.php 中注册中间件,并且命名:

1
2
3
protected $routeMiddleware = [
'auth.check' => AuthCheck::class,
];

最后只要在路由配置中为需要验证用户身份的路由加上中间件即可:

1
$router->middleware('auth.check')->get('/user', 'UserController@index')->name('user.index');

只需如此简单的配置即可实现路由拦截。

Laravel 中间件的原理

一个路由可以有很多中间件,只有满足所有中间件才让请求继续下去,否则就终端请求返回错误的结果。

看起来只需要一个 foreach 循环就能实现中间件了,用伪代码实现思路如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$flag = true;

$conditions = [条件1, 条件2, 条件3];

foreach ($conditions as $condition) {
if($condition == false) {
$flag = false;
break;
}
}

if ($flag == true) {
// 成功,进入下一步
} else {
// 失败,返回失败页
}

好像确实可以,但我出于好奇研究了一下 Laravel 的源码。

Laravel 源码

在定义 Middleware 类的时候,我发现 Middleware 不需要继承任何框架的基类:

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
<?php

namespace App\Http\Middleware;

use Closure;

class AuthCheck
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
// 判断用户是否登录状态,如果已登录则进入下一步
if (auth()->check()) {
return $next($request);
}

// 如果未登录则返回提示页面的视图
$message = '用户未登录,无法操作,<a href="#">前往登录</a>。';
return $this->showErrorPage($message);
}

public functio showErrorPage($message) {
// ... 返回自定义视图页面
}
}

只是定义一个 handle 方法,一共接收两个参数,

一个是 Laravel 的 $request,另一个是闭包类型 $next。

如果请求验证成功,则直接返回闭包执行结果 $next($request)

如果请求不符合要求,就自定义一个响应返回。

看来,玄机并不在 Middleware 的定义里。

基于 php-fpm 的框架入口文件基本上都是 index.php,

因此找到 Laravel 的入口文件在 public 目录下面,index.php 的内容如下:

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
<?php

/**
* Laravel - A PHP Framework For Web Artisans
*
* @package Laravel
* @author Taylor Otwell <taylor@laravel.com>
*/

define('LARAVEL_START', microtime(true));

/*
|--------------------------------------------------------------------------
| Register The Auto Loader
|--------------------------------------------------------------------------
|
| Composer provides a convenient, automatically generated class loader for
| our application. We just need to utilize it! We'll simply require it
| into the script here so that we don't have to worry about manual
| loading any of our classes later on. It feels great to relax.
|
*/

require __DIR__.'/../vendor/autoload.php';

/*
|--------------------------------------------------------------------------
| Turn On The Lights
|--------------------------------------------------------------------------
|
| We need to illuminate PHP development, so let us turn on the lights.
| This bootstraps the framework and gets it ready for use, then it
| will load up this application so that we can run it and send
| the responses back to the browser and delight our users.
|
*/

$app = require_once __DIR__.'/../bootstrap/app.php';

/*
|--------------------------------------------------------------------------
| Run The Application
|--------------------------------------------------------------------------
|
| Once we have the application, we can handle the incoming request
| through the kernel, and send the associated response back to
| the client's browser allowing them to enjoy the creative
| and wonderful application we have prepared for them.
|
*/

$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

$response = $kernel->handle(
$request = Illuminate\Http\Request::capture()
);

$response->send();

$kernel->terminate($request, $response);

这里引入了一个文件,然后得到一个 $app 对象,接着调用 handle 方法执行响应事件,

然后就没有其他代码了,因此这个引入的 app.php 是关键所在:

1
$app = require_once __DIR__.'/../bootstrap/app.php';

找到 app.php 发现如下内容:

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
<?php

/*
|--------------------------------------------------------------------------
| Create The Application
|--------------------------------------------------------------------------
|
| The first thing we will do is create a new Laravel application instance
| which serves as the "glue" for all the components of Laravel, and is
| the IoC container for the system binding all of the various parts.
|
*/

$app = new Illuminate\Foundation\Application(
$_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
);

/*
|--------------------------------------------------------------------------
| Bind Important Interfaces
|--------------------------------------------------------------------------
|
| Next, we need to bind some important interfaces into the container so
| we will be able to resolve them when needed. The kernels serve the
| incoming requests to this application from both the web and CLI.
|
*/

$app->singleton(
Illuminate\Contracts\Http\Kernel::class,
App\Http\Kernel::class
);

$app->singleton(
Illuminate\Contracts\Console\Kernel::class,
App\Console\Kernel::class
);

$app->singleton(
Illuminate\Contracts\Debug\ExceptionHandler::class,
App\Exceptions\Handler::class
);

/*
|--------------------------------------------------------------------------
| Return The Application
|--------------------------------------------------------------------------
|
| This script returns the application instance. The instance is given to
| the calling script so we can separate the building of the instances
| from the actual running of the application and sending responses.
|
*/

return $app;

这里是注册服务容器的地方,服务容器 Laravel 实例化类的一种设计模式,

具体的原理我也没有搞懂,只要知道这是一个“注册和实例化类”的地方就可以了。

跟 HTTP 请求相关的部分:

1
2
3
4
$app->singleton(
Illuminate\Contracts\Http\Kernel::class,
App\Http\Kernel::class
);

按住 Ctrl 点击 App\Http\Kernel::class 可以跳转到类定义的地方,

结果发现跳转到中间件配置的地方了:

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
<?php

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* These middleware are run during every request to your application.
*
* @var array
*/
protected $middleware = [
// \App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class,
\Fruitcake\Cors\HandleCors::class,
\App\Http\Middleware\CheckForMaintenanceMode::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
];

/**
* The application's route middleware groups.
*
* @var array
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
// \Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],

'api' => [
// 'throttle:60,1',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];

/**
* The application's route middleware.
*
* These middleware may be assigned to groups or used individually.
*
* @var array
*/
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
];
}

看来离想要找到的结果不远了,这个配置也没什么奇怪的地方,

接着发现这个类继承了另一个类:

1
use Illuminate\Foundation\Http\Kernel as HttpKernel;

于是我们继续前往这个类,发现这个类有很多方法,

我就直接截取关键部分了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Handle an incoming HTTP request.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function handle($request)
{
try {
$request->enableHttpMethodParameterOverride();

$response = $this->sendRequestThroughRouter($request);
} catch (Throwable $e) {
$this->reportException($e);

$response = $this->renderException($request, $e);
}

$this->app['events']->dispatch(
new RequestHandled($request, $response)
);

return $response;
}

handle 方法?也就是说最开始入口文件执行的便是这个方法了。

根据注释:Handle an incoming HTTP request

可以知道这里确实是处理进来请求的地方。

第一行执行的方法:enableHttpMethodParameterOverride,即 Laravel 重写请求方法的地方,

在 Laravel 除了 GET 和 POST 之外,还定义了 PUT、DELETE 等方法,

这里就是判断 _method 变量生成特殊请求方法的地方。

接着查看 sendRequestThroughRouter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* Send the given request through the middleware / router.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
protected function sendRequestThroughRouter($request)
{
$this->app->instance('request', $request);

Facade::clearResolvedInstance('request');

$this->bootstrap();

return (new Pipeline($this->app))
->send($request)
->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
->then($this->dispatchToRouter());
}

这里的代码应该就是我想要找的了,

1
2
3
4
return (new Pipeline($this->app))
->send($request)
->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
->then($this->dispatchToRouter());

send 方法非常简单:

1
2
3
4
5
6
7
8
9
10
11
12
 /**
* Set the object being sent through the pipeline.
*
* @param mixed $passable
* @return $this
*/
public function send($passable)
{
$this->passable = $passable;

return $this;
}

并不是处理中间件的逻辑,接着看 through:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* Set the array of pipes.
*
* @param array|mixed $pipes
* @return $this
*/
public function through($pipes)
{
$this->pipes = is_array($pipes) ? $pipes : func_get_args();

return $this;
}

也不是,最后的 then:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Run the pipeline with a final destination callback.
*
* @param \Closure $destination
* @return mixed
*/
public function then(Closure $destination)
{
$pipeline = array_reduce(
array_reverse($this->pipes()), $this->carry(), $this->prepareDestination($destination)
);

return $pipeline($this->passable);
}

也只有短短数行的代码,难道最后也没找到中间件的实现逻辑?

而且……这个 array_reduce 是什么鬼?

仔细的研究了一番,发现这里的代码虽然只有 4 行,可真的不简单!

其中,最关键的部分是这个叫做 carry 的方法:

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
/**
* Get a Closure that represents a slice of the application onion.
*
* @return \Closure
*/
protected function carry()
{
return function ($stack, $pipe) {
return function ($passable) use ($stack, $pipe) {
try {
if (is_callable($pipe)) {
// If the pipe is a callable, then we will call it directly, but otherwise we
// will resolve the pipes out of the dependency container and call it with
// the appropriate method and arguments, returning the results back out.
return $pipe($passable, $stack);
} elseif (! is_object($pipe)) {
[$name, $parameters] = $this->parsePipeString($pipe);

// If the pipe is a string we will parse the string and resolve the class out
// of the dependency injection container. We can then build a callable and
// execute the pipe function giving in the parameters that are required.
$pipe = $this->getContainer()->make($name);

$parameters = array_merge([$passable, $stack], $parameters);
} else {
// If the pipe is already an object we'll just make a callable and pass it to
// the pipe as-is. There is no need to do any extra parsing and formatting
// since the object we're given was already a fully instantiated object.
$parameters = [$passable, $stack];
}

$carry = method_exists($pipe, $this->method)
? $pipe->{$this->method}(...$parameters)
: $pipe(...$parameters);

return $this->handleCarry($carry);
} catch (Throwable $e) {
return $this->handleException($passable, $e);
}
};
};
}

array_reduce

如果猜的没错,Laravel 应该就是使用 array_reduce 来实现中间件的。

查了一下 PHP 的官方文档,它对 array_reduce 的描述是:

1
array_reduce — 用回调函数迭代地将数组简化为单一的值

嗯……不愧是官方文档,说了跟没讲一样。

还是通过实战来了解一下什么是 array_reduce

1
2
3
4
5
6
7
8
9
10
$params = ['a', 'b', 'c'];
$result = array_reduce($params, function ($carry, $item) {

var_dump('carry=' . $carry);
var_dump('item=' . $item);

return $carry . $item;
});

var_dump($result);

声明一个数组 $params 且包含三个字符串,

然后通过 array_reduce 传入数组参数,同时还有一个闭包,

闭包接收两个参数 $carry, $item,然后试着打印这两个参数以及最终结果:

1
2
3
4
5
6
7
string(6) "carry="
string(6) "item=a"
string(7) "carry=a"
string(6) "item=b"
string(8) "carry=ab"
string(6) "item=c"
string(3) "abc"

也就是说,一开始 $carry 的值是空的(Null),然后随着循环,

$carry 会逐渐合并数组的每一个元素。

array_reduce 可以说是如下代码构成的:

1
2
3
4
5
6
7
8
$params = ['a', 'b', 'c'];
$result = null;

foreach ($params as $param) {
$result .= $param;
}

var_dump($result);

循环遍历数组的每一个元素,然后保持一个不变的值。

与官方文档的描述对应起来了!将数组简化为一个单一的值。

也就是说通过 array_reduce 最终会返回一个值作为处理的结果。

array_reduce 可以接收第三个参数,即初始值:

1
2
3
4
5
6
$params = ['a', 'b', 'c'];
$result = array_reduce($params, function ($carry, $item) {
return $carry . $item;
}, 'init');

var_dump($result);

最终会输出:initabc

如果不设置第三个参数,那么初始值就会默认为 Null。

最开始,我以为会是返回 true 或者 false 来判定中间件的执行结果,

但是 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
/**
* Get a Closure that represents a slice of the application onion.
*
* @return \Closure
*/
protected function carry()
{
return function ($stack, $pipe) {
return function ($passable) use ($stack, $pipe) {
try {
if (is_callable($pipe)) {
// If the pipe is a callable, then we will call it directly, but otherwise we
// will resolve the pipes out of the dependency container and call it with
// the appropriate method and arguments, returning the results back out.
return $pipe($passable, $stack);
} elseif (! is_object($pipe)) {
[$name, $parameters] = $this->parsePipeString($pipe);

// If the pipe is a string we will parse the string and resolve the class out
// of the dependency injection container. We can then build a callable and
// execute the pipe function giving in the parameters that are required.
$pipe = $this->getContainer()->make($name);

$parameters = array_merge([$passable, $stack], $parameters);
} else {
// If the pipe is already an object we'll just make a callable and pass it to
// the pipe as-is. There is no need to do any extra parsing and formatting
// since the object we're given was already a fully instantiated object.
$parameters = [$passable, $stack];
}

$carry = method_exists($pipe, $this->method)
? $pipe->{$this->method}(...$parameters)
: $pipe(...$parameters);

return $this->handleCarry($carry);
} catch (Throwable $e) {
return $this->handleException($passable, $e);
}
};
};
}

上面的代码十分生涩难懂,简直如同“天书”,

因此我自己尝试实现相同的逻辑并且让代码变成“说人话”。

对暗号游戏

接下来我开始参考着 Laravel 中间件的代码实现一个“对暗号”的“游戏”,

比如在一个军营里,一共有 A、B、C 三个巡逻队,

为了避免整个暗号泄露出去,规定每一个巡逻队都只持有暗号的其中一句,

今晚的暗号是:“上山打老虎”,

那么三个巡逻队分别得到的暗号是:

A:上山

B:打

C:老虎

而你半夜出去嘘嘘,刚好被巡逻队给碰上了……

于是,你必须说出你的口令,否则就会被当做奸细就地正法……

三只巡逻队可以抽象成“巡逻队”概念,即定义一个 Middleware 作为父类,

他们都有核对口号的方法 handle,以及自己的密令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
abstract class Middleware
{
public $keyword;

public function handle($value, Closure $closure)
{
var_dump('暗号:' . $this->keyword);

// 包含指定关键词的口令视为核对成功
if (strstr($value, $this->keyword) != false) {
return $closure($value);
}

return '口令核对失败';
}
}

接着创建三个巡逻队,继承基类并且拥有独立的口令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

class Middleware_A extends Middleware
{
public $keyword = '上山';
}

class Middleware_B extends Middleware
{
public $keyword = '打';
}

class Middleware_C extends Middleware
{
public $keyword = '老虎';
}

接下来实现具体的逻辑,声明一个包含 N 只巡逻队的数组(可以是 0-3 个):

1
2
3
4
5
6
7
function middlewares()
{
$params = ['Middleware_A', 'Middleware_B', 'Middleware_C'];
$params = array_reverse($params);

return $params;
}

这里增加了一个 array_reverse 将数组反转的方法,下文会解释。

然后实现核对密令的逻辑,比如你遇到巡逻队 A,那就核对巡逻队 A 的密令,

如果同时遇到两只巡逻队,A+B 或者 A+C 或者 B+C,那就应该核对两个巡逻队的密令,

如果你非常不幸的同时遇到三只巡逻队,那就要核对 ABC 的密令:

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
function carry()
{
return function ($stack, $pipe) {
return function ($passable) use ($stack, $pipe) {

if ($pipe instanceof Closure) {
return $pipe($passable, $stack);
} elseif (!is_object($pipe)) {
$pipe = new $pipe;
}

return $pipe->handle($passable, $stack);
};
};
}

function init()
{
return function ($destination) {

var_dump($destination);

return 'ok';
};
}


$response = array_reduce(middlewares(), carry(), init());

var_dump($response('小鸡炖蘑菇'));
var_dump($response('上山打野鸡'));
var_dump($response('上山打老虎'));

上面的代码虽然很短,但是要理解起来非常不易。

array_reduce 可以接收三个参数:

第一个参数是数组,即要遍历的数组;

第二个参数是一个方法/闭包(匿名函数),即执行遍历的逻辑;

第三个参数是初始值。

初始值是最终想要实现的结果,当满足所有条件后,就会返回初始值函数里的代码。

middlewares 是最开始定义巡逻队的地方,很不幸你同时遇到三只巡逻队:

1
2
3
4
5
6
7
function middlewares()
{
$params = ['Middleware_A', 'Middleware_B', 'Middleware_C'];
$params = array_reverse($params);

return $params;
}

array_reverse 这里的反转数组与接下来的堆栈调用有关,

栈结构是先进后出,会导致乱序,我们希望的结果是按照 A、B、C 的顺序执行。

init 方法定义了最终希望输出的值,如果满足所有条件的话,就返回这个值。

1
2
3
4
5
6
7
8
9
function init()
{
return function ($destination) {

var_dump($destination);

return 'ok';
};
}

carry 是整个逻辑最关键的部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function carry()
{
return function ($stack, $pipe) {
return function ($passable) use ($stack, $pipe) {

if ($pipe instanceof Closure) {
return $pipe($passable, $stack);
} elseif (!is_object($pipe)) {
$pipe = new $pipe;
}

return $pipe->handle($passable, $stack);
};
};
}

这个方法涉及了闭包的递归调用,最终的返回结果依然是一个闭包。

carry 方法传入两个参数 $stack,

$stack 即遍历过程中持续引用的值,而 $pipe 则是当前元素。

回忆一下上面的代码:

1
2
3
4
5
6
$params = ['a', 'b', 'c'];
$result = array_reduce($params, function ($carry, $item) {
return $carry . $item;
}, 'init');

var_dump($result);

应该不难理解,这里就是循环遍历一个数组,依次取值进行计算,最终返回一个结果而已。

接下来分析代码:

1
2
3
4
5
6
7
8
9
10
11
12
return function ($stack, $pipe) {
return function ($passable) use ($stack, $pipe) {

if ($pipe instanceof Closure) {
return $pipe($passable, $stack);
} elseif (!is_object($pipe)) {
$pipe = new $pipe;
}

return $pipe->handle($passable, $stack);
};
};

carry 返回一个闭包,同时它内层的代码也是返回一个闭包,并且接收一个 $passable 作为参数。

$passable 的作用就是递归函数中不断传给下一次调用的值

在最内层,是一个条件判断语句:

1
2
3
4
5
6
7
if ($pipe instanceof Closure) {
return $pipe($passable, $stack);
} elseif (!is_object($pipe)) {
$pipe = new $pipe;
}

return $pipe->handle($passable, $stack);

如果传来的值不是 Closure(闭包类型),则判断它是否是一个对象,

如果不是对象则根据这个元素的名字实例化出对象来:

1
2
3
if (!is_object($pipe)) {
$pipe = new $pipe;
}

最终调用实例化对象的 handle 方法,并且把 $passable 和 持续保留的那个值 $stack 传给 handle。

再看一次执行的逻辑,并且模拟每一次执行的结果:

1
2
3
4
5
6
7
8
9
10
11
12
return function ($stack, $pipe) {
return function ($passable) use ($stack, $pipe) {

if ($pipe instanceof Closure) {
return $pipe($passable, $stack);
} elseif (!is_object($pipe)) {
$pipe = new $pipe;
}

return $pipe->handle($passable, $stack);
};
};

假设调用方法:

1
2
3
4
5
6
7
8
// array_reduce 返回的是一个闭包,可以当做函数调用
$response = array_reduce(middlewares(), carry(), init());

// 传入一个用来验证的口令
$result = $response('上山打野鸡');

// 打印出验证结果
var_dump($result);

第一次遍历:

通过 init 方法赋值:

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
// 第一步:$stack 赋值,init 方法也是一个闭包,接收一个 $destination 参数
// $stack 的初始值即 init 方法返回的闭包,所以是:

$stack = function ($destination) {
var_dump($destination);

return 'ok';
};

// 接着,取出数组的第一个元素
$pipe = 'Middleware_A';

// 第二步,进入闭包:function ($passable) use ($stack, $pipe)
// 这里的 $passable 就是上面调用时传入的值:“上山打野鸡”
// 执行判断语句

if ($pipe instanceof Closure) {
return $pipe($passable, $stack);
} elseif (!is_object($pipe)) {
$pipe = new $pipe;
}

// 很明显 $pipe 此时只是一个字符串,因此不满足 $pipe instanceof Closure
// 于是进入 else 条件 !is_object($pipe) 它并不是一个对象,因此满足此条件
// 所以将 $pipe = new $pipe; 实例化成对象
// 此处的代码即:$pipe = new Middleware_A();
// 实例化出巡逻队A的对象,然后调用他的 handle 方法并返回

return $pipe->handle($passable, $stack);

// Middleware_A 继承了父类 Middleware,因此 handle 为:

public function handle($value, Closure $closure)
{
var_dump('暗号:' . $this->keyword);

// 包含指定关键词的口令视为核对成功
if (strstr($value, $this->keyword) != false) {
return $closure($value);
}

return '口令核对失败';
}

// 此时 value 的值为:上山打野鸡,巡逻队A的暗号是:上山
// 因此巡逻队A验证成功,将这个值传给闭包然后返回

return $closure($value);

// 接收到参数的 $closure 就是 $stack,也就是我们最开始定义的 init 方法

第二次遍历:

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
// 此时已经不会经过 init 初始化了,
// $stack的值是第一步返回的 return $pipe->handle($passable, $stack);
// 也就是说init方法

// 第二次遍历$pipe就会取第二个巡逻队:Middleware_B
// 继续进入 function ($passable) use ($stack, $pipe) 执行判断语句

if ($pipe instanceof Closure) {
return $pipe($passable, $stack);
} elseif (!is_object($pipe)) {
$pipe = new $pipe;
}

// 同样Middleware_B只是一个字符串,因此会被实例化成类
// 然后与第一步一样,进行比对暗号,Middleware_B的暗号是:打
// 因此:上山打野鸡包含了这个字符,就符合巡逻队B的暗号
// 又经过父类的方法:

public function handle($value, Closure $closure)
{
var_dump('暗号:' . $this->keyword);

// 包含指定关键词的口令视为核对成功
if (strstr($value, $this->keyword) != false) {
return $closure($value);
}

return '口令核对失败';
}

// init 方法继续被传递给下一个执行的对象

第三步,也就是最后的一个巡逻队了,这里产生了一个分歧点,即最后一个暗号不符合要求:

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
// 第三次遍历$pipe就会取第三个巡逻队:Middleware_C
// 继续进入 function ($passable) use ($stack, $pipe) 执行判断语句

if ($pipe instanceof Closure) {
return $pipe($passable, $stack);
} elseif (!is_object($pipe)) {
$pipe = new $pipe;
}

// 一样是实例化的过程,然后分歧点出现了
// 第三个巡逻队的暗号是:老虎,而此时给出的却是:上山打野鸡
// 不包括“老虎”两个字

public function handle($value, Closure $closure)
{
var_dump('暗号:' . $this->keyword);

// 包含指定关键词的口令视为核对成功
if (strstr($value, $this->keyword) != false) {
return $closure($value);
}

return '口令核对失败';
}

// 不符合结果就直接返回了一个字符串“口令核对失败”
// 这个返回的值会被当做$stack的值
// 最后就跟递归函数一样层层返回,将“口令核对失败”作为array_reduce将数组简化的唯一值

// 也就是说,$response('上山打野鸡') 最后返回的是“口令核对失败”

// array_reduce 返回的是一个闭包,可以当做函数调用
$response = array_reduce(middlewares(), carry(), init());

// 传入一个用来验证的口令
$result = $response('上山打野鸡');

// 打印出验证结果
var_dump($result);

如果是传入正确的口令:上山打老虎呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 巡逻队C核对口令正确,就会继续把参数传给闭包
public function handle($value, Closure $closure)
{
var_dump('暗号:' . $this->keyword);

// 包含指定关键词的口令视为核对成功
if (strstr($value, $this->keyword) != false) {
return $closure($value);
}

return '口令核对失败';
}

// 此时三个巡逻队已经遍历完了,还记得一直传下来的$stack的值是什么吗?
// 答案是:init
// 你可以重新返回去查看第一步到第三步,只要是验证口令成功的时候,
// init 方法都会被当做下一个闭包传递下去,init 闭包即 $stack 的值
// 所以最终返回的 $stack 即 init 方法

注意!上面的 array_reduce 执行完毕后并不是真的执行了代码,而是返回一个层层嵌套的递归函数(闭包),只有在调用的时候才会一层一层的执行,因而最先调用的中间件反而会变成最后执行(栈结构先进后出),所以我们才会在最开始反转数组,以保证执行顺序。

至此,Laravel 中间件验证路由请求的原理也就搞清楚了。

捋顺之后只剩下久久的深思,一段简单的代码却蕴藏着如此精深的奥妙。

可是……写完了如此长篇的文章,我的框架的中间件却还没有开始着手……

为框架添加中间件

中间件原理

中间件其实跟路由的原理类似,即创建一个专门保存命名和映射关系的配置文件:

1
2
// 键值对数组的键即中间件名称,值即对应的中间件
'auth' => 'App\\Middleware\\AuthMiddleware'

只需要用一个简单的名称字符串即可映射到对应的中间件类。

由于一个路由可以有很多个中间件,所以路由配置里需要添加一个数组用来存储中间件的名称。

优化路由模块

在之前的设计中,Router 的 $routes 设计为静态变量,

其实只要修改 http_server.php 修改引入方式即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
require './vendor/autoload.php';
require_once './app/route/web.php';

$http = new Swoole\Http\Server('0.0.0.0', 9527);

$http->on('request', function ($request, $response) use ($router) {

var_dump('请求URI:' . $request->server['request_uri']);

$router->handle($request, $response);
});

$http->start();

而路由配置文件 web.php 只要返回 $router 即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php

$router = new \FireRabbitEngine\Module\Route\Router();

$router->setConfig([

'namespace' => 'App\\Controller\\Home\\',

])->group(function () use ($router) {

$router->get('/user', 'IndexController@index')->name('index');

});

return $router;

这样 Router 的 $routes 就不再需要设置为静态变量了。

路由添加中间件

在路由配置的时候,期望效果是可以通过如下两种方式配置中间件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php

$router = new \FireRabbitEngine\Module\Route\Router();

$router->setConfig([

'namespace' => 'App\\Controller\\Home\\',
'middleware' => ['auth']

])->group(function () use ($router) {

$router->get('/user', 'IndexController@index')->name('index')->middleware(['auth']);

});

return $router;

第一种是在分组的时候,配置全组共用的中间件,

第二种是在单个路由配置的时候,可以自定义该路由的中间件,

如果使用第二种方法,并且该路由在一个分组里,该路由不仅有分组的中间件,还有自己单独添加的中间件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php

$router = new \FireRabbitEngine\Module\Route\Router();

$router->setConfig([

'namespace' => 'App\\Controller\\Home\\',
'middleware' => ['auth']

])->group(function () use ($router) {

// 这个路由的中间件为:[auth, other]
$router->get('/user', 'IndexController@index')->middleware(['other']);

// 这个路由的中间件为:[auth]
$router->get('/admin', 'IndexController@index');

});

return $router;

上面的 /user 路由额外添加了一个中间件 other,而 /admin 路由不会受到影响。

中间件的合并顺序为:分组>自定义

即优先执行分组设置的全局中间件,然后再执行自定义中间件。

middleware 方法必须放在 get/post/any 方法之后。

修改 name 方法,让该方法也返回 $this,这样就可以链式调用了。

然后为 Router 添加 middleware 方法,该方法接收一个数组参数:

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
// 新增属性(全局中间件)
protected $middlewares = [];

/**
* 路由添加中间件
*
* @param array $middlewares
* @return Router
*/
public function middleware(array $middlewares)
{
if ($this->lastHandleRouteIndex === null) {
return $this;
}

// 合并中间件,优先级为:分组>单个路由自定义配置
$middlewares = array_merge($this->middlewares, $middlewares);
// 去除重复中间件
$middlewares = array_unique($middlewares);
// 找到最后一个添加的路由
$route = $this->routes[$this->lastHandleRouteIndex];
$route->middleware = $middlewares;

$this->routes[$this->lastHandleRouteIndex] = $route;
}

然后是分组的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 设置参数
* @param $key
* @param $value
*/
protected function createConfig($key, $value)
{
switch ($key) {
case 'namespace':
$this->namespace = $value;
break;
case 'middleware':
$this->middlewares = $value;
break;
}
}

分组配置时将中间件加入全局的中间件数组。

在调用结束的时候,应该把这个数组清空:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 路由分组
* @param $func
*/
public function group($func)
{
$func();

// 执行完成后将参数初始化
$this->namespace = '';
$this->middlewares = [];
}

给路由增加中间件的功能就完成了。

现在 RouteParams 路由配置对象里已经可以取到 middleware 属性的值了。

封装请求与响应

框架的请求和响应是 swoole 的对象,内置的方法无法满足框架的需求,

因此需要将请求和响应进行封装,在框架的 module 目录新建文件夹 Http,

Http 模块用于实现 Http 请求相关的处理类,新建两个类:Request 和 Response 用于封装请求和响应:

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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# Request.php

<?php

namespace FireRabbitEngine\Module\Http;

use FireRabbitEngine\Module\Route\RouteParams;

class Request
{
protected $request, $route;

public function __construct($request, $route)
{
$this->request = $request;
$this->route = $route;
}

public function getRequest()
{
return $this->request;
}

/**
* 获取路由
* @return mixed
*/
public function getRoute(): RouteParams
{
return $this->route;
}

/**
* 判断该请求是否ajax
* @return bool
*/
public function isAjax()
{
return 'XMLHttpRequest' == $this->request->header['x-requested-with'];
}

/**
* 获取get参数
* @param null $key
* @param null $default
* @return mixed
*/
public function getQueryParams($key = null, $default = null)
{
if ($key == null) {
return $this->request->get;
}

return isset($this->request->get[$key]) ? $this->request->get[$key] : $default;
}

/**
* 获取post参数
* @param null $key
* @param null $default
* @return mixed
*/
public function getPostParams($key = null, $default = null)
{
if ($key == null) {
return $this->request->post;
}

return isset($this->request->post[$key]) ? $this->request->post[$key] : $default;
}

/**
* 获取请求方法
* @return string
*/
public function getRequestMethod()
{
return $this->request->server['request_method'];
}

/**
* 获取请求IP地址
* @return string | null
*/
public function getRequestIP()
{
return $this->request->header['x-real-ip'] ?? null;
}

/**
* 获取请求头
* @param $key
* @return string | null
*/
public function getHeaders($key = null)
{
if ($key == null) {
return $this->request->header;
}

return $this->request->header[$key] ?? null;
}

/**
* 获取cookie
* @param $key
* @return string | null
*/
public function getCookies($key = null)
{
if ($key == null) {
return $this->request->cookie;
}

return $this->request->cookie[$key] ?? null;
}

/**
* 获取请求URI
* @return mixed
*/
public function getRequestURI()
{
return rtrim($this->request->server['request_uri'], '/');
}
}

Request 请求类实现了一些简单方法的封装,后续如有需求还可以继续扩展。

接下来创建 Response 响应类:

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
<?php
/**
* Created by PhpStorm
* Author:FireRabbit
* Date:2/12/21
* Time:11:31 AM
**/


namespace FireRabbitEngine\Module\Http;


class Response
{
protected $response;

public function __construct($response)
{
$this->response = $response;
}

public function getResponse()
{
return $this->response;
}

public function showMessage($message)
{
$this->response->header("Content-Type", "text/html; charset=utf-8");
$this->response->end($message);
}
}

这个类实现了一个简单的输出消息的方法,后续将会增加输出 view 和 API 类型的响应。

现在 Request 和 Response 都有了,但是每次都要分别取这两个对象不太方便,

于是我又定义了一个 Kernel(Http 请求核心类):

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
<?php

namespace FireRabbitEngine\Module\Http;

class Kernel
{
protected $request, $response;

public function __construct(Request $request, Response $response)
{
$this->request = $request;
$this->response = $response;
}

public function getRequest()
{
return $this->request->getRequest();
}

public function getResponse()
{
return $this->response->getResponse();
}

public function getHttpRequest()
{
return $this->request;
}

public function getHttpResponse()
{
return $this->response;
}
}

这个类可以取到 swoole 的请求和响应,也可以取到框架自定义的请求和响应。

这样就把请求和响应封装成一个 Http 核心类了。

中间件类

Laravel 的中间件不需要继承任何类,完全由用户自定义,

为了统一规范,我定义了一个中间件的父类:

1
2
3
4
5
6
7
8
9
10
<?php

namespace FireRabbitEngine\Module\Http;

use Closure;

abstract class Middleware
{
abstract public function handle(Kernel $kernel, Closure $next);
}

这里类只有一个抽象方法 handle,所有的中间件继承这个类实现统一的标准。

handle 第一个参数即上文封装的 kernel,在中间件里通过 kernel 来获取参数和返回响应。

再在博客项目的路径下,新建 app/middleware 用来存放中间件 TestMiddlewareA 和 TestMiddlewareB:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

namespace App\Middleware;

use Closure;
use FireRabbitEngine\Module\Http\Kernel;
use FireRabbitEngine\Module\Http\Middleware;

class TestMiddlewareA extends Middleware
{
public function handle(Kernel $kernel, Closure $next)
{
$request = $kernel->getHttpRequest();

if ($request->getQueryParams('a') == 1) {
$kernel->getHttpResponse()->showMessage('aa');
return null;
}

return $next($kernel);
}
}

TestMiddlewareB:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php

namespace App\Middleware;

use Closure;
use FireRabbitEngine\Module\Http\Kernel;
use FireRabbitEngine\Module\Http\Middleware;

class TestMiddlewareB extends Middleware
{

public function handle(Kernel $kernel, Closure $next)
{
$request = $kernel->getHttpRequest();

if ($request->getQueryParams('b') == 1) {
$kernel->getHttpResponse()->showMessage('bb');
return null;
}

return $next($kernel);
}
}

这两个中间件的逻辑非常简单,就是通过 get 参数来判断是否通过请求,

这样在测试的时候就很方便了,只要在路径上面修改参数即可看到中间件的效果。

中间件实际上可以不需要 return null,为了美观后面会对此处的代码进行优化。

中间件逻辑

前文通过 array_reduce 来演示 Laravel 中间件的处理逻辑,

现在就要把这个逻辑在框架中进行实现,在 Http 文件夹下新建一个 PipeLine 类:

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
<?php

namespace FireRabbitEngine\Module\Http;

use Closure;

class Pipeline
{
protected $pipes, $kernel;

public function send(Kernel $kernel)
{
$this->kernel = $kernel;

return $this;
}

public function through($pipes)
{
$this->pipes = $pipes;

return $this;
}

public function then(Closure $destination)
{
return array_reduce($this->pipes, $this->carry(), $this->dispatchRouter($destination));
}

function carry()
{
return function ($stack, $pipe) {

return function ($passable) use ($stack, $pipe) {

if ($pipe instanceof Closure) {
return $pipe($passable, $stack);
} elseif (!is_object($pipe)) {
$pipe = new $pipe;
}

return $pipe->handle($passable, $stack);
};
};
}

function dispatchRouter($destination)
{
return function ($passable) use ($destination) {
$destination($passable);
};
}
}

这个类就是用来处理中间件逻辑的地方,具体逻辑与前文“对口令游戏”一样。

这个类通过 send 方法接收上面封装好的 Http 核心类 Kernel,

Kernel 类具有获取请求参数和返回响应的权限,它会被传到中间件里。

中间件要根据请求参数判断是否符合条件,在中间件还可以直接返回响应

中间件的逻辑类也完成了,接下来就要修改 RouteParams 解析路由实例化控制器的地方。

将原来创建控制器实例的方法抽取出来,封装为 routeResponse:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 执行路由响应
* @return \Closure
*/
protected function routeResponse()
{
return function ($kernel) {

// 实例化类
$controllerName = $this->getFullControllerName();
$controllerObject = new $controllerName($kernel);
$this->uri = rtrim($this->request->server['request_uri'], '/');

$params = $this->getRouteParams();

// 执行方法时,路径参数作为方法的参数
call_user_func_array([$controllerObject, $this->action], $params);
};
}

这个方法返回的是一个闭包,也就是说返回值是一个匿名函数。

接下来修改原来的 createResponse 方法,现在可以直接实例化 PipeLine 来调用中间件:

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
/**
* 执行路由
* @param $request
* @param $response
*/
public function createResponse($request, $response)
{
// 判断请求方法是否正确
if ($this->method != RequestMethod::ANY && $request->server['request_method'] != $this->method) {
(new MethodErrorResponse())->response($request, $response, $this);
return;
}

// 判断方法是否存在
$controllerName = $this->getFullControllerName();
if (!class_exists($controllerName)) {
(new ClassNotFoundResponse())->response($request, $response, $this);
}

$action = $this->action;

// 不存在方法则返回404
if (!method_exists($controllerName, $action)) {
(new ActionNotFoundResponse())->response($request, $response, $this);
return;
}

$this->request = $request;
$this->response = $response;

// 测试用
$pipes = ['App\\middleware\\TestMiddlewareA', 'App\\middleware\\TestMiddlewareB'];

$pipeline = new Pipeline();

$kernel = new Kernel(new Request($request, $this), new Response($response));

$routeResponse = $pipeline->send($kernel)
->through(array_reverse($pipes))
->then($this->routeResponse());

$routeResponse($kernel);
}

上述代码中使用:

1
$pipes = ['App\\Middleware\\TestMiddlewareA', 'App\\middleware\\TestMiddlewareB'];

手动声明了两个中间件,然后访问任意路由就可以看到中间件的效果了。

测试之后发现中间件正常运行。

添加映射关系

框架现在没有中间件名称和类名的映射关系,所以才只能用上面的测试代码来调试。

接下来创建一个配置中间件映射关系的文件,在博客目录下创建 app/Middleware/Kernel.php:

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
<?php

namespace App\Middleware;

class Kernel
{
/**
* 实例化的中间件
*
* @var [Middleware]
*/
protected static $instances;

protected static $middlewares = [
'a' => TestMiddlewareA::class,
'b' => TestMiddlewareB::class,
];

public static function getMiddlewareInstance($name)
{
// 从已实例化的对象数组中取
if(isset(self::$instances[$name])) {
return self::$instances[$name];
}

// 未实例化的创建新对象
$middlewareName = self::$middlewares[$name] ?? null;

if($middlewareName == null) {
self::$instances[$name] = null;
} else {
self::$instances[$name] = new $middlewareName;
}

return self::$instances[$name];
}
}

在这里我用一个静态变量来保存实例化的中间件,因为中间件的对象是固定的,

没必要每次调用的时候都重新创建一次,一旦实例化之后就直接放进内存,这样可以提高效率。

这样就完成整个中间件的功能了。

测试中间件

编辑 web.php,添加两个测试路由:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php

$router = new \FireRabbitEngine\Module\Route\Router();

$router->setConfig([

'namespace' => 'App\\Controller\\Home\\',
'middleware' => ['a']

])->group(function () use ($router) {

// 这个路由的中间件为:[auth, other]
$router->get('/user', 'IndexController@index')->middleware(['b']);

// 这个路由的中间件为:[auth]
$router->get('/admin', 'IndexController@index');

});

return $router;

通过访问上述定义的路由,然后修改 a 和 b 参数的值即可看到中间件的拦截功能。

修改记录

中间件配置化

修改时间:2020-02-13 22:47

突然发现 PipeLine 方法调用 Kernel 类十分不合理。

框架的代码不应该依赖项目的代码,因此需要优化。

在 app/config 目录下创建 middleware.php 用来保存中间件的名称映射关系:

1
2
3
4
5
6
<?php

return [
'a' => App\Middleware\TestMiddlewareA::class,
'b' => App\Middleware\TestMiddlewareB::class,
];

接着将原本放在 app/Middleware 下面的 Kernel 删掉,

并且在框架 module/Http 目录新建一个 Middleware 目录,将 Middleware.php 移到这个目录下。

同时重新创建一个 Kernel 类:

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
<?php

namespace FireRabbitEngine\Module\Http\Middleware;

class Kernel
{
/**
* 实例化的中间件
*
* @var [Middleware]
*/
protected static $instances;

protected static $middlewares = [];

/**
* 读取配置文件
* @param $middlewares
*/
public static function setConfig($middlewares)
{
self::$middlewares = $middlewares;
}

public static function getMiddlewareInstance($name)
{
// 从已实例化的对象数组中取
if (isset(self::$instances[$name])) {
return self::$instances[$name];
}

// 未实例化的创建新对象
$middlewareName = self::$middlewares[$name] ?? null;

if ($middlewareName == null) {
self::$instances[$name] = null;
} else {
self::$instances[$name] = new $middlewareName;
}

return self::$instances[$name];
}
}

中间件的配置不再直接写在这个类里,而是通过 setConfig 读取配置参数。

接着在修改文件 http_server.php,加入一行代码:

1
\FireRabbitEngine\Module\Http\Middleware\Kernel::setConfig(require './app/config/middleware.php');

这样框架和项目之间就不再有直接的依赖关系了。

文章作者: 火烧兔子
文章链接: http://huotuyouxi.com/2021/02/12/my-swoole-framework-8/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 火兔游戏工作室