一步一步实现 laravel 洋葱模型 [pipeline, middleware]
23 September 2018

介绍

下面的内容来自 ouzan/php-co-koa ,文笔不行,直接 copy 吧。

Ruby Rack, Python django, Golang matini, Node Express, PHP Laravel, Java Spring ,Web 框架大多会有一个面向 AOP 的中间件模块,内部操纵 Req/Res 对象可选执行 next 动作,Koa 与 martini 类似,都属于设计清爽的中间件 web 框架,采用洋葱模型 middleware stack,但合并了 Request 与 Response 对象,编写更直观方便。

一图胜千言

实现

在下面的代码中,假设 f1, f2, f3 都是一个 middleware

function f1(Closure $next)
{
    echo 'pre f1' . PHP_EOL;
    $next();
    echo 'post f1' . PHP_EOL;
}

function f2(Closure $next)
{
    echo 'pre f2' . PHP_EOL;
    $next();
    echo 'post f2' . PHP_EOL;
}

function f3(Closure $next)
{
    echo 'pre f3' . PHP_EOL;
    $next();
    echo 'post f3' . PHP_EOL;
}

// 我们想要的输出结果是
// pre f1
// pre f2
// pre f3
// post f3
// post f2
// post f1

第一步

f1(function () {
    f2(function () {
        f3(function(){

        });
    });
});

第二步

$t3 = function () {
    f3(function (){});
};
$t2 = function () use ($t3){
    f2($t3);
};
$t1 = function () use ($t2)
{
    f1($t2);
};
$t1();

再简化

$next = function () {
    f3(function (){});
};
$next = function () use ($next){
    f2($next);
};
$next = function () use ($next)
{
    f1($next);
};
$next();

第三步

$arr = ['f3', 'f2', 'f1'];
$next = function (){};
foreach ($arr as $k => $f) {
    $next = function () use ($f, $next) {
        $f($next);
    };
}
$next();

美化

$arr = ['f1', 'f2', 'f3'];  // 按正常顺序来初始化
$next = function (){};
foreach (array_reverse($arr) as $k => $f) {
    $next = function () use ($f, $next) {
        $f($next);
    };
}
$next();

第四步,使用 array_reduce

$arr = [1, 2, 3, 4];
$value = array_reduce($arr, function ($carry, $item) {
    return $carry * $item;
}, 1);
echo $value . PHP_EOL;          // 24


$arr = [1, 2, 3, 4];
$func = array_reduce($arr, function ($carry, $item) {
    return function () use ($carry, $item) {
        echo $item . " ";
        $carry($item);
        echo $item . " ";
    };
}, function () {
    echo ' you call me ';
});
echo $func() . PHP_EOL;        // 4 3 2 1 you call me 1 2 3 4
$arr = ['f1', 'f2', 'f3'];  // 按正常顺序来初始化
$next = function (){};
$func = array_reduce(array_reverse($arr), function ($next, $f) {
    return function () use ($next, $f) {
        $f($next);
    };
}, $next);
$func();

laravel 实现

分析 laravel 框架的入口文件 index.php

$app = require __DIR__ . '/../bootstrap/app.php';

// app.php 里面注册了 Kernel::class
//   $app->singleton(
//       Illuminate\Contracts\Http\Kernel::class,
//       App\Http\Kernel::class
//   );
//
// App\Http\Kernel 这个文件并非真正的核心,真正的核心是 Illuminate\Foundation\Http\Kernel,
// 这里是利用了 extends 来屏蔽用户无关的代码,只暴露用户需要关心的(面向对象的封装)
//
//   use Illuminate\Foundation\Http\Kernel as HttpKernel;
//   class Kernel extends HttpKernel
//   {
//        ...
//   }

$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
$response = $kernel->handle($request = Illuminate\Http\Request::capture());
$response->send();
$kernel->terminate($request, $response);

laravel 的核心是 kernel 里面的 handler 操作

class Kernel implements KernelContract
{
    public function handle($request)
    {
        // ...
        $response = $this->sendRequestThroughRouter($request);

        // ...
    }

    protected function sendRequestThroughRouter($request)
    {
        $this->app->instance('request', $request);

        Facade::clearResolvedInstance('request');

        $this->bootstrap();

        return (new Pipeline($this->app))
            ->send($request)
            ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
            ->then($this->dispatchToRouter());
    }
}

laravel 实现洋葱模型的骚操作就在 pipeline.php 里面的 then 方法中

class Pipeline implements PipelineContract
{
    public function through($pipes)
    {
        $this->pipes = is_array($pipes) ? $pipes : func_get_args();
        return $this;
    }

    public function then(Closure $destination)
    {
        // $this->pipes 就是 request 的 middleware
        $pipeline = array_reduce(
            array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination)
        );

        return $pipeline($this->passable);
    }

    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)) {
                    list($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;
            };
        };
    }

    protected function prepareDestination(Closure $destination)
    {
        return function ($passable) use ($destination) {
            return $destination($passable);
        };
    }
}