Laravel中间件的实现

Laravel 中间件 是通过pipeline执行的。

pipeline 顾名思义,就是管道,相当于unnix中的管道符(|),laravel中间件大部分都是对request的过滤,这种过滤的行为相当于管道符对输出结果的处理,举个例子 ls -alh|grep acb.txt 这里grep abc.txt 就是对ls的结果进行处理,在laravel框架中相当于一个中间件。

Laravelpipeline实现的方式非常简洁、有力,不但其实现原理如此,面对开发人员,它的调用方式也十分清晰,利用匿名函数使得前置与后置的调用都很直观。不过,很多人看到源码也很迷惑,因为中间存在着非常多的回调,只要基础不够扎实,就很容易在期间产生诸多困惑。不过,逐步分析和对基础知识的补完,就会发现再复杂的框架也不过是零碎的功能有序的构建起来的pipeline 的终极奥义就是对PHP的一个函数array_reduce的妙用。

array_reduce 函数介绍:

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

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

参数

array 输入的 array。

callback mixed callback ( mixed $carry , mixed $item )
    carry 携带上次迭代里的值; 如果本次迭代是第一次,那么这个值是 initial。
    item 携带了本次迭代的值。

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

返回值

返回结果值。initial 参数,array_reduce() 返回 NULL。

使用举例:

<?php
function sum($carry, $item)
{
    $carry += $item;
    return $carry;
}

function product($carry, $item)
{
    $carry *= $item;
    return $carry;
}

$a = array(1, 2, 3, 4, 5);
$x = array();

var_dump(array_reduce($a, "sum")); // int(15)
var_dump(array_reduce($a, "product", 10)); // int(1200), because: 10*1*2*3*4*5
var_dump(array_reduce($x, "sum", "No data to reduce")); // string(17) "No data to reduce"
?>

Laravelpipeline组件中巧妙的使用了array_reduce函数实现了中间件的效果,Laravelpipeline已经非常简洁了,这里比葫芦画瓢参照pipeline代码和接口来实现一个毫无实际利用价值的pipeline来理解它到底是如何工作的。

Laravelpipeline interface一共定义了四个方法:

<?php
interface Pipeline
{
    /**
     * Set the traveler object being sent on the pipeline.
     *
     * @param  mixed  $traveler
     * @return $this
     */
    public function send($traveler);

    /**
     * Set the stops of the pipeline.
     *
     * @param  dynamic|array  $stops
     * @return $this
     */
    public function through($stops);

    /**
     * Set the method to call on the stops.
     *
     * @param  string  $method
     * @return $this
     */
    public function via($method);

    /**
     * Run the pipeline with a final destination callback.
     *
     * @param  \Closure  $destination
     * @return mixed
     */
    public function then(Closure $destination);
}

我们分别比葫芦画瓢来写个实现,直接定义就不implements接口了:

<?php
class Pipeline
{
    
    /**
     * The object being passed through the pipeline.
     *
     * @var mixed
     */
    protected $passable;

    /**
     * The array of class pipes.
     *
     * @var array
     */
    protected $pipes = [];

  
    /**
     * Set the object being sent through the pipeline.
     *
     * @param  mixed  $passable
     * @return $this
     */
    public function send($passable)
    {
        $this->passable = $passable;

        return $this;
    }

    /**
     * Set the array of pipes.
     *
     * @param  array|mixed  $pipes
     * @return $this
     */
    public function through($pipes)
    {
        $this->pipes = is_array($pipes) ? $pipes : func_get_args();

        return $this;
    }

    /**
     * Set the method to call on the pipes.
     *
     * @param  string  $method
     * @return $this
     */
    public function via($method)
    {
        return $this;
    }

    /**
     * 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);
    }

    /**
     * Get the final piece of the Closure onion.
     *
     * @param  \Closure  $destination
     * @return \Closure
     */
    protected function prepareDestination(Closure $destination)
    {
        return function ($passable) use ($destination) {
            return $destination($passable);
        };
    }

    /**
     * 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 ($pipe instanceof Closure) {
                    // 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);
                }
            };
        };
    }
}


我真是太他妈机智了,把框架的代码拷贝过来删除了几行就说已经实现了,我们来使用测试这个类:

test.php

<?php
require_once 'pipeline.php';
//定义pipe1 类似中间件 
$pipe1= function ($passable, Closure $next) {
    $passable += 1;
    echo "pipe1: $passable\n";
    return $next($passable);
};
 
$pipe2 = function ($passable, Closure $next) {
    if ($passable > 7) {
        return $passable;
    }
 
    $passable += 3;
    echo "pipe2: $passable\n";
    return $next($passable);
};

$pipe3 = function ($passable, Closure $next) {
    $passable += 2;
    echo "pipe3 : $passable\n";
    return $next($passable);
};

//将所有要用的pipe放到一个数组 
$pipes = [$pipe1, $pipe2, $pipe3];

//test function
function test($passable,$pipes)
{
    echo "result: " . (new Pipeline)->send($passable)->through($pipes)->then(function ($passable) {
            echo "received: $passable\n";
            return 3;
        }) . "\n";
}


//=========run test=========

echo "==> test 1:\n";
test(5, $pipes);
echo "==> test 2:\n";
test(7, $pipes);

运行test.php 结果如下:

==> test 1:
pipe1: 6
pipe2: 9
pipe3 : 11
received: 11
result: 3
==> test 2:
pipe1: 8
result: 8

我们看到 passable为7时根本到不了最后的function 被pipe2直接过滤掉直接return回来了。
这里我们的pipeline中的send方法所接受的值passable即是要被过滤的条件,在Laravel中即是request对象,through 方法接受的pipes即是过滤方式,也就相当于Laravel的中间件,最后then接受的参数即是自己最终对符合条件的passable想要的处理,只有符合pipe条件的passable会最终传递到then中的方法,在Laravel中也就是只有符合中间件条件的request才会被传递到Controller。

仔细分析代码的执行过程:
先实例化pipeline 然后调用send方法将要处理的passable赋值给passable属性并返回this,接着调用through方法将pipe数组传入将pipes属性赋值并返回this,最后调用then方法传入了一个匿名函数,将值带入所以最终的then执行的代码可能是这个样子:

$pipeline = array_reduce(
    array_reverse($this->pipes), $this->carry(), $this->prepareDestination(funtion($passable){
        echo "received: $passable\n";
        return 3;
    }));
return $pipeline($this->passable);

在array_reduce第一次迭代时,使用initial的值作为callback中的carry参数,pipe3作为item,因为array先进行了array_reverse,我们将值带入获得第一次的迭代结果为:

function ($passable) {
     if ($pipe3 instanceof Closure) {
         return $pipe3($passable, function ($passable) {
             return $function($passable){
                 echo "received: $passable\n";
                 return 3;
             };
        });
     }
}

然后第二次迭代的结果为:

function ($passable) {
    if($pip2 instanceof Closure){
        return $pip2($passable,function ($passable) {
         if ($pipe3 instanceof Closure) {
             return $pipe3($passable, function ($passable) {
                 return $function($passable){
                     echo "received: $passable\n";
                     return 3;
                 };
            });
         }
        });       
    }
}

然后第三次迭代结果为

function ($passable) {
    if($pip1 instanceof Closure){
        return $pip1($passable,function ($passable) {
            if($pip2 instanceof Closure){
                return $pip2($passable,function ($passable) {
                 if ($pipe3 instanceof Closure) {
                 return $pipe3($passable, function ($passable) {
                     return $function($passable){
                         echo "received: $passable\n";
                         return 3;
                      };
                });
             }
        });       
        }
    });
    }
}

这就是array_reduce的结果了,最后$pipeline($this->passable)就正式的调用被返回的这个函数。正好是按照正常的顺序一个一个执行。

添加新评论