生命周期 5 管道流分析 
                                                    
                        
                    
                    
  
                    
                    管道流,也就是中间件过滤,代码初看很复杂,让我望而生畏,从而无法理解其中原理。经过一番修行,终于看懂了,然后又花了一整晚上,用自己的语言写了出来,很有成就感的说。。看我百万军中取上将首级!~~~(破音)
什么是洋葱?
上个图好理解一些。
代码位置
\Illuminate\Foundation\Http\Kernel::sendRequestThroughRouter:140
链式操作
return (new Pipeline($this->app))
                    ->send($request)
                    ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
                    ->then($this->dispatchToRouter());
可以改写为:
//获得pipeline对象并设置属性,后面会用。
$pipeline = (new Pipeline($this->app)) //创建管道对象
                    ->send($request) //设置$this->passable,要过滤的request对象
                    ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware); //设置$this->pipes,要经过的过滤层。
//执行then方法
$pipeline->then($this->dispatchToRouter());                 then方法执行了哪些操作:简单的说then方法创建了一个嵌套的闭包对象,然后将request传入这个闭包对象,在经过每个闭包层的时候,会去执行这个闭包层(中间件的)的handle方法,从而达到中间件过滤的效果。
$this->dispatchToRouter方法代码(此段代码function里面的东西在最后才会执行):
/**
* Get the route dispatcher callback.
*
* @return \Closure
*/
protected function dispatchToRouter()
{
    return function ($request) {
        $this->app->instance('request', $request);
        return $this->router->dispatch($request);
    };
}Get the route dispatcher callback. 意思是获得路由分发器的回调函数,既然是获得函数,那么就不会执行里面的回调函数,只是执行了return 语句,返回一个回调函数,回调函数本身不执行。
因此,$pipeline->then($this->dispatchToRouter());可以看成如下代码:
$pipeline->then(function ($request) {
        $this->app->instance('request', $request);
        return $this->router->dispatch($request);
    });  $pipeline->then方法代码:
/**
    * Run the pipeline with a final destination callback.
    *
    * @param  \Closure  $destination
    * @return mixed
    */
public function then(Closure $destination)
{
    $pipeline = array_reduce(
        array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination)
    );
    return $pipeline($this->passable);
}Run the pipeline with a final destination callback.
这句话的意思是运行pipeline,并指定一个最终的目标回调函数,也就是我们传入的$destination变量,也就是$this->dispatchToRouter(),这里就叫它「大BOSS」。
可以看到then方法中进行了两大步操作:
- 使用array_reduce函数将所有的管道倒序传入$this->carry()这个回调函数,最后再到大BOSS那里。整个过程最终形成了类似洋葱的多层关卡,只有不断通关,才能见到大BOSS,决一死战。
- 而 return $pipeline($this->passable)就是整个通关的过程,并返回一个通关结果。
接下来详细描述一下关卡形成过程和通关过程.
通关之前,先去此地打怪修炼。
关卡形成过程
三个参数分析
在then方法中
$pipeline = array_reduce(
            array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination)
        );首先有7道关卡的名字(默认5道,另外两道是第三方包添加的中间件),这些关卡都是全局中间件,即$this->pipes,内容如下:
array (
  0 => 'Dingo\\Api\\Http\\Middleware\\Request',
  1 => 'App\\Http\\Middleware\\CheckForMaintenanceMode',
  2 => 'Illuminate\\Foundation\\Http\\Middleware\\ValidatePostSize',
  3 => 'App\\Http\\Middleware\\TrimStrings',
  4 => 'Illuminate\\Foundation\\Http\\Middleware\\ConvertEmptyStringsToNull',
  5 => 'App\\Http\\Middleware\\TrustProxies',
  6 => 'Barryvdh\\Debugbar\\Middleware\\InjectDebugbar',
)array_reverse($this->pipes)倒序,就是:
array (
  0 => 'Barryvdh\\Debugbar\\Middleware\\InjectDebugbar',
  1 => 'App\\Http\\Middleware\\TrustProxies',
  2 => 'Illuminate\\Foundation\\Http\\Middleware\\ConvertEmptyStringsToNull',
  3 => 'App\\Http\\Middleware\\TrimStrings',
  4 => 'Illuminate\\Foundation\\Http\\Middleware\\ValidatePostSize',
  5 => 'App\\Http\\Middleware\\CheckForMaintenanceMode',
  6 => 'Dingo\\Api\\Http\\Middleware\\Request',
)为什么要倒序?因为关卡从内往外开始形成的,大BOSS在最里面,最外层的是最后才形成,因此Dingo\\Api\\Http\\Middleware\\Request跑到了第一关,Barryvdh\\Debugbar\\Middleware\\InjectDebugbar是第7关,大BOSS在第8关。
$this->carry()就是关卡构建器,代码如下
    /**
     * Get a Closure that represents a slice of the application onion.
     *
     * @return \Closure
     */
    protected function carry()
    {
        return function ($stack, $pipe) {
            return function ($passable) use ($stack, $pipe) {
                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));
                }
            };
        };
    }Get a Closure that represents a slice of the application onion.
获得一个闭包代表洋葱型应用程序的一层。意思就是我们游戏的一个关卡了。这个也是获得闭包,那么同上面的大BOSS关一样,它也是只执行第一个return语句,返回一个关卡,内容为:
function ($stack, $pipe) {
    return function ($passable) use ($stack, $pipe) {
        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));
        }
    };
};而根据我们官方文档array_reduce的$callback方法签名参数的说明:
mixed callback ( mixed $carry , mixed $item )
- carry 携带上次迭代里的值; 如果本次迭代是第一次,那么这个值是 initial。
- item 携带了本次迭代的值.
由此对照可知,在创建第1关(第1次迭代)时,carry()方法中的return function ($stack, $pipe)中的$stack就应该是then方法中的第三参数$this->prepareDestination($destination),而$pipe就是第1关的名字Dingo\\Api\\Http\\Middleware\\Request。
而$this->prepareDestination方法的代码如下
    /**
     * Get the final piece of the Closure onion.
     *
     * @param  \Closure  $destination
     * @return \Closure
     */
    protected function prepareDestination(Closure $destination)
    {
        return function ($passable) use ($destination) {
            try {
                return $destination($passable);
            } catch (Exception $e) {
                return $this->handleException($passable, $e);
            } catch (Throwable $e) {
                return $this->handleException($passable, new FatalThrowableError($e));
            }
        };
    }Get the final piece of the Closure onion.
获得闭包洋葱的最后一片,这不就是大BOSS么?
它返回一个闭包,也就是只执行第一个return语句。相当于大BOSS关为:
function ($passable) use ($destination) {
    try {
        return $destination($passable);
    } catch (Exception $e) {
        return $this->handleException($passable, $e);
    } catch (Throwable $e) {
        return $this->handleException($passable, new FatalThrowableError($e));
    }
}而这个$destination就是我们一开始看到的
function ($request) {
    $this->app->instance('request', $request);
    return $this->router->dispatch($request);
}关键点
首先必须要理解array_reduce的思想,通过遍历第一参数array中的值,每次都去执行第二参数callback。而每次执行callback时,执行的结果都作为下一次执行callback的第一参数,而遍历的值都作为每次执行callback的第二参数。也就是说类似于如下代码:
function array_reduce($pipes, $callback, $initial=null)
{
    $stack = $initial;
    foreach($pipes as $pipe){
        $stack = $callback($stack, $pipe);
    }
    return $stack;
}形成具体过程
我们这里的$this->carry(),它每次的操作都仅仅是返回了一个待执行的闭包对象。注意特别注意:每次都使用use引入静态变量,每次的$stack, $pipe都是固定的具体值嵌入这个闭包对象中。
function ($passable) use ($stack, $pipe) {
    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));
    }
}每次返回闭包对象后,都放到下一次的$stack参数中。因此,第1次迭代时,返回的闭包对象就是「大BOSS关|倒数第1关」,伪代码如下:
function ($passable) use (「大BOSS」, '倒数第1关名字') {
    try {
        $slice = parent::carry();
        $callable = $slice(「大BOSS」, '倒数第1关名字');
        return $callable($passable);
    } catch (Exception $e) {
        return $this->handleException($passable, $e);
    } catch (Throwable $e) {
        return $this->handleException($passable, new FatalThrowableError($e));
    }
}这个结果会直接赋给在第二次迭代的$stack参数,那么第2次迭代返回的结果就是「大BOSS关|倒数第1关|倒数第2关」:
function ($passable) use (「大BOSS关|倒数第1关」, '倒数第2关名字') {
    try {
        $slice = parent::carry();
        $callable = $slice(「大BOSS关|倒数第1关」, '倒数第2关名字');
        return $callable($passable);
    } catch (Exception $e) {
        return $this->handleException($passable, $e);
    } catch (Throwable $e) {
        return $this->handleException($passable, new FatalThrowableError($e));
    }
}这个结果会直接赋给在第3次迭代的$stack参数,那么第3次迭代返回的结果就是「大BOSS关|倒数第1关|倒数第2关|倒数第3关」:
function ($passable) use (「大BOSS关|倒数第1关|倒数第2关」, '倒数第3关名字') {
    try {
        $slice = parent::carry();
        $callable = $slice(「大BOSS关|倒数第1关|倒数第2关」, '倒数第3关名字');
        return $callable($passable);
    } catch (Exception $e) {
        return $this->handleException($passable, $e);
    } catch (Throwable $e) {
        return $this->handleException($passable, new FatalThrowableError($e));
    }
}以此类推,
最后形成的关卡集群为:
return function ($passable) use (「大BOSS关|倒数第1关|倒数第2关|倒数第3关|倒数第4关|倒数第5关|倒数第6关」, '倒数第7关名字') {
    try {
        $slice = parent::carry();
        $callable = $slice(「大BOSS关|倒数第1关|倒数第2关|倒数第3关|倒数第4关|倒数第5关|倒数第6关」」, '倒数第7关名字');
        return $callable($passable);
    } catch (Exception $e) {
        return $this->handleException($passable, $e);
    } catch (Throwable $e) {
        return $this->handleException($passable, new FatalThrowableError($e));
    }
}这就是array_reduce形成的一个嵌套多层的超级大闭包(洋葱),超级牛逼(头痛)。
所以说,要学好英语,stack不就是栈的意思么,小小一个参数其实是一个超大的栈。
好了,现在终于知道$pipeline就是上面这个关卡集群了。
冲关过程
$pipeline($this->passable)$pipeline是一个闭包对象,那么上面这种写法就是去执行它,传入的是$this->passable,也就是IlluminateRequest对象。
这次执行,首先到了$slice = parent::carry();
也就是去执行父类的carry()方法,
   /**
     * Get a Closure that represents a slice of the application onion.
     *
     * @return \Closure
     */
    protected function carry()
    {
        return function ($stack, $pipe) {
            return function ($passable) use ($stack, $pipe) {
                if (is_callable($pipe)) {
                    // If the pipe is an instance of a Closure, we will just call it directly but
                    // otherwise we'll resolve the pipes out of the container and call it with
                    // the appropriate method and arguments, returning the results back out.
                    return $pipe($passable, $stack);
                } elseif (! is_object($pipe)) {
                    [$name, $parameters] = $this->parsePipeString($pipe);
                    // If the pipe is a string we will parse the string and resolve the class out
                    // of the dependency injection container. We can then build a callable and
                    // execute the pipe function giving in the parameters that are required.
                    $pipe = $this->getContainer()->make($name);
                    $parameters = array_merge([$passable, $stack], $parameters);
                } else {
                    // If the pipe is already an object we'll just make a callable and pass it to
                    // the pipe as-is. There is no need to do any extra parsing and formatting
                    // since the object we're given was already a fully instantiated object.
                    $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;
            };
        };
    }看起来非常吓人,但一切反动派都是纸老虎,实际上这个只会执行第一个return,也就是说$slice实际上得到的是一个闭包对象。
下面这句就是执行这个闭包对象,这里的$stack就是关卡栈(管道流)
$callable = $slice($stack, $pipe)也就是
$callable = $slice(「大BOSS关|倒数第1关|倒数第2关|倒数第3关|倒数第4关|倒数第5关|倒数第6关」, '倒数第7关名字');这里的$callable也是闭包,而执行这个闭包对象,也只是执行上面这个carry方法的第二个return,还是返回一个闭包对象。
然后再执行它。
return $callable($passable)好了,到了这里,终于开始正式的执行冲关操作了。在我们这个上下文中,传入$pipe的都是字符串,因此都是在执行下面的操作:
//通过$pipe名字拆分$name和$parameters
[$name, $parameters] = $this->parsePipeString($pipe);
//解析中间件对象
$pipe = $this->getContainer()->make($name);
//将request对象,关卡栈对象,$parameters进行array_merge操作并赋给$parameters变量,它是一个数组。
$parameters = array_merge([$passable, $stack], $parameters);接下来,就是看中间件对象中有没有handle方法,有就执行handle方法。并且使用...$parameters这种写法将一个数组变量拆分为方法中的参数序列。
 $response = method_exists($pipe, $this->method)
                                ? $pipe->{$this->method}(...$parameters)
                                : $pipe(...$parameters);这样,$passable就是中间件中$request参数,而$stack就是$next。
下面是DingoRequest中间件的handle代码:
    public function handle($request, Closure $next)
    {
        try {
            if ($this->validator->validateRequest($request)) {
                $this->app->singleton(LaravelExceptionHandler::class, function ($app) {
                    return $app[ExceptionHandler::class];
                });
                $request = $this->app->make(RequestContract::class)->createFromIlluminate($request);
                $this->events->fire(new RequestWasMatched($request, $this->app));
                return $this->sendRequestThroughRouter($request);
            }
        } catch (Exception $exception) {
            $this->exception->report($exception);
            return $this->exception->handle($exception);
        }
        return $next($request);
    }可以看到$request对象会经历很多磨难。如果一切正常,就会执行到return $next($request)。
而这里的$next就是我们的关卡栈对象$stack。$next($request)就是执行一次闭包,也就是拆包(剥皮/冲关)。因此,每执行一次,层数就少一层。为什么?因为我们前面构建关卡的时候,每个关卡的$stack参数都是固定的,静态的,越执行到后面,$stack剩下的关卡就越少了!
好了,经过层层磨难,终于见到大boss了。。
执行到了下面这句:
return $destination($passable);别忘了,$passable是request对象,而$destination是个闭包对象,就是我们一开始的那个
    /**
     * Get the route dispatcher callback.
     *
     * @return \Closure
     */
    protected function dispatchToRouter()
    {
        return function ($request) {
            $this->app->instance('request', $request);
            return $this->router->dispatch($request);
        };
    }因此,执行这个闭包,首先执行到$this->app->instance('request', $request);,注册request对象到容器(百宝箱)的instances(实例缓存)属性中。
然后就是$this->router->dispatch($request),将request通过路由到达应用程序,然后返回最终结果了!
小结
至此,管道流已经大致理解了。
- 在看这个代码前,必须对闭包,array_reduce,laravel生命周期有一定理解。
- 学好英语,看注释,看变量命名,方法命名,往往能帮助理解。
- 有一个好的IDE,可以在不理解的情况下先有一个直观印象,从而帮助理解。
本作品采用《CC 协议》,转载必须注明作者和本文链接
此帖子中有些表述仍然觉得不太清晰,更清晰的表述可以见下下个帖子--《路由&控制器中间件过滤》。
 
           hustnzj 的个人博客
 hustnzj 的个人博客
         
             
             
             
             
             
             
             
             
             
             
             
             
             
             
             
             
             
             
                     
                     
           
           关于 LearnKu
                关于 LearnKu
               
                     
                     
                     粤公网安备 44030502004330号
 粤公网安备 44030502004330号 
 
推荐文章: