Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

理解Laravel中间件核心实现原理 #69

Open
woodongwong opened this issue Aug 28, 2022 · 0 comments
Open

理解Laravel中间件核心实现原理 #69

woodongwong opened this issue Aug 28, 2022 · 0 comments

Comments

@woodongwong
Copy link
Owner

Laravel路由中间件是一个洋葱模型,http请求会从第一个中间件经过第二个中间件、第三个中间件,最后到达控制器(一般情况下),然后再从第三个中间件返回至第二个中间件,再返回至第一个中间件,整个路线像洋葱一样一层一层的。

图片

我们将其模型和功能简单化,将每一个中间件类中的handle方法看作是一个普通的方法,然后先理解洋葱模型的左半部分。这非常简单,假如有方法f1、f2、f3,其左边的调用顺序就是f1->f2->f3,即f3(f2(f1())),我们换一种代码形态:

$run = function() {              // 将匿名方法赋值给变量run,当执行 $run() 时,相当于调用了这一层方法,返回的是f1方法
    return function () {         // 相当于f1,当执行 $run()() 时,相当于调用了这一层方法,输出f1,返回的是f2方法
        echo 'f1';
        return function () {     // 相当于f2,当执行 $run()()() 时,相当于调用了这一层方法,输出f2,返回的是f3方法
            echo 'f2';
            return function () { // 相当于f3,当执行 $run()()()() 时,相当于调用了这一层方法,输出f3,没有返回值
                echo 'f3';
            };
        };
    };
};

$run()()()(); // 输出:f1 f2 f3

(示例1)

假如有N个方法,难道要$run()()()()...N;?这个问题可以通过以下代码结构解决:

$run = function() {              // 将匿名方法赋值给变量run
    return (function() {         // 相当于f1,自动调用,输出f1并返回调用结果
        echo 'f1';
        return (function() {     // 相当于f2,自动调用,输出f2并返回调用结果
            echo 'f2';
            return (function() { // 相当于f3,自动调用,输出f3,没有返回值
                echo 'f3';
            })();
        })();
    })();
};

$run(); // 输出:f1 f2 f3

(示例2)

注:PHP7+版本,匿名方法自动调用方式(IIFE):(function(){})();php7以下版本:call_user_func(function(){})

上面两段代码展示了洋葱模型的左半部分,相信你已经知道右半部分怎么实现了,没错就是这样:

$run = function() {
    return (function() {
        echo 'f1-left';
        $result = (function() {
            echo 'f2-left';
            $result = (function() {
                echo 'f3';
            })();

            echo 'f2-right';
            return $result;
        })();

        echo 'f1-right';
        return $result;
    })();
};

$run(); // 输出:f1-left f2-left f3 f2-right f1-right

(示例3)

我们再实现一个传参版的:

$run = function(array $arr) {
    return (function(array $arr) {
        $arr[] = 'f1-left';
        $arr = (function(array $arr) {
            $arr[] = 'f2-left';
            $arr = (function(array $arr) {
                $arr[] = 'f3';
                return $arr;
            })($arr);

            $arr[] = 'f2-right';
            return $arr;
        })($arr);

        $arr[] = 'f1-right';
        return $arr;
    })($arr);
};

$arr = $run(['start']);
$arr[] = 'end';
print_r($arr); // 输出:
/*
Array
(
   [0] => start
   [1] => f1-left
   [2] => f2-left
   [3] => f3
   [4] => f2-right
   [5] => f1-right
   [6] => end
)
*/

(示例4)

上面的例子都是展开的代码来展示原理,那我们怎么样才能实现下面这样灵活的调用方式呢?

$arr = (new Pipeline(['f1', 'f2', 'f3', ...]))->run(['start']);
$arr[] = 'end';
// 定一个 Pipeline 类
class Pipeline
{
    // 存放f1 f2 f3...N的方法名
    protected $pipes = [];

    public function __construct(array $pipes)
    {
        $this->pipes = $pipes;
    }

    public function run($data)
    {
        // 需定义一个“芯”,这样就f1 f2 f3都可以使用统一的参数,就不需要指定f3作为最中间的芯了
        $stack = function($data) {
            return $data;
        };

        // php7.4可以这样写: $stack = fn($data) => $data;
        
        // f1 f2 f3 变成 f3 f2 f1,因为需要先从“洋葱芯”开始包装
        $pipes = array_reverse($this->pipes);

        // 循环包装每一个方法
        foreach ($pipes as $pipe) {
            // 每次循环,$stack的层级都会增加
            $stack = function ($data) use($pipe, $stack) {
                return $pipe($data, $stack);
            };
        }

        // 相当于上面例子中的 $run();
        return $stack($data);
    }
}

/**
 * @param array    $arr
 * @param \Closure $next 匿名方法
 *
 * @return mixed
 */
function f1(array $arr, \Closure $next)
{
    $arr[] = 'f1-left';
    $arr = $next($arr);
    $arr[] = 'f1-right';
    return $arr;
}

/**
 * @param array    $arr
 * @param \Closure $next 匿名方法
 *
 * @return mixed
 */
function f2(array $arr, \Closure $next)
{
    $arr[] = 'f2-left';
    $arr = $next($arr);
    $arr[] = 'f2-right';
    return $arr;
}

/**
 * @param array    $arr
 * @param \Closure $next 匿名方法,这里比上面例子中的f3多了一个参数,因为Pipeline::run中定义了一个“芯”
 *
 * @return mixed
 */
function f3(array $arr, \Closure $next)
{
    $arr[] = 'f3';
    $arr = $next($arr);
    return $arr;
}

$arr = (new Pipeline(['f1', 'f2', 'f3']))->run(['start']);
$arr[] = 'end';
print_r($arr);  // 输出:
/*
Array
(
    [0] => start
    [1] => f1-left
    [2] => f2-left
    [3] => f3
    [4] => f2-right
    [5] => f1-right
    [6] => end
)
*/

(示例5)

比较难理解的是foreach部分,我们再分解一下,注意观察一下$stack的值的变化:

// 第0次循环
$stack = function($data) {
    return $data;
};

// 循环每一个方法
$pipes = ['f3', 'f2', 'f1'];
foreach ($pipes as $pipe) {
    // 每次循环,$stack的层级都会增加
    $stack = function ($data) use($pipe, $stack) {
        return $pipe($data, $stack);
    };

    // 第1次循环
    $stack = function($data) {
        return f3($data, function($data) {
            return $data;
        });
    };

    // 第2次循环
    $stack = function($data) {
        return f2($data, function ($data) {
            return f3($data, function($data) {
                return $data;
            });
        });
    };

    // 第3次循环
    $stack = function($data) {
        return f1($data, function ($data) {
            return f2($data, function ($data) {
                return f3($data, function($data) {
                    return $data;
                });
            });
        });
    };
}

(示例6)

通过示例6的三次循环可以看出,刚好和示例4相吻合。到这里我们通过上面的几个示例可以明白Laravel路由中间件的核心实现原理。其实代码还可以使用array_reduce函数来代替示例5中的foreach,laravel中就是使用的这个方法

public function run($data)
{
    return array_reduce(array_reverse($this->pipes), function ($stack, $item) {
        return function ($data) use($item, $stack) {
            return $item($data, $stack);
        };
    }, fn($data) => $data)($data);
}

(示例7)

参考:

扩展阅读:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant