Pipeline 管道操作实现请求中间件过滤(最详细讲解)

未匹配的标注

简介

通过 8 章的内容,我们终于或多或少,或粗略或详细的把 Laravel 引导启动给写完了。无论内容详细与否,相信看到的童鞋,一定会有所收获。Laravel 框架为什么深受欢迎与追捧,我想与它严谨的程序工程设计思想分不开,就好比我们的电脑要启动,一定有引导和初始化;Laravel 在这方面从不懈怠,我们能从中学到很多。

上面一堆废话,大家左眼进右眼就行啦哈。下面我们就进入 Laravel 中最牛逼也是最核心的操作之一: Pipeline 管道操作。

为什么说他牛逼呢,因为它用了一卡车的闭包和回调,如果学会了,对 PHP 闭包的理解就会进入一个更深的层次。

Pipeline 管道操作大体过程

我们先从管道操作开始的代码看起

protected function sendRequestThroughRouter($request)
{
    $this->app->instance('request', $request);

    Facade::clearResolvedInstance('request');

    // 前 8 章,就围绕这个简单 bootstrap 方法,疯狂展开。。。
    $this->bootstrap();

    // 这一节我们开始疯狂展开下面的这些代码
    return (new Pipeline($this->app))
                ->send($request)
                ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
                ->then($this->dispatchToRouter());
}

看这个链式调用,我们捋一下思路。首先实例化管道(Pipeline)类,将容器对象传入构造函数,返回管道对象;然后调用管道对象的 send(送) 方法,以 request(请求) 对象为参数(即把 requset 送入管道,准备穿透中间件);再然后,调用对象的 through(穿过)方法,以中间件数组为参数;最后,如果请求(经过过滤的)成功过来了,调用管道对象的 then 方法,将请求派遣分配的路由中。

注:在路由中,还有一套中间件过滤,其中一部分就是我们自行定义的。上面这部分中间件过滤,是对全局请求的过滤。当路由中的中间件过滤完毕后,就进入我们定义的控制器部分了。

下面我们进入展开细看阶段

  • 第一步,实例化管道类,主要看构造函数

    public function __construct(Container $container = null)
    {
      $this->container = $container;
    }

    咳咳!!(尴尬),这个好简单。。。

  • 第二步,调用 send 方法,以 request 为参

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

    这个额,注意 $this->passable 就是 request 对象,以后看到,要注意。

  • 第三步,调用 through 方法

    调用之前,我们先会执行参数位置的三元运算,结果返回的是 $this->middleware,因为这个三元判断的是中间件操作是否有关闭,默认是开启的哈,也一定会开启,关闭那不是扯淡吗

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

    注意:$this->pipes 就是上面的 $this->middleware ,他们是数组类型,我们来看一下这个数组有什么吧

    protected $middleware = [
      \App\Http\Middleware\CheckForMaintenanceMode::class,
      \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
      \App\Http\Middleware\TrimStrings::class,
      \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
      \App\Http\Middleware\TrustProxies::class,
    ];

    看到没,就是 5 个类,待会请求会穿透这个类。具体穿透方式就是:实例化 5 个类,依次倒序(重点!!!倒序)调用实例化的 handle 方法。

  • 第四步,调用 then 方法,以 $this->dispatchToRouter() 返回值为参

    最后一步是最重要,也是最难理解的一步。因为里面涉及实现穿透中间件的原理的实现方式,主要利用返回多个闭包,逐次循环调用,比较难以理解。我会尽最大可能讲解明白。下面我们用新章节段单独讲解

then 方法里面到底有什么

先看源码

public function then(Closure $destination)  // $destination 参数是 $this->dispatchToRouter() 返回的闭包
{
    // 下面这个 array_reduce 函数(PHP内置的),是实现穿透的主要函数。这个函数作用详解下面单独讲。
    $pipeline = array_reduce(
        // array_reverse 函数(PHP内置)作用很简单:就是将索引数组的顺序颠倒一下。
        array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination)
    );

    // 执行请求穿透。
    return $pipeline($this->passable);
}

先简单说一下:

上面方法的第一段 $pipeline = array_reduce(... 用形象一点说法就是:将最终出口用洋葱皮一层一层的包起来。

上面方法的第二段 return $pipeline($this->passable) 用形象一点的说法就是:用请求穿透洋葱皮,然后洋葱皮一层一层的被剥落,最终请求出去了(进入程序下一段)

  • $destination 到底是什么鬼

    我们先看一下参数 $this->dispatchToRouter() 返回的是什么鬼吧

    protected function dispatchToRouter()
    {
      return function ($request) {
          $this->app->instance('request', $request);
    
          return $this->router->dispatch($request);
      };
    }

    明白了,返回了一个函数(也称闭包对象),这个函数需要一个参数(实际就是那个请求对象)。这个函数还不执行,只是当做一个变量被扔沙包一样,扔来扔去,它肯定想:我什么时候能爆发我的小宇宙(执行内部的代码)。所以,我们作为人,现在可以完全不理 function 里面是什么鬼。我们就知道它是个沙包(参数),可以传来传去。最后,如果我们看到这个沙包在某一个地方后面出现一对小括号,并且里面有个参数(请求对象),这时候我们知道它的小宇宙爆发,我们可以进入这个小宇宙一探究竟了。

  • array_reduce 内置函数的作用。。

    官方说明:用回调函数迭代地将数组简化为单一的值。

    这句话什么鬼,我怎么这么晕,我在哪,我要去哪里????

    不用担心我们先换句形象的一段内容:

    大家都知道洋葱是什么吧。洋葱皮是一层一层的大家没有意见吧。剥洋葱的动作大家都能想象吧(不就是一层一层剥皮吧)。假设现在我们对剥洋葱进行逆操作:首先我们得有一个洋葱芯($destination),然后还得有洋葱皮们(颠倒后的 $this->pipes);然后我们还得需要包葱皮操作呀:其实就是上面的 $this->carry() 方法的返回值(注意是返回值,不是 $this->carry() 本身,是它的返回值),我们知道 array_reduce 函数的包葱皮操作是一个沙包(这个沙包会在 array_reduce 函数体内爆发,执行包这个动作),所以,$this->carry() 返回值,一定是一个沙包;我们看 PHP 官方文档的时候知道这个实现了包葱皮的沙包,需要两个参数 $carry$item ,这两个是什么呢?$carry 就是我们手上的洋葱(它正在被洋葱皮包起来),$item 就是我们手上的洋葱皮(它从数组中逐次拿出来的,然后一层层包在我们手上的洋葱上)。那么数组的洋葱皮用完的,我们会得到什么?这不是废话吗,当然得到已经包好的洋葱啦。这就是上面官方说的:用回调函数(包葱皮动作)迭代地将数组(洋葱皮们)简化为单一的值(最后包好的洋葱)

    进一步细讲:关于洋葱芯($destination)还需要做进一步加工才能作为芯被皮包起来。这个加工就是 $this->prepareDestination($destination),我们看一下源码

    protected function prepareDestination(Closure $destination)
    {
      return function ($passable) use ($destination) {
          return $destination($passable);
      };
    }

    哇。。又返回一个沙包。。。这个沙包有个 use 哎,哦我知道了,就是小宇宙中加上外部来的神奇元素,可以让爆发更牛逼一些。行了,看着逻辑我知道了,这是一个连锁爆发。即:如果返回的这个沙包爆发了(后面跟上小括号),内部的另一个沙包 $destination 紧接着爆发。这个内部的 $destination 沙包原来就是 use 引进来的神奇元素呀。。原来如此。

    管它呢现在经过 $this->prepareDestination($destination) 加工返回就是一个沙包,它没爆发,先不管它里面是什么鬼。。

  • 我们来看包洋葱动作吧。。

    !! carry 方法是子类的方法!!

    Illuminate\Routing\Pipeline 类,而非父类 Illuminate\Pipeline\Pipeline!!

    protected function carry()
    {
      return function ($stack, $pipe) {  // 这句 return 返回了 array_reduce 所需的闭包参数。此闭包将在 array_reduce 中被执行。
          return function ($passable) use ($stack, $pipe) {  // 上一个闭包在 array_reduce 中被执行后,返回这个闭包。这个才是真正被洋葱皮不断包着的洋葱。
              try {
                  $slice = parent::carry();
    
                  $callable = $slice($stack, $pipe);
    
                  return $callable($passable);
              } catch (Exception $e) {
                  return $this->handleException($passable, $e);
              } catch (Throwable $e) {
                  return $this->handleException($passable, new FatalThrowableError($e));
              }
          };
      };
    }

    还记得我刚才说的吧,是 $this->carry() 返回值是真正包洋葱的动作,即 return function ($stack, $pipe) {...} 这个沙包。上面我说过,包洋葱动作需要两个参数,一个是正在包的洋葱($stack),一个是洋葱皮($pipe)。

    那么,经过 array_reduce 函数执行包这个动作(爆发沙包的小宇宙),实际就是调用 function ($stack, $pipe) {...} 函数,这个函数的返回值就是包好当前洋葱皮的洋葱,而这个洋葱将参与下一个包葱皮的行为,这个行为同样是这个闭包。

    下面我们看一下怎么包的。第二行代码就是包的动作 return function ($passable) use ($stack, $pipe) {...} 返回了一个沙包(真正的洋葱),从沙包的参数和引入的神奇变量我们知道,当洋葱被一层层剥落的时候,执行的动作与 $passable (请求对象)、$stack (剥去当前层葱皮的洋葱)、$pipe (中间件)三个参数有关。

    我就明说吧,大家在中间件 handle 方法中所看到的 $request$next 对应的就是 $passable$stack。那么 $pipe 跑哪去了。呵呵,$pipe 就是 handle 方法所在的类呀。。

    现在我们看一眼把 $this->middleware (那 5 个洋葱皮)包好的洋葱的数据结构

    file

  • 下面我们来看一下剥洋葱的过程

    return $pipeline($this->passable); 是从这里开始的。$pipeline 是最终包好那个洋葱,这是一个沙包,哇。后面跟了一个小括号,爆发啦。。。

    爆发代码:

    !! 从子类的 carry 方法开始爆发!!

    protected function carry()
    {
      return function ($stack, $pipe) {  // 上面讲了,这个闭包会在 array_reduce 被执行掉
          return function ($passable) use ($stack, $pipe) {  // 而这个就通过 return $pipeline($this->passable) 以及 $next($request) 被不断的执行掉,从而不断过滤 $request 请求。
    
              // 爆发从这里开始。。。
              try {
    
                  // 这个才是调用父类 carry 方法的地方,返回的是 return function ($stack, $pipe) {...}
                  $slice = parent::carry();
    
                  // 这行执行了 function ($stack, $pipe) {...},返回了 function ($passable) use ($stack, $pipe) {...}
                  $callable = $slice($stack, $pipe);
    
                  // 而这行执行了 function ($passable) use ($stack, $pipe) {...} ,从而在父类中爆发
                  return $callable($passable);
              } catch (Exception $e) {
                  return $this->handleException($passable, $e);
              } catch (Throwable $e) {
                  return $this->handleException($passable, new FatalThrowableError($e));
              }
          };
      };
    }

    父类 carry!!

    protected function carry()
    {
      return function ($stack, $pipe) {
          return function ($passable) use ($stack, $pipe) {
              // 承接子类的 return $callable($passable); 从这爆发
    
              // 这里判断 洋葱皮 是沙包吗,明显不是,跳过
              if (is_callable($pipe)) {
                  return $pipe($passable, $stack);
              // 这里判断 洋葱皮 不是对象吗,明显不是,执行(洋葱皮只是一段字符串,一个类全名的字符串,需要实例化的)
              } elseif (! is_object($pipe)) {
                  list($name, $parameters) = $this->parsePipeString($pipe);                  
                  // 实例化洋葱皮
                  $pipe = $this->getContainer()->make($name);
    
                  // 定义好 handle 方法需要的参数,请求对象,和下一层洋葱
                  $parameters = array_merge([$passable, $stack], $parameters);
              } else {
                  $parameters = [$passable, $stack];
              }
    
              // 呵呵,控制器操作全在这一行里面,当然想进入控制器,首先剥完当前洋葱。还要把路由的洋葱剥完。后面的先不管,现在只知道,我们请求的响应内容从这里获取的。。。。这行代码,首先判断我这个洋葱皮对象有没有 handle 方法,有的话用上面准备好的参数执行这个方法。另一种说法,就是剥夺掉这个洋葱皮
              $response = method_exists($pipe, $this->method)
                              ? $pipe->{$this->method}(...$parameters)
                              : $pipe(...$parameters);
    
              return $response instanceof Responsable
                          ? $response->toResponse($this->container->make(Request::class))
                          : $response;
          };
      };
    }

    额外补充一点。我们在中间件 handle 方法中看到的 $next 参数,之前我说过是 剥好当前洋葱皮的洋葱。在 handle 方法最后一定有一个 return $next($request); 是不是。你看看哈,和上面 return $pipeline($this->passable); 不就是一个意思。通过这种 一层一层地剥落我的洋葱,把过来的请求过滤各遍。

写在最后

最后没什么可写的了,如果还有什么疑惑。留言询问。记得点播赞。我认为这是到现在讲 管道操作 最详细的教程啦。。。感觉学到的,点赞点赞点赞,重要事说三遍。。。

额外补充

关于为什么要用 array_reverse 函数倒排一下 $this->pipes,这是因为,包洋葱的时候,是从数组第一个元素开始拿葱皮,而执行请求穿透(剥洋葱)是从数组最后一个元素开始剥。这就如同栈一样,后进先出。而倒排一下 $this->pipes,最终实现穿透顺序,就是没有倒序 $this->pipes 时的顺序,大家想一下是不是这样。因为后进先出,倒排一下,就变成数组什么顺序,穿透时就是什么顺序。

本篇如有错误、不当或者需补充的内容,请各位同僚多提宝贵意见。

本文章首发在 LearnKu.com 网站上。

上一篇 下一篇
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
贡献者:1
讨论数量: 2
发起讨论 查看所有版本


18874127314
这里为什么要用两次闭包呢,一层不就够了吗?
1 个点赞 | 4 个回复 | 问答 | 课程版本 5.6
yema
请问请求开始穿透中间件层,返回的响应我不太理解。
0 个点赞 | 1 个回复 | 问答 | 课程版本 5.6