热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

ThinkPHP6.0管道模式与中间件的实现分析

说明ThinkPHP6.0RC5开始使用了管道模式来实现中间件,比起之前版本的实现更加简洁、有序。这篇文章对其实现细节进行分析。首先我们从入口文件publicindex.php开始






说明

ThinkPHP 6.0 RC5 开始使用了管道模式来实现中间件,比起之前版本的实现更加简洁、有序。这篇文章对其实现细节进行分析。

首先我们从入口文件public/index.php开始,$http = (new App())->http;

获得一个http类的实例后调用它的run方法:$respOnse= $http->run();,然后它的run方法又调用了runWithRequest方法:

protected function runWithRequest(Request $request)
{
.
.
.
return $this->app->middleware->pipeline()
->send($request)
->then(function ($request) {
return $this->dispatchToRoute($request);
});
}

中间件的执行都在最后的return语句中。


pipeline、through、send方法

$this->app->middleware->pipeline()pipeline方法:

public function pipeline(string $type = 'global')
{
return (new Pipeline())
// array_map将所有中间件转换成闭包,闭包的特点:
// 1. 传入参数:$request,请求实例; $next,一个闭包
// 2. 返回一个Response实例
->through(array_map(function ($middleware) {
return function ($request, $next) use ($middleware) {
list($call, $param) = $middleware;
if (is_array($call) && is_string($call[0])) {
$call = [$this->app->make($call[0]), $call[1]];
}
// 该语句执行中间件类实例的handle方法,传入的参数是外部传进来的$request和$next
// 还有一个$param是中间件接收的参数
$respOnse= call_user_func($call, $request, $next, $param);
if (!$response instanceof Response) {
throw new LogicException('The middleware must return Response instance');
}
return $response;
};
// 将中间件排序
}, $this->sortMiddleware($this->queue[$type] ?? [])))
->whenException([$this, 'handleException']);
}

through方法代码:

public function through($pipes)
{
$this->pipes = is_array($pipes) ? $pipes : func_get_args();
return $this;
}

前面调用through是传入的array_map(...)把中间件封装为一个个闭包,through则是把这些闭包保存在Pipeline类的$pipes属性中。

PHP的array_map方法签名:

array_map ( callable $callback , array $array1 [, array $... ] ) : array

$callback迭代作用于每一个 $array的元素,返回新的值。所以,最后得到$pipes中每个闭包的形式特征是这样的(伪代码):

function ($request, $next) {
$respOnse= handle($request, $next, $param);
return $response;
}

该闭包接收两个参数,一个是请求实例,一个是回调用函数,handle方法处理后得到相应并返回。

through返回一个Pipeline类的实例,接着调用send方法:

public function send($passable)
{
$this->passable = $passable;
return $this;
}

该方法很简单,只是将传入的请求实例保存在$passable成员变量,最后同样返回Pipeline类的实例,这样就可以链式调用Pipeline类的其他方法。


then,carry方法

send方法之后,接着调用then方法:

return $this->app->middleware->pipeline()
->send($request)
->then(function ($request) {
return $this->dispatchToRoute($request);
});

这里的then接收一个闭包作为参数,这个闭包实际上包含了控制器操作的执行代码。
then方法代码:

public function then(Closure $destination)
{
$pipeline = array_reduce(
//用于迭代的数组(中间件闭包),这里将其倒序
array_reverse($this->pipes),
// array_reduce需要的回调函数
$this->carry(),
//这里是迭代的初始值
function ($passable) use ($destination) {
try {
return $destination($passable);
} catch (Throwable | Exception $e) {
return $this->handleException($passable, $e);
}
});
return $pipeline($this->passable);
}

carry代码:

protected function carry()
{
// 1. $stack 上次迭代得到的值,如果是第一次迭代,其值是后面的「初始值
// 2. $pipe 本次迭代的值
return function ($stack, $pipe) {
return function ($passable) use ($stack, $pipe) {
try {
return $pipe($passable, $stack);
} catch (Throwable | Exception $e) {
return $this->handleException($passable, $e);
}
};
};
}

为了更方便分析原理,我们把carry方法内联到then中去,并去掉错误捕获的代码,得到:

public function then(Closure $destination)
{
$pipeline = array_reduce(
array_reverse($this->pipes),
function ($stack, $pipe) {
return function ($passable) use ($stack, $pipe) {
return $pipe($passable, $stack);
};
},
function ($passable) use ($destination) {
return $destination($passable);
});
return $pipeline($this->passable);
}

这里关键是理解array_reduce以及$pipeline($this->passable)的执行过程,这两个过程可以类比于「包洋葱」和「剥洋葱」的过程。
array_reduce第一次迭代,$stack初始值为:
(A)

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

回调函数的返回值为:
(B)

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

将A代入B可以得到第一次迭代之后的$stack的值:
(C)

function ($passable) use ($stack, $pipe) {
return $pipe($passable,
function ($passable) use ($destination) {
return $destination($passable);
})
);
};

第二次迭代,同理,将C代入B可得:
(D)

// 伪代码
// 每一层的$pipe都代表一个中间件闭包
function ($passable) use ($stack, $pipe) {
return $pipe($passable, //倒数第二层中间件
function ($passable) use ($stack, $pipe) {
return $pipe($passable, //倒数第一层中间件
function ($passable) use ($destination) {
return $destination($passable); //包含控制器操作的闭包
})
);
};
);
};

以此类推,有多少个中间件,就代入多少次,最后一次得到$stack就返回给$pipeline。由于前面对中间件闭包进行了倒序,排在前面的闭包被包裹在更里层,所以倒序后的闭包越是后面的在外面,从正序来看,则变成越前面的中间件在最外层。

层层包裹好闭包后,我们得到了一个类似洋葱结构的「超级」闭包D,该闭包的结构如上面的代码注释所示。最后把$request对象传给这个闭包,执行它:$pipeline($this->passable);,由此开启一个类似剥洋葱的过程,接下来我们看看这洋葱是怎么剥开的。


剥洋葱过程分析

回顾上文,array_map(...)把每一个中间件类加工成一个类似这种结构的闭包:

function ($request, $next) {
$respOnse= handle($request, $next, $param);
return $response;
}

其中handle是中间件中的入口,其结构特点是这样的:

public function handle($request, $next, $param) {
// do sth ------ M1-1 / M2-1
$respOnse= $next($request);
// do sth ------ M1-2 / M2-2
return $response;
}

我们上面的「洋葱」一共只有两层,也就是有两层中间件的闭包,假设M1-1,M1-2分别是第一个中间件handle方法的前置和后值操作点位,第二个中间件同理,是M2-1,M2-2。现在,让程序执行$pipeline($this->passable),展开来看,也就是执行:

// 伪代码
function ($passable) use ($stack, $pipe) {
return $pipe($passable,
function ($passable) use ($stack, $pipe) {
return $pipe($passable,
function ($passable) use ($destination) {
return $destination($passable);
})
);
};
);
}($this->passable)

此时,程序要求从:

return $pipe($passable,
function ($passable) use ($stack, $pipe) {
return $pipe($passable,
function ($passable) use ($destination) {
return $destination($passable);
})
);
};
);

返回值,也就是要执行第一个中间件闭包,$passable对应handle方法的$request参数,而下一层闭包

function ($passable) use ($stack, $pipe) {
return $pipe($passable,
function ($passable) use ($destination) {
return $destination($passable);
})
);
}

则对应handle方法的$next参数。

要执行第一个闭包,即要执行第一个闭包的handle方法,其过程是:首先执行M1-1点位的代码,即前置操作,然后执行$respOnse= $next($request);,这时程序进入执行下一个闭包,$next($request)展开来,也就是:

function ($passable) use ($stack, $pipe) {
return $pipe($passable,
function ($passable) use ($destination) {
return $destination($passable);
})
);
}($request)

依次类推,执行该闭包,即执行第二个中间件的handle方法,此时,先执行M2-1点位,然后执行$respOnse= $next($request),此时的$next闭包是:

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

属于洋葱之芯——最里面的一层,也就是包含控制器操作的闭包,展开来看:

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

最终,我们从return $destination($passable)中返回一个Response类的实例,也就是,第二层的$respOnse= $next($request)语句成功得到了结果,接着执行下面的语句,也就是M2-2点位,最后第二层闭包返回结果,也就是第一层闭包的$respOnse= $next($request)语句成功得到了结果,然后执行这一层闭包该语句后面的语句,即M1-2点位,该点位之后,第一层闭包也成功返回结果,于是,then方法最终得到了返回结果。

整个过程过来,程序经过的点位顺序是这样的:M1-1→M2-1→控制器操作→M2-2→M1-2→返回结果。


总结

整个过程看起来虽然复杂,但不管中间件有多少层,只要理解了前后两层中间件的这种递推关系,洋葱是怎么一层层剥开又一层层返回的,来多少层都不在话下。




thinkphp


推荐阅读
  • Spring源码解密之默认标签的解析方式分析
    本文分析了Spring源码解密中默认标签的解析方式。通过对命名空间的判断,区分默认命名空间和自定义命名空间,并采用不同的解析方式。其中,bean标签的解析最为复杂和重要。 ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • 本文讨论了使用差分约束系统求解House Man跳跃问题的思路与方法。给定一组不同高度,要求从最低点跳跃到最高点,每次跳跃的距离不超过D,并且不能改变给定的顺序。通过建立差分约束系统,将问题转化为图的建立和查询距离的问题。文章详细介绍了建立约束条件的方法,并使用SPFA算法判环并输出结果。同时还讨论了建边方向和跳跃顺序的关系。 ... [详细]
  • 知识图谱——机器大脑中的知识库
    本文介绍了知识图谱在机器大脑中的应用,以及搜索引擎在知识图谱方面的发展。以谷歌知识图谱为例,说明了知识图谱的智能化特点。通过搜索引擎用户可以获取更加智能化的答案,如搜索关键词"Marie Curie",会得到居里夫人的详细信息以及与之相关的历史人物。知识图谱的出现引起了搜索引擎行业的变革,不仅美国的微软必应,中国的百度、搜狗等搜索引擎公司也纷纷推出了自己的知识图谱。 ... [详细]
  • 本文介绍了Oracle数据库中tnsnames.ora文件的作用和配置方法。tnsnames.ora文件在数据库启动过程中会被读取,用于解析LOCAL_LISTENER,并且与侦听无关。文章还提供了配置LOCAL_LISTENER和1522端口的示例,并展示了listener.ora文件的内容。 ... [详细]
  • 怀疑是每次都在新建文件,具体代码如下 ... [详细]
  • sklearn数据集库中的常用数据集类型介绍
    本文介绍了sklearn数据集库中常用的数据集类型,包括玩具数据集和样本生成器。其中详细介绍了波士顿房价数据集,包含了波士顿506处房屋的13种不同特征以及房屋价格,适用于回归任务。 ... [详细]
  • Python正则表达式学习记录及常用方法
    本文记录了学习Python正则表达式的过程,介绍了re模块的常用方法re.search,并解释了rawstring的作用。正则表达式是一种方便检查字符串匹配模式的工具,通过本文的学习可以掌握Python中使用正则表达式的基本方法。 ... [详细]
  • 本文讨论了clone的fork与pthread_create创建线程的不同之处。进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合。在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性。但是二者之间的通讯需要通过专门的通讯机制,另外通过fork创建子进程系统开销很大。因此,在某些情况下,使用clone或pthread_create创建线程可能更加高效。 ... [详细]
  • 深入理解Kafka服务端请求队列中请求的处理
    本文深入分析了Kafka服务端请求队列中请求的处理过程,详细介绍了请求的封装和放入请求队列的过程,以及处理请求的线程池的创建和容量设置。通过场景分析、图示说明和源码分析,帮助读者更好地理解Kafka服务端的工作原理。 ... [详细]
  • 李逍遥寻找仙药的迷阵之旅
    本文讲述了少年李逍遥为了救治婶婶的病情,前往仙灵岛寻找仙药的故事。他需要穿越一个由M×N个方格组成的迷阵,有些方格内有怪物,有些方格是安全的。李逍遥需要避开有怪物的方格,并经过最少的方格,找到仙药。在寻找的过程中,他还会遇到神秘人物。本文提供了一个迷阵样例及李逍遥找到仙药的路线。 ... [详细]
  • 本文介绍了操作系统的定义和功能,包括操作系统的本质、用户界面以及系统调用的分类。同时还介绍了进程和线程的区别,包括进程和线程的定义和作用。 ... [详细]
  • 本文讨论了微软的STL容器类是否线程安全。根据MSDN的回答,STL容器类包括vector、deque、list、queue、stack、priority_queue、valarray、map、hash_map、multimap、hash_multimap、set、hash_set、multiset、hash_multiset、basic_string和bitset。对于单个对象来说,多个线程同时读取是安全的。但如果一个线程正在写入一个对象,那么所有的读写操作都需要进行同步。 ... [详细]
  • 本文介绍了Codeforces Round #321 (Div. 2)比赛中的问题Kefa and Dishes,通过状压和spfa算法解决了这个问题。给定一个有向图,求在不超过m步的情况下,能获得的最大权值和。点不能重复走。文章详细介绍了问题的题意、解题思路和代码实现。 ... [详细]
  • 本文介绍了在Android开发中使用软引用和弱引用的应用。如果一个对象只具有软引用,那么只有在内存不够的情况下才会被回收,可以用来实现内存敏感的高速缓存;而如果一个对象只具有弱引用,不管内存是否足够,都会被垃圾回收器回收。软引用和弱引用还可以与引用队列联合使用,当被引用的对象被回收时,会将引用加入到关联的引用队列中。软引用和弱引用的根本区别在于生命周期的长短,弱引用的对象可能随时被回收,而软引用的对象只有在内存不够时才会被回收。 ... [详细]
author-avatar
捕风的默小墨
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有