Laravel 5.4 管道模式(pipeLine)由浅入深学习和理解

原创 Laravel
阅读数: 406 2017年05月12日

之前自己研究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', '&nbsp&nbsp&nbsp&nbsp&nbsp');
//计数器
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', '&nbsp&nbsp&nbsp&nbsp&nbsp');

//计数器
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


phpriji.cn | 网站地图 | 沪ICP备17015433号-1