生命周期 5 管道流分析

管道流,也就是中间件过滤,代码初看很复杂,让我望而生畏,从而无法理解其中原理。经过一番修行,终于看懂了,然后又花了一整晚上,用自己的语言写了出来,很有成就感的说。。看我百万军中取上将首级!~~~(破音)

什么是洋葱?

上个图好理解一些。
file

代码位置

\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,可以在不理解的情况下先有一个直观印象,从而帮助理解。
附言 1  ·  4个月前

此帖子中有些表述仍然觉得不太清晰,更清晰的表述可以见下下个帖子--《路由&控制器中间件过滤》。

《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

请勿发布不友善或者负能量的内容。与人为善,比聪明更重要!