Laravel 管道流原理

Laravel 管道流原理强烈依赖 array_reduce 函数,我们先来了解下 array_reduce 函数的使用。

原标题 PHP 内置函数 array_reduce 在 Laravel 中的使用

array_reduce#

array_reduce() 将回调函数 callback 迭代地作用到 array 数组中的每一个单元中,从而将数组简化为单一的值。

mixed array_reduce ( array $array , callable $callback [, mixed $initial = NULL ] )
  1. array

    输入的 array。

  2. callback

    mixed callback ( mixed $carry , mixed $item )
    $carry 包括上次迭代的值,如果本次迭代是第一次,那么这个值是 initialitem 携带了本次迭代的值

  3. initial

    如果指定了可选参数 initial,该参数将在处理开始前使用,或者当处理结束,数组为空时的最后一个结果。

从文档说明可以看出,array_reduce 函数是把数组的每一项,都通过给定的 callback 函数,来简化的。

那我们就来看看是怎么简化的。

$arr = ['AAAA', 'BBBB', 'CCCC'];

$res = array_reduce($arr, function($carry, $item){
    return $carry . $item;
});

给定的数组长度为 3,故总迭代三次。

  1. 第一次迭代时 $carry = null $item = AAAA 返回 AAAA
  2. 第二次迭代时 $carry = AAAA $item = BBBB 返回 AAAABBBB
  3. 第三次迭代时 $carry = AAAABBBB $item = CCCC 返回 AAAABBBBCCCC

这种方式将数组简化为一串字符串 AAAABBBBCCCC

带初始值的情况#

$arr = ['AAAA', 'BBBB', 'CCCC'];

$res = array_reduce($arr, function($carry, $item){
    return $carry . $item;
}, 'INITIAL-');
  1. 第一次迭代时($carry = INITIAL-),($item = AAAA) 返回 INITIAL-AAAA
  2. 第二次迭代时($carry = INITIAL-AAAA),($item = BBBB), 返回 INITIAL-AAAABBBB
  3. 第三次迭代时($carry = INITIAL-AAAABBBB),($item = CCCC),返回 INITIAL-AAAABBBBCCCC

这种方式将数组简化为一串字符串 INITIAL-AAAABBBBCCCC

闭包#

$arr = ['AAAA', 'BBBB', 'CCCC'];

//没带初始值
$res = array_reduce($arr, function($carry, $item){
    return function() use ($item){//这里只use了item
        return strtolower($item) . '-';
    };
});
  1. 第一次迭代时,$carry:null,$item = AAAA,返回一个 use 了 $item = AAAA 的闭包
  2. 第二次迭代时,$carry:use 了 $item = AAAA 的闭包,$item = BBBB,返回一个 use 了 $item = BBBB 的闭包
  3. 第三次迭代时,$carry:use 了 $item = BBBB 的闭包,$item = CCCC,返回一个 use 了 $item = CCCC 的闭包

这种方式将数组简化为一个闭包,即最后返回的闭包,当我们执行这个闭包时 $res() 得到返回值 CCCC-

上面这种方式只 use ($item),每次迭代返回的闭包在下次迭代时,我们都没有用起来。只是又重新返回了一个 use 了当前 item 值的闭包。

闭包 USE 闭包#

$arr = ['AAAA'];

$res = array_reduce($arr, function($carry, $item){
    return function () use ($carry, $item) {
        if (is_null($carry)) {
            return 'Carry IS NULL' . $item;
        }
    };
});

注意,此时的数组长度为 1,并且没有指定初始值

由于数组长度为 1,故只迭代一次,返回一个闭包 use($carry = null, $item = 'AAAA'),当我们执行($res())这个闭包时,得到的结果为 Carry IS NULLAAAA

接下来我们重新改造下,

$arr = ['AAAA', 'BBBB'];

$res = array_reduce($arr, function($carry, $item){
    return function () use ($carry, $item) {
        if (is_null($carry)) {
            return 'Carry IS NULL' . $item;
        }
        if ($carry instanceof \Closure) {
            return $carry() . $item;
        }
    };
});

我们新增了一个条件判断,若当前迭代的值是一个闭包,返回该闭包的执行结果。

第一次迭代时,$carry 的值为 null$item 的值为 AAAA,返回一个闭包,

//伪代码
function () use ($carry = null, $item = AAAA) {
    if (is_null($carry)) {
        return 'Carry IS NULL' . $item;
    }
    if ($carry instanceof \Closure) {
        return $carry() . $item;
    }
}

假设我们直接执行该闭包,将会返回 Carry IS NULLAAAA 的结果。

第二次迭代时,$carry 的值为上述返回的闭包(伪代码),$item 的值为 BBBB,返回一个闭包,

当我们执行这个闭包时,满足 $carry instanceof \Closure,得到结果 Carry IS NULLAAAABBBB

Laravel 中的 array_reverse#

大致了解了 array_reverse 函数的使用后,我们来瞅瞅 laravel 管道流里使用 array_reverse 的情况。

我在 Laravel 中间件原理中有阐述,强烈建议先去看看 Laravel 中间件原理再回过头来接着看。

php 内置方法 array_reduce 把所有要通过的中间件都通过 callback 方法并压缩为一个 Closure。最后在执行 Initial

Laravel 中通过全局中间件的核心代码如下:

//Illuminate\Foundation\Http\Kernel.php
protected function sendRequestThroughRouter($request)
{
    return (new Pipeline($this->app))
        ->send($request)
        ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
        ->then($this->dispatchToRouter());
}
protected function dispatchToRouter()
{
    return function ($request) {
        $this->app->instance('request', $request);
        return $this->router->dispatch($request);
    };
}

正如我前面说的,我们发送一个 $request 对象通过 middleware 中间件数组,最后在执行 dispatchToRouter 方法。

假设有两个全局中间件,我们来看看这两个中间件是如何通过管道压缩为一个 Closure 的。

Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
App\Http\Middleware\AllowOrigin::class,//自定义中间件

Illuminate\Pipeline\Pipeline 为 laravel 的管道流核心类.

Illuminate\Pipeline\Pipelinethen 方法中,$destination 为上述的 dispatchToRouter 闭包,pipes 为要通过的中间件数组,passableRequest 对象。

public function then(Closure $destination)
{
    $pipeline = array_reduce(
        array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination)
    );
    return $pipeline($this->passable);
}

array_reverse 函数将中间件数组的每一项都通过 $this->carry(),初始值为上述 dispatchToRouter 方法返回的闭包。

protected function prepareDestination(Closure $destination)
{
    return function ($passable) use ($destination) {
        return $destination($passable);
    };
}
protected function carry()
{
    return function ($stack, $pipe) {
        return function ($passable) use ($stack, $pipe) {
            if ($pipe instanceof Closure) {
                return $pipe($passable, $stack);
            } elseif (! is_object($pipe)) {
                //解析中间件参数
                list($name, $parameters) = $this->parsePipeString($pipe);
                $pipe = $this->getContainer()->make($name);
                $parameters = array_merge([$passable, $stack], $parameters);
            } else {
                $parameters = [$passable, $stack];
            }
            return $pipe->{$this->method}(...$parameters);
        };
    };
}

第一次迭代时,返回一个闭包,use$stack$pipe$stack 的值为初始值闭包,$pipe 为中间件类名,此处是 App\Http\Middleware\AllowOrigin::class(注意 array_reverse 函数把传进来的中间件数组倒叙了)。

假设我们直接运行该闭包,由于此时 $pipe 是一个 String 类型的中间件类名,只满足 ! is_object($pipe) 这个条件,我们将直接从容器中 make 一个该中间件的实列出来,在执行该中间件实列的 handle 方法(默认 $this->methodhandle)。并且将 request 对象和初始值作为参数,传给这个中间件。

public function handle($request, Closure $next)
{
    //......
}

在这个中间件的 handle 方法中,当我们直接执行 return $next($request) 时,相当于我们开始执行 array_reduce 函数的初始值闭包了,即上述的 dispatchToRouter 方法返回的闭包。

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

好,假设结束。在第二次迭代时,也返回一个 use$stack$pipe$stack 的值为我们第一次迭代时返回的闭包,$pipe 为中间件类名,此处是 Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class

两次迭代结束,回到 then 方法中,我们手动执行了第二次迭代返回的闭包。

return $pipeline($this->passable);

当执行第二次迭代返回的闭包时,当前闭包 use$pipeIlluminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,同样只满足 ! is_object($pipe) 这个条件,我们将会从容器中 makeCheckForMaintenanceMode 中间件的实列,在执行该实列的 handle 方法,并且把第一次迭代返回的闭包作为参数传到 handle 方法中。

当我们在 CheckForMaintenanceMode 中间件的 handle 方法中执行 return $next($request) 时,此时的 $next 为我们第一次迭代返回的闭包,将回到我们刚才假设的流程那样。从容器中 make 一个 App\Http\Middleware\AllowOrigin 实列,在执行该实列的 handle 方法,并把初始值闭包作为参数传到 AllowOrigin 中间件的 handle方法中。当我们再在 AllowOrigin 中间件中执行 return $next($request) 时,代表我们所有中间件都通过完成了,接下来开始执行 dispatchToRouter

  1. 中间件是区分先后顺序的,从这里你应该能明白为什么要把中间件用 array_reverse 倒叙了。
  2. 并不是所有中间件在运行前都已经实例化了的,用到的时候才去想容器取
  3. 中间件不执行 $next ($request) 后续所有中间件无法执行。

这篇文章是专们为了上一篇 Laravel 中间件原理写的,因为在写 Laravel 中间件原理时我也不很清楚 array_reducelaravel 中的运行流程。如果有什么不对的,欢迎指正。

本作品采用《CC 协议》,转载必须注明作者和本文链接
二愣的闲谈杂鱼
本帖由 Summer 于 7年前 加精
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 9

非常好!

特别是前面对于 array_reduce 中 callback return 一个闭包的分析,对 array_reduce 的原理分析的很透彻哈。

看你文章的同时,自己结合代码这一块理了一下,确实比较绕。下面写一下我自己理清这个思路的过程,借你的地盘,权当是自己做个笔记吧。

1、为了后续分析的方便,先把 array_reduce 方法的参数替换,替换之后大概是这样

array_reduce(
    ['App\Http\Middleware\AllowOrigin::class','Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class'],
    function ($stack, $pipe) {        //callback
        return function ($request) use ($stack, $pipe) {//$request http request对象 
            if ($pipe instanceof Closure) {
                return $pipe($request, $stack);
            } elseif (!is_object($pipe)) {
                //解析中间件参数
                list($name, $parameters) = $this->parsePipeString($pipe);
                $pipe = $this->getContainer()->make($name);
                $parameters = array_merge([$request, $stack], $parameters);
            } else {
                $parameters = [$request, $stack];
            }
            return $pipe->handle($request,$stack);//stack变量很关键
        };
    },
    function ($request) {    //init闭包
        $this->app->instance('request', $request);
        return $this->router->dispatch($request);
    }
);

2、array_reduce 第一次循环:

callback 参数值:

$stack = init闭包;  
$pipe = 'App\Http\Middleware\AllowOrigin::class';

进入 callback 方法:

!is_object('App\Http\Middleware\AllowOrigin::class') = true;
$pipe = new App\Http\Middleware\AllowOrigin();
$pipe->handle($request,$stack = 'init闭包');

第一次循环结束,$stack 变量的值变为:

//注意返回值是个闭包。
function(){
    $pipe = new App\Http\Middleware\AllowOrigin();
    return $pipe->handle($request,$stack = 'init闭包');
}

3、array_reduce 第二次循环:

callback 参数值:

$stack = function(){
    $pipe = new App\Http\Middleware\AllowOrigin();
    return $pipe->handle($request,$stack = 'init闭包');
};//第一次循环返回的闭包
$pipe = 'Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class';

进入 callback 方法:

!is_object('Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class') = true;
$pipe = new Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode();
$pipe->handle($request,$stack); //注意这里的$stack。

第二次循环结束,$stack 变量的值变为:

function(){
    $pipe = Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode();
    return $pipe->handle($request,$stack = '第一次循环返回的闭包');
}

4、array_reduce () 循环结束。

array_reduce () 方法的返回值,就是 $stack 变量的最终值。即‘第二次循环返回的闭包’。

5、回到 then () 方法:

    public function then(Closure $destination)
    {
        $pipeline = array_reduce(
            array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination)
        );

        return $pipeline($this->passable); //执行array_reduce()返回的闭包。
    }

下面开始执行 array_reduce () 返回的闭包。即 “第二次循环返回的闭包”:

//执行CheckForMaintenanceMode的handle方法:
//这里把参数值代入handle()方法
public function handle($request, Closure $next = function(){
    $pipe = new App\Http\Middleware\AllowOrigin();
    return $pipe->handle($request,$stack = 'init闭包');
})
    {
        ...  //Middleware的业务逻辑,此处省略。
        return $next($request);
    }
//很明显,下面该执行$next闭包。即该执行App\Http\Middleware\AllowOrigin()->handle()方法,
//再接着,就该执行init闭包,即$this->router->dispatch($request);

总结一下: 一个 http 请求,在通过了所有中间件(一般是权限验证、安全过滤等)之后,接下来就该执行 laravel 框架的路由了。

7年前 评论

非常好!

特别是前面对于 array_reduce 中 callback return 一个闭包的分析,对 array_reduce 的原理分析的很透彻哈。

看你文章的同时,自己结合代码这一块理了一下,确实比较绕。下面写一下我自己理清这个思路的过程,借你的地盘,权当是自己做个笔记吧。

1、为了后续分析的方便,先把 array_reduce 方法的参数替换,替换之后大概是这样

array_reduce(
    ['App\Http\Middleware\AllowOrigin::class','Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class'],
    function ($stack, $pipe) {        //callback
        return function ($request) use ($stack, $pipe) {//$request http request对象 
            if ($pipe instanceof Closure) {
                return $pipe($request, $stack);
            } elseif (!is_object($pipe)) {
                //解析中间件参数
                list($name, $parameters) = $this->parsePipeString($pipe);
                $pipe = $this->getContainer()->make($name);
                $parameters = array_merge([$request, $stack], $parameters);
            } else {
                $parameters = [$request, $stack];
            }
            return $pipe->handle($request,$stack);//stack变量很关键
        };
    },
    function ($request) {    //init闭包
        $this->app->instance('request', $request);
        return $this->router->dispatch($request);
    }
);

2、array_reduce 第一次循环:

callback 参数值:

$stack = init闭包;  
$pipe = 'App\Http\Middleware\AllowOrigin::class';

进入 callback 方法:

!is_object('App\Http\Middleware\AllowOrigin::class') = true;
$pipe = new App\Http\Middleware\AllowOrigin();
$pipe->handle($request,$stack = 'init闭包');

第一次循环结束,$stack 变量的值变为:

//注意返回值是个闭包。
function(){
    $pipe = new App\Http\Middleware\AllowOrigin();
    return $pipe->handle($request,$stack = 'init闭包');
}

3、array_reduce 第二次循环:

callback 参数值:

$stack = function(){
    $pipe = new App\Http\Middleware\AllowOrigin();
    return $pipe->handle($request,$stack = 'init闭包');
};//第一次循环返回的闭包
$pipe = 'Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class';

进入 callback 方法:

!is_object('Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class') = true;
$pipe = new Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode();
$pipe->handle($request,$stack); //注意这里的$stack。

第二次循环结束,$stack 变量的值变为:

function(){
    $pipe = Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode();
    return $pipe->handle($request,$stack = '第一次循环返回的闭包');
}

4、array_reduce () 循环结束。

array_reduce () 方法的返回值,就是 $stack 变量的最终值。即‘第二次循环返回的闭包’。

5、回到 then () 方法:

    public function then(Closure $destination)
    {
        $pipeline = array_reduce(
            array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination)
        );

        return $pipeline($this->passable); //执行array_reduce()返回的闭包。
    }

下面开始执行 array_reduce () 返回的闭包。即 “第二次循环返回的闭包”:

//执行CheckForMaintenanceMode的handle方法:
//这里把参数值代入handle()方法
public function handle($request, Closure $next = function(){
    $pipe = new App\Http\Middleware\AllowOrigin();
    return $pipe->handle($request,$stack = 'init闭包');
})
    {
        ...  //Middleware的业务逻辑,此处省略。
        return $next($request);
    }
//很明显,下面该执行$next闭包。即该执行App\Http\Middleware\AllowOrigin()->handle()方法,
//再接着,就该执行init闭包,即$this->router->dispatch($request);

总结一下: 一个 http 请求,在通过了所有中间件(一般是权限验证、安全过滤等)之后,接下来就该执行 laravel 框架的路由了。

7年前 评论
godruoyi

@许胜斌 你喜欢就好:smiley:

7年前 评论

会让您好啊,呵呵!

7年前 评论

借宝地发一下我从装饰者模式基础上对 pipeline 的理解哈:博客:Laravel HTTP——Pipeline 中间件装饰者模式源码分析

7年前 评论

你好能转载么,会注明出处的

6年前 评论

牛逼,我得再回复一下。

6年前 评论
$arr = ['MW_A', 'MW_B'];
$arr = array_reverse($arr);

$destination = function ($request) {
    echo 'Destination, request:  ' . $request . PHP_EOL;
};

$middleware = function ($request, $carry, $mw) {
    $request .= '.' . $mw;
    echo 'Middleware: ' . $mw . ', request: ' . $request . PHP_EOL;
    return $carry($request);
};

$res = array_reduce($arr, function ($carry, $item) {
    return function ($request) use ($carry, $item) {
        // IoC
        global $middleware;
        return $middleware($request, $carry, $item);
    };
}, $destination);

var_dump($res('RequestObject'));

Laravel

Middleware: MW_A, request: RequestObject.MW_A
Middleware: MW_B, request: RequestObject.MW_A.MW_B
Destination, request:  RequestObject.MW_A.MW_B
NULL
5年前 评论
sanders

谁能解释一下 Illuminate\Pipeline\Hub 的作用?预先声明好 管道 避免重复实例化吗?

5年前 评论

有一个简化版本,希望能帮助理解,可以在每个中间件和路由中打断点,然后 debug 一步一步看看结果

// $m* 代表中间件  
$m1 = function ($request, $next) {  
    $request[] = 'm1';  
    $response = $next($request);  
    $response[] = 'm1-end';  

    return $response;  
};  

$m2 = function ($request, $next) {  
    $request[] = 'm2';  
    $response = $next($request);  
    $response[] = 'm2-end';  

    return $response;  
};  

$m3 = function ($request, $next) {  
    $request[] = 'm3';  
    return $next($request);  
};  

// $r 代表路由  
$r = function ($request) {  
    $request[] = 'route';  
    $response = $request;  
    return $response;  
};  

// 简易 Pipeline
$res = array_reduce(  
    array_reverse([$m1, $m2, $m3]),  
    function ($carry, $item) {  
        return function ($request) use ($carry, $item) {  
            return $item($request, $carry);  
        };  
    },  
    (function ($r) {  
        return function ($request) use ($r) {  
            return $r($request);  
        };  
    })($r)  
);  

$request = [];  
$response = $res($request);  
var_dump($response);
1年前 评论