Laravel Pipeline 组件的实现

Laravel 框架中有一个很是有趣的功能,就是 HTTP 中间件,咱们在定义路由的时候,经过中间件对访问进行过滤。来自外部的请求首先通过全局中间件,若经过,则会继续穿过层层路由组所设置的中间件,在到达目的路由,固然,目的路由也可能定义了个中间件,经过后,该路由的处理对象(如控制器),获得的就是一个通过过滤的请求了。php

开始

本文固然不是讨论中间件如何使用,而是其实现的基础。Laravel 框架中有一个组件叫作 Illuminate\Pipeline,意味 “管道”,咱们看看下面这个代码示例:数组

<?php
use Illuminate\Pipeline\Pipeline;

$pipe1 = function ($poster, Closure $next) {
    $poster += 1;
    echo "pipe1: $poster\n";
    return $next($poster);
};

$pipe2 = function ($poster, Closure $next) {
    if ($poster > 7) {
        return $poster;
    }

    $poster += 3;
    echo "pipe2: $poster\n";
    return $next($poster);
};

$pipe3 = function ($poster, Closure $next) {
    $result = $next($poster);
    echo "pipe3: $result\n";
    return $result * 2;
};

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

$pipes = [$pipe1, $pipe2, $pipe3, $pipe4];

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

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

上述代码执行结果以下:闭包

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

流程概览

Pipeline 组件实现了一个过滤流程:框架

原始数据 ---> 【前置管道】 ---> 目标处理逻辑 ---> 【后置管道】 ---> 结果数据函数

经过这种机制,能够将目标处理逻辑与过滤、认证等机制的代码分离开来,这样咱们就更容易让代码清晰和易于维护。经过前置、后置管道,在其中 “放置” 咱们须要过滤的逻辑便可,如上述代码,虽然只是一个简单的示例,就已经可以看得出,整个流程的动向,譬如咱们在上面示例中准备了四个过滤组件(中间件): pipe一、pipe二、pipe三、pipe4,其中 一、二、4 是前置,3 为后置。post

输入的原始数据为 5,执行过程首先经过 1 号过滤组件,而后是 2 号,再而后是 4 号,到达目标处理逻辑后,再经过 3 号过滤组件,最终输出结果。this

输入原始数据为 7,一样是先通过 1 号过滤组件,随后是 2 号,不过在 2 号中,直接返回告终果,这意味着过程被拦截,再也不继续向下传递数据,至此结束并返回结果。spa

Laravel 框架中,原始数据是一个 Request 对象,经过所定义的前置中间件,开发者可在中间件中获取 Request 的信息,好比用户的 Session/Cookie 以及 Header 等,验证数据是否完备等等,不完备或不符合要求的,则被拦截并返回一个响应告知。若能正常经过则继续传递至最终的处理逻辑,如控制器的某个方法或者一个匿名函数。经过这种模式,咱们就实现了请求校验和业务逻辑的分离,并且这样十分便于开发和维护。.net

实现

前面说这么多,不知道读者是否已经有一套实现的思路了没。code

Pipeline 这个组件的功能十分明确,实现这种相似功能的确定很多,选择其做为表明分析,缘由就是其实现的方式很是简洁、有力,不但其实现原理如此,面对开发人员,它的调用方式也十分清晰,利用匿名函数使得前置与后置的调用都很直观,本文分析的重点就在这里。

实现的思路即便有了,在没有很好地基础以前,估计也很难去完成。固然不少人愿意去阅读其代码,这样就少走了很多弯路,在这里,个人建议也是这样。不过,不少人看到源码也很迷惑,由于中间存在着很是多的回调,只要基础不够扎实,就很容易在期间产生诸多困惑。

不过,逐步分析和对基础知识的补完,就会发现再复杂的框架也不过是零碎的功能有序的构建起来的。

array_reduce 的妙用

public function then(Closure $destination)
{
    $firstSlice = $this->getInitialSlice($destination);
    
    $callable = array_reduce(
        array_reverse($this->pipes), $this->getSlice(), $firstSlice
    );
    
    return $callable($this->passable);
}

上面的代码就是 Pipeline 启动过程的起点,固然在调用 then 方法以前咱们还有必要调用 sendthroughsend 是传递初始数据,through 则是传递须要经过的中间件构成的数组,不必赘述。

then 方法接受一个要求匿名函数的参数,该参数所接受的匿名函数,就是用于整个流程的逻辑处理部分的,数据穿过层层中间件,最终到达这里,因此该匿名函数可接受一个参数,就是通过过滤的数据啦。该方法囊括着全部功能,可是代码不过几行,所以确定有额外的调度过程。

代码中首先映入眼帘的就是 $this->getInitialSlice() ,该方法顾名思义,建立了一个初始化用的 Slice,这块咱们先不细说,由于随后就是本文的重点,亦是组件实现的 核心功能array_reduce 函数!。

array_reduce 函数的做用文档上写的十分详细,可至官方中文文档查阅:http://php.net/manual/zh/function.array-reduce.php

经过查阅文档,咱们可经过示例了解其做用本质就是经过用户自定义的方式去将一个数组合并成单一的一个值,所以该函数要求三个参数:待合并的数组、用于合并逻辑的回调函数、初始合并的值(亦或者特殊情境下的最终值),用于合并逻辑的回调须接受两个参数值,分别是上一次处理逻辑处理的结果(第一次不存在处理结果,则默认为空,若设置了 array_reduce 的第三个参数,则以该参数为初始值)和待处理的数组项。

Pipeline 组件恰到好处的使用了它。咱们看获得,Pipeline 首先将咱们用于处理的中间件数组经过 array_reverse 取相反顺序(至于为何这么作后面大家就知道了),传递至 array_reduce 的第一个参数。第三个参数做为 array_reduce 认定的默认处理对象,Pipeline 用的是先前经过 getInitalSlice 获取到的(其实是用户传进来的目标逻辑处理函数)做为值传递。

而后就是本文第二个介绍的重点,array_reduce 所接受的第二个参数,经过调用 $this->getSlice() 获取的一个匿名函数!

实现的核心

array_reduce 的第二个参数要求传递一个回调函数用于处理数组合并,$this->getSlice() 返回的正是这个处理函数,我相信大家必定看到了 getSlice 返回的值,那么我就将这个匿名函数单独拿出来:

function ($stack, $pipe) {
    return function ($passable) use ($stack, $pipe) {
        if ($pipe instanceof Closure) {
            return call_user_func($pipe, $passable, $stack);
        }
        // 省略了一部分,该部分是针对中间件 “类” 而不是中间件匿名函数的,
        // 先前例子中咱们用的都是以匿名函数做为数组传递进来的,所以只会进入上面那个条件,
        // 固然 Laravel 框架中,传递进来的则基本是中间件对象的类名,这段省略的代码,
        // 和上面那个 if 中的本质的区别就是,省略的代码中包含了中间件类的实例化过程并调用的是
        // 其 handle 方法而不是直接调用函数,仅此~~
    };
};

我知道你们看到的代码有不少行,可是实际上就只有一行 return function() { ... };,被执行的也只有它。对于一些初学者,很容易产生一种错觉:那个返回的 function 会在 return 前执行。既然是错觉,那就意味着不会被执行,而是做为一个值被返回,可能会被后续某个地方所调用!可能会被后续某个地方所调用!可能会被后续某个地方所调用!这里只是个值!重要的事情说三遍。

虽然说会被后面所调用,但咱们依旧要在这里提一下这个被返回的匿名函数,在这里,它又有着另外一个名称:闭包。闭包是由匿名函数(也成闭包函数)构成的一个总体,和普通的匿名函数有所不一样,闭包中必定存在引用了外部数据并在内部操做的状况。

这里须要注意,返回的不只仅是个匿名函数,更是一个闭包,该闭包中引用了两个外部值,分别是 array_reduce 提供给第二参数中的回调的两个参数,即数组合并结果和当前待合并的值。

第一次执行时,$stack 就是咱们的目标处理逻辑代码段,$pipe 则是第一个中间件;

第二次执行时,$stack 是第一次执行所返回的闭包,$pipe 则是第二个中间件,随后以此类推。

最后一次执行,返回的结果仍旧是一个闭包,该闭包中所引用的外部数据是倒数第二次的执行返回的闭包,$pipe 是最后一个中间件。随后,该闭包在 then 方法中被调用,传递进了咱们经过 send 方法传递的值。

上面的描述可能异常抽象,咱们让其变得稍微直观一些,我会将全部遍历每一次执行带来的变化体现出来。不过为了方便理解,我须要改一下示例代码,去掉中间的条件判断,由于咱们如今重点是理解这个流程而不是其功能,新的代码与执行结果以下:

<?php
use Illuminate\Pipeline\Pipeline;

$pipes = [
    function ($poster, $callback) {
        $poster += 1;
        return $callback($poster);
    },
    function ($poster, $callback) {
        $result = $callback($poster);

        return $result - 1;
    },
    function ($poster, $callback) {
        $poster += 2;

        return $callback($poster);
    }
];

echo (new Pipeline)->send(0)->through($pipes)->then(function ($poster) {
    return $poster;
}); // 执行输出为 2

上述代码,咱们定义了三个中间件,同时咱们的目标逻辑代码并没作什么特殊的事情,这样咱们就能够专一在执行流程上。下面便于分析,我作了一份伪代码以及等式方便理解:

poster     = 0
f^0        = f(z)->{ z }                     // 定义目标处理逻辑
f^1        = f(z, y)->{ f^y( z + 1 ) }       // 定义中间件 1
f^2        = f(z, y)->{ result = f^y(z); result - 1 }  // 定义中间件 2
f^3        = f(z, y)->{ f^y( z + 2 ) }       // 定义中间件 3
f^getSlice = f(y, x)->{
    f(z)->{
        call( f^x(z, y) )
    }
}

callback = array_reduce([f^3, f^2, f^1], f^getSlice, f^0);
callback(poster)

>>> 执行上述过程

exec^1:
    // 第一次进行 reduce,y 是目标逻辑片断,x 是最后一个中间件,被闭包引用,
    // 闭包则做为合并结果返回,在此定义为 f^a。
    y   = f^0(z);
    x   = f^3;
    f^a = f(z)->{ call( f^x(z, y) ) }
exec^2:
    // 第二次进行,y 是上次处理返回的闭包(即 f^a),x 是第二个中间件,再次生成闭包返回。
    y   = f^a;
    x   = f^2;
    f^b = f(z)->{ call( f^x(z, y) ) }
exec^3:
    // 第三次也是最后一次合并,同第二次。如今三个数组项被合并,
    // 合并结果为最后一次合并所返回的闭包。
    y   = f^b;
    x   = f^1;
    f^c = f(z)->{ call( f^x(z, y) ) }
exec^4:
    // 该闭包(最后一次合并结果)返回后,被调用,第一个参数为 z = poster = 1,开始执行。
    // 该闭包的 z 参数即为 1,其他如 x、y 值见 exec^3。
    call( f^c(0) ) = call( f^1(0, f^b) )
exec^5:
    // 继续等式替换
    call( f^b(0 + 1) ) = call( f^2(0 + 1, f^a) )
exec^6:
    // 根据上已执行过程返回结果,已执行至中间件 2 的回调,继续等式替换
    result = f^a(0 + 1); result - 1
exec^7:
    result = call( f^3(0 + 1 , f^0) ); result - 1
exec^8:
    result = call( f^0(0 + 1 + 2) ); result - 1
exec^9:
    result = 3; result - 1

// 处理结果
result: 2

分析

根据伪代码,和执行过程,咱们能了解到先前经过 array_reverse 反序排列的中间件,因为在本文中,此处闭包逆向传递下去的特性(由于所引用的外部参数中,是前一执行结果所返回的闭包),实际上依旧是按顺序执行的,咱们在这里也看到了如何利用该特性,实现前置和后置调用的原理以及拦截的原理。

前置调用时,先处理自上传递下来的结果,随后调用下一个(由中间件构成的)闭包。后置调用时,先调用下一个(有中间件构成的)闭包,里面仍旧可能无数的引用,直到其中的目标处理逻辑,最终返回结果,再处理。

拦截的原理就更简单了,因为拦截只存在于前置中间件,而前置中间件是先处理,而后调用传递进来的闭包并返回其值,而若这个值不是来自于一个闭包调用的结果,就意味着确定中间不存在调用关系,也就根本不会执行到闭包中的下一个中间件。

总结

以上就是整个 Pipeline 以及中间件的实现,我知道不少人依旧十分纠结,心里充满困惑。我仍旧建议老老实实,从 array_reduce 这个函数的实际功能着手,而后把每一步执行过程,写下来,慢慢的就明白了。这篇文章不只仅只是 Laravel 组件的一个讲解,更可能是从中发现 PHP 的一些基础概念和知识,要知道在强大的 PHP 框架也是用 PHP 写出的,本质上仍旧是在一个大的基础上构建的小世界而已。

因此做为一名 PHPer,永远不要忘了,你是在写 PHP 的代码。

个人博客地址:https://www.insp.top

相关文章
相关标签/搜索