之前自己研究laravel5的管道代码,只能懵懂。后来又忘记了。
今天看到《Laravel框架关键技术解析》6.2.2 请求管道处理,讲解的比较详细,把复杂的管道简化后容易理解其处理逻辑。
为了加深印象,彻底理解管道模式,必须在本机写代码进行测试,并把代码做了些改进,方便理解。书里面是从最复杂的管道到最简化的管道。个人觉得还是从最简化版开始学会比较容易接收和消化。
学习方法:第一次学习的时候,光看代码没什么效果,最好复制代码去自己的机器上跑一跑。咱这行最重要的是实践,光学理论很容易忘记,也无法消化。
【简化版】pipe.php
<?php interface Step{ public static function go(Closure $next); } class FirstStep implements Step{ public static function go(Closure $next) { echo "第一步开始", nl2br(PHP_EOL); $next(); echo "第一步结束", nl2br(PHP_EOL); } } function goFun($step, $className){ return function () use ($step, $className){ return $className::go($step); }; } function then(){ $steps = [ 'FirstStep' ]; $prepare = function(){ echo "初始化开始", nl2br(PHP_EOL); }; $go = array_reduce($steps, 'goFun', $prepare); //获得一个回调函数 $go(); } then(); <<<OUTPUT 运行后输出: 第一步开始 初始化开始 第一步结束 OUTPUT
这个管道只有一个处理过程,最难理解也是最关键之一是 array_reduce($steps, "goFun", $prepare) 函数和 goFun($step, $className)函数。
其中 array_reduce() 函数估计大部分人都没用过。点击查看 array_reduce() 有关PHP手册介绍, array_reduce() 的第一个是要处理的数组,第二个是处理函数名称或回调函数,第三个参数为可选参数,为初始化参数,就是第一个被传入管道的“数据对象”,按装饰器模式解释就是“被装饰对象”。
goFun($step, $className) 函数中 $step 就是上一个或初始化传入的结果,$className,当前遍历的数组值。通过 array_reduce() 把数组 $steps 里面的类,迭代调用 goFun() 函数处理。而 goFun() 返回的是一个回调函数,所以相当于一个公式,把每个类的处理嵌套到另外一个匿名函数里面。类似“堆”。
下面的例子增加了2个执行步骤,更直观的看到执行顺序: 【中等版】pipe2.php
<?php /** * Created by PhpStorm. * User: peter * Date: 2017/5/12 * Time: 14:33 * * 管道原理总结: * 1、类似装饰器模式,自动按照顺序装饰第一个传入的对象。 * 2、每次装饰都是以回调函数的方式,代入下一个回调函数,等待最后统一执行。 * 3、回调函数“代入”后,就形成堆一样的结构(后进先出),最后“代入”的回调函数优先执行。 * laravel形象的把管道比喻成洋葱,先长出的层在里面,后长出的层在外面,但是拨的时候,必须从外面开始拨。 * 4、管道的执行顺序可以自己安排,不一定是正序或倒序执行,根据每个逻辑处理的优先情况, * 关键在 go() 执行的时候,是否让上一个回调函数先执行,如果让上一个回调函数先执行,则自己的逻辑将排在上一个回调函数的后面,反之在前面。 * */ define('SUOJIN', '     '); //计数器 final class count{ static $count = 0; static function showRunStep(){ return '排第 '.++self::$count.' 个执行:'; } } interface Step{ public static function go(Closure $next); } class FirstStep implements Step{ public static function go(Closure $next) { echo count::showRunStep(),str_repeat(SUOJIN, 1),"第一步开始", nl2br(PHP_EOL); //先自己执行 $next(); //再让上一个匿名函数执行 echo count::showRunStep(),str_repeat(SUOJIN, 1),"第一步结束", nl2br(PHP_EOL); //等上一个匿名函数执行完,再执行 } } class SecondStep implements Step{ public static function go(Closure $next) { echo count::showRunStep(),str_repeat(SUOJIN, 2),"第二步开始", nl2br(PHP_EOL); $next(); echo count::showRunStep(),str_repeat(SUOJIN, 2),"第二步结束", nl2br(PHP_EOL); } } function goFun($step, $className){ return function () use ($step, $className){ return $className::go($step); }; } function then(){ $steps = [ 'FirstStep', 'SecondStep', ]; $prepare = function(){ echo count::showRunStep(),"初始化开始", nl2br(PHP_EOL); }; //$steps = array_reverse($steps); //使用数组倒序后,可以优先执行 FirstStep $go = array_reduce($steps, 'goFun', $prepare); //获得一个回调函数(包含多层的匿名函数) $go(); } then(); 执行输出: <<<OUTPUT 排第 1 个执行: 第二步开始 排第 2 个执行: 第一步开始 排第 3 个执行:初始化开始 排第 4 个执行: 第一步结束 排第 5 个执行: 第二步结束 OUTPUT
【管道原理总结】:
1、类似装饰器模式,自动按照顺序装饰第一个传入的对象。
2、每次装饰都是以回调函数的方式,代入下一个回调函数,等待最后统一执行。
3、回调函数代入后,就形成堆一样的结构(后进先出),最后执行的回调函数优先执行。 laravel形象的把管道比喻成洋葱,先长出的层在里面,后长出的层在外面,但是拨的时候,必须从外面开始拨。
4、管道的执行顺序可以自己安排,不一定是正序或倒序执行,根据每个逻辑处理的优先情况, 关键在 go() 执行的时候,是否让上一个回调函数先执行,如果让上一个回调函数先执行,则自己的逻辑将排在上一个回调函数的后面,反之在前面。
最后在看看比较接近Laravel管道复制的模式例子(Laravel目前管道更复杂) 【复杂版】pipe3.php
<?php /** * Created by PhpStorm. * User: peter * Date: 2017/5/12 * Time: 13:43 * * 管道的执行顺序可以自己安排,不一定是正序或倒序执行,根据每个逻辑处理的优先情况 * */ define('SUOJIN', '     '); //计数器 final class count{ static $count = 0; static function showRunStep(){ return '排第 '.++self::$count.' 个执行:'; } } //接口 interface Middleware { public static function handle(Closure $next); } class VerifyCsrfToken implements Middleware { public static function handle(Closure $next) { echo count::showRunStep(),str_repeat(SUOJIN, 6),"[pipes排序:6]【VerifyCsrfToken】【即将执行上一个回调函数(初始化回调函数)】[验证 Csrf-Token]", nl2br(PHP_EOL); $next(); //执行上一个回调函数(初始化回调函数) } } class ShareErrorsFromSession implements Middleware { public static function handle(Closure $next) { echo count::showRunStep(),str_repeat(SUOJIN, 5),"[pipes排序:5]【ShareErrorsFromSession】【即将执行上一个回调函数(VerifyCsrfToken)】[如果 session 中有 errors 变量,则共享它]", nl2br(PHP_EOL); $next(); //执行上一个回调函数( pipes排序=6 ) } } class StartSession implements Middleware { public static function handle(Closure $next) { // TODO: Implement handle() method. echo count::showRunStep(),str_repeat(SUOJIN, 4),"[pipes排序:4]【StartSession】【即将执行上一个回调函数(ShareErrorsFromSession)】[开启 session,获取数据]", nl2br(PHP_EOL); $next(); //执行上一个回调函数(pipes排序=5) echo count::showRunStep(),str_repeat(SUOJIN, 4),"[pipes排序:4]【StartSession】[保存数据,关闭 session]", nl2br(PHP_EOL); } } class AddQueueCookiesToResponse implements Middleware { public static function handle(Closure $next) { // TODO: Implement handle() method. $next(); //先执行上一个:4(因为用 array_reverse 倒序了) echo count::showRunStep(),str_repeat(SUOJIN, 3),"[pipes排序:3]【AddQueueCookiesToResponse】【执行完(StartSession)后在执行本函数】[添加下一次需要的cookie]", nl2br(PHP_EOL); } } class EncryptCookies implements Middleware { public static function handle(Closure $next) { // TODO: Implement handle() method. echo count::showRunStep(),str_repeat(SUOJIN, 2),"[pipes排序:2]【EncryptCookies】【即将执行上一个回调函数(AddQueueCookiesToResponse)】[对输入请求的 cookie 进行解密]", nl2br(PHP_EOL); $next(); //先执行上一个:3(因为用 array_reverse 倒序了) echo count::showRunStep(),str_repeat(SUOJIN, 2),"[pipes排序:2]【EncryptCookies】【执行完(AddQueueCookiesToResponse)后在执行本函数】[对输出响应的 cookie 进行加密]", nl2br(PHP_EOL); } } class CheckForMaintenanceMode implements Middleware{ public static function handle(Closure $next) { // TODO: Implement handle() method. echo count::showRunStep(),str_repeat(SUOJIN, 1),"[pipes排序:1]【CheckForMaintenanceMode】【即将执行上一个回调函数(EncryptCookies)】[确定当前程序是否处于维护状态]", nl2br(PHP_EOL); $next(); } } function getSlice(){ /** * $stack 结果,就是回调函数的嵌套(堆) */ return function ($stack, $pipe){ return function() use ($stack, $pipe){ return $pipe::handle($stack); }; }; } function then(){ $pipes = [ "CheckForMaintenanceMode", "EncryptCookies", "AddQueueCookiesToResponse", "StartSession", "ShareErrorsFromSession", "VerifyCsrfToken", ]; $firstSlice = function(){ echo count::showRunStep(),"[初始化]请求向路由器传递,返回响应", nl2br(PHP_EOL); }; $pipes = array_reverse($pipes); //数组倒序排 call_user_func( array_reduce($pipes, getSlice(), $firstSlice) ); } then();
排第 1 个执行: [pipes排序:1]【CheckForMaintenanceMode】【即将执行上一个回调函数(EncryptCookies)】[确定当前程序是否处于维护状态] 排第 2 个执行: [pipes排序:2]【EncryptCookies】【即将执行上一个回调函数(AddQueueCookiesToResponse)】[对输入请求的 cookie 进行解密] 排第 3 个执行: [pipes排序:4]【StartSession】【即将执行上一个回调函数(ShareErrorsFromSession)】[开启 session,获取数据] 排第 4 个执行: [pipes排序:5]【ShareErrorsFromSession】【即将执行上一个回调函数(VerifyCsrfToken)】[如果 session 中有 errors 变量,则共享它] 排第 5 个执行: [pipes排序:6]【VerifyCsrfToken】【即将执行上一个回调函数(初始化回调函数)】[验证 Csrf-Token] 排第 6 个执行:[初始化]请求向路由器传递,返回响应 排第 7 个执行: [pipes排序:4]【StartSession】[保存数据,关闭 session] 排第 8 个执行: [pipes排序:3]【AddQueueCookiesToResponse】【执行完(StartSession)后在执行本函数】[添加下一次需要的cookie] 排第 9 个执行: [pipes排序:2]【EncryptCookies】【执行完(AddQueueCookiesToResponse)后在执行本函数】[对输出响应的 cookie 进行加密]
把 pipe3.php 代码修改,直接打印:
var_dump(array_reduce($pipes, getSlice(), $firstSlice));die;
直接输出匿名函数的嵌套结构:
object(Closure)#8 (1) { ["static"]=> array(2) { ["stack"]=> object(Closure)#7 (1) { ["static"]=> array(2) { ["stack"]=> object(Closure)#6 (1) { ["static"]=> array(2) { ["stack"]=> object(Closure)#5 (1) { ["static"]=> array(2) { ["stack"]=> object(Closure)#4 (1) { ["static"]=> array(2) { ["stack"]=> object(Closure)#3 (1) { ["static"]=> array(2) { ["stack"]=> object(Closure)#1 (0) { } ["pipe"]=> string(15) "VerifyCsrfToken" } } ["pipe"]=> string(22) "ShareErrorsFromSession" } } ["pipe"]=> string(12) "StartSession" } } ["pipe"]=> string(25) "AddQueueCookiesToResponse" } } ["pipe"]=> string(14) "EncryptCookies" } } ["pipe"]=> string(23) "CheckForMaintenanceMode" } }
这段代码就是执行嵌套好的匿名函数,类似 laravel 里面的 PipeLine->then()
call_user_func( array_reduce($pipes, getSlice(), $firstSlice) );
注意:laravel 的 handel() (匿名函数)是有2个参数的,第一个参数是 传入的数据,第二个是上一个匿名函数。本文例子只有一个参数(匿名函数)。
推荐其他文章:
如何使用 Laravel 管道:
https://www.jianshu.com/p/09948badf244
https://learnku.com/articles/2769/laravel-pipeline-realization-of-the-principle-of-single-component