Pipeline in Laravel

904 阅读2分钟
原文链接: www.xiaoxiao.work

从管道说起

Pipeline是管道的意思。比如作为一个合格的韭菜,我们首先需要读小学、初中、高中等等,每个阶段你都会被修剪处理保证最后的产品是符合社会预期的,然后毕业了你就可以被收割了。

上面的小学、初中、高中就是管子,韭菜就是工序上需要处理的东西。反映到Laravel中,各种内置中间件、自定义中间价都是管子,每一个Request都是需要处理的东西。

这里我们先不探讨Laravel中的设计,我们先来看array_reduce这个函数,最常见的例子就是求和。

array_reduce([1,2,3], function($carry, $item) {
        return $carry + $item;
}, 0)

上面的例子返回了6,简单而富有表现力。再来下面的代码

    $a = function() {
        echo 'a' . PHP_EOL;
    };

    $b = function() use($a) {
        echo 'b' . PHP_EOL;
        $a();
    };

    $c = function() use($b) {
        echo 'c' . PHP_EOL;
        $b();
    };

    var_dump($c);
    $c();

以上代码会输出如下内容,简单来说就是形成了嵌套的闭包。调用结果显示,最后面的闭包是最先执行的,最开始的闭包是最后执行的,这个行为类似于栈。

object(Closure)#3 (1) {
  ["static"]=>
  array(1) {
    ["b"]=>
    object(Closure)#2 (1) {
      ["static"]=>
      array(1) {
        ["a"]=>
        object(Closure)#1 (0) {
        }
      }
    }
  }
}
c
b
a

再来看下面的结构

    /*定义一个 中间件接口*/
    interface Middleware
    {
        public static function handle(Closure $next); 
    }
    
    /*定义一个 csrf验证类*/
    class VerifyCsrfToken implements Middleware
    {
        public static function handle(Closure $next)
        {
            echo "验证Csrf-Token"."<br/> \n";
            $next();
        }   
            
    }
    
    /*定义一个 错误分享类*/
    class ShareErrorsFromSession implements Middleware
    {
        public static function handle(Closure $next)
        {
            echo "如果session中有'errors'变量,则共享它"."<br/> \n";
            $next();
        }
    }
    
    /*定义一个 开启session类*/
    class StartSession implements Middleware
    {
        public static function handle(Closure $next)
        {
            echo "开启session,获取数据"."<br/> \n";
            $next();
            echo "报错数据,关闭session"."<br> \n";
        }
    }
    
    /*定义一个 添加cookie队列至响应 类*/
    class AddQueuedCookiesToResponse implements Middleware
    {
        public static function handle(Closure $next)
        {
            $next();
            echo "添加下一次请求需要的cookie"."<br/> \n";
        }
    }
    
    /*定义一个 加密cookie类*/
    class EncryptCookies implements  Middleware
    {
        public static function handle(Closure $next)
        {
            echo "对输入请求的cookie进行解密"."<br/> \n";
            $next();
            echo "对输出响应的cookie进行加密"."<br/> \n";
        }
    }
    
    /*定义一个 检测维护状态类*/
    class ChecKForMaintenanceMode implements Middleware
    {
        public static function handle(Closure $next)
        {
            echo "确定当前程序是否处于维护状态"."<br/> \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 "请求向路由器传递,返回响应"."<br/> \n";
        };
        // print_r($pipes);
        $pipes = array_reverse($pipes);  
        $reduce = array_reduce($pipes, getSlice(), $firstSlice);
        // var_dump($reduce);
        call_user_func($reduce); 
    }

以上例子会有如下输出:

确定当前程序是否处于维护状态<br/> 
对输入请求的cookie进行解密<br/> 
开启session,获取数据<br/> 
如果session中有'errors'变量,则共享它<br/> 
验证Csrf-Token<br/> 
请求向路由器传递,返回响应<br/> 
保存数据,关闭session<br> 
添加下一次请求需要的cookie<br/> 
对输出响应的cookie进行加密<br/>

重新审视Laravel的Pipeline设计

再回头看LaravelPipeline设计,和我们上面的例子差别不大。其中carry函数的设计更加复杂些。

protected function carry()
    {
        return function ($stack, $pipe) {
            return function ($passable) use ($stack, $pipe) {
                if (is_callable($pipe)) {
                    //如果管道是闭包 就直接调用
                    return $pipe($passable, $stack);
                } elseif (! is_object($pipe)) {
                    list($name, $parameters) = $this->parsePipeString($pipe);

                    // 如果管道不是对象,例如Laravel中的can:update,post 这类用字符串表示,但是背后是绑定的中间件的东西,从容器中拿到实例然后组成参数。
                    $pipe = $this->getContainer()->make($name);

                    $parameters = array_merge([$passable, $stack], $parameters);
                } else {
                    // 管道是对象,直接组成参数使用即可。
                    $parameters = [$passable, $stack];
                }

                // 调用
                $response = method_exists($pipe, $this->method)
                                ? $pipe->{$this->method}(...$parameters)
                                : $pipe(...$parameters);

                // 返回
                return $response instanceof Responsable
                            ? $response->toResponse($this->container->make(Request::class))
                            : $response;
            };
        };
    }

整个Laravel Http Kernal其实就是使用的管道处理的Request

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());
}

最后 我还使用 Js 实现了一个类似的设计