laravel 源码解析之 Pipeline

解析 Pipeline 之前,我觉得有必要好好看一下 这个 $request 是怎么生成的,底层用了 Symfony 的 request 类。

$response = $kernel->handle(
    $request = Illuminate\Http\Request::capture()
);

首先进入 capture 方法

public static function capture()
{
    # 开启 http 方法重写,之前一篇文章已经讲到过了,你也可以看看它的注释,你会懂的
    static::enableHttpMethodParameterOverride();

    return static::createFromBase(SymfonyRequest::createFromGlobals());
}

tips: application/x-www-form-urlencoded: 数据被编码成以 '&' 分隔的键-值对, 同时以 '=' 分隔键和值. 非字母或数字的字符会被 percent-encoding: 这也就是为什么这种类型不支持二进制数据的原因 (应使用 multipart/form-data 代替).

# parse_str 的使用
<?php
$str = "first=value&arr[]=foo+bar&arr[]=baz";

// Recommended
parse_str($str, $output);
echo $output['first'];  // value
echo $output['arr'][0]; // foo bar
echo $output['arr'][1]; // baz
public static function createFromGlobals()
{
    # 拿到全局变量中的 $_GET, $_POST, $_COOKIE,$_FILES,$_SERVER
    $request = self::createRequestFromFactory($_GET, $_POST, array(), $_COOKIE, $_FILES, $_SERVER);

    # 前半句判断请求是否是form表单的形式发送的,后半句判断请求的方法是否是 put patch 或者 delete,因为php本身不支持 put 或者 delete 方法
    if (0 === strpos($request->headers->get('CONTENT_TYPE'), 'application/x-www-form-urlencoded')
        && \in_array(strtoupper($request->server->get('REQUEST_METHOD', 'GET')), array('PUT', 'DELETE', 'PATCH'))
       ) {
        # 如果是 application/x-www-form-urlencoded 形式的,那么需要解析参数
        parse_str($request->getContent(), $data);
        # 把初始化的 request 覆盖掉
        $request->request = new ParameterBag($data);
    }

    return $request;
}
private static function createRequestFromFactory(
    array $query = array(), 
    array $request = array(), 
    array $attributes = array(), 
    array $cookies = array(), 
    array $files = array(), 
    array $server = array(), 
    $content = null
) {
    # 如果已经有实例? $requestFactory ,这一步情景暂时看不出,但可以肯定的是一个新的请求,这个判断返回 false
    if (self::$requestFactory) {
        $request = \call_user_func(self::$requestFactory, $query, $request, $attributes, $cookies, $files, $server, $content);

        if (!$request instanceof self) {
            throw new \LogicException('The Request factory must return an instance of Symfony\Component\HttpFoundation\Request.');
        }

        return $request;
    }

    # 生成一个 request 实例
    return new static($query, $request, $attributes, $cookies, $files, $server, $content);
}
# Request 构造函数
public function __construct(
    array $query = array(), 
    array $request = array(), 
    array $attributes = array(), 
    array $cookies = array(), 
    array $files = array(), 
    array $server = array(), 
    $content = null
) {
    $this->initialize($query, $request, $attributes, $cookies, $files, $server, $content);
}

初始化了相关类,这里把不同的属性不同的参数给到不同的类做了解耦,good!

public function initialize(
    array $query = array(), 
    array $request = array(), 
    array $attributes = array(), 
    array $cookies = array(), 
    array $files = array(), 
    array $server = array(), 
    $content = null
) {
    $this->request = new ParameterBag($request);
    $this->query = new ParameterBag($query);
    $this->attributes = new ParameterBag($attributes);
    $this->cookies = new ParameterBag($cookies);
    $this->files = new FileBag($files);
    $this->server = new ServerBag($server);
    $this->headers = new HeaderBag($this->server->getHeaders());

    $this->content = $content;
    $this->languages = null;
    $this->charsets = null;
    $this->encodings = null;
    $this->acceptableContentTypes = null;
    $this->pathInfo = null;
    $this->requestUri = null;
    $this->baseUrl = null;
    $this->basePath = null;
    $this->method = null;
    $this->format = null;
}
# 注意这里传入的 $request 实例!这个实例是 SymfonyRequest 的实例,而第一句判断中的 static 是 Illuminate\Http\Request,所以是 false,laravel 框架还要对其进一步包装
public static function createFromBase(SymfonyRequest $request)
{
    if ($request instanceof static) {
        return $request;
    }

    $content = $request->content;

    # laravel clone 了一份 request
    $request = (new static)->duplicate(
        $request->query->all(), $request->request->all(), $request->attributes->all(),
        $request->cookies->all(), $request->files->all(), $request->server->all()
    );

    $request->content = $content;
    # 如果请求给的是json数据格式,那么将其转化为 ParameterBag 实例,因为 SymfonyRequest 对 json 请求格式没有做处理
    $request->request = $request->getInputSource();

    return $request;
}
Symfony 的 Request
laravel 本身的 Request

了解了 request 的实例过程之后,我们来看 Pipeline

return (new Pipeline($this->app)) # 传入 app 实例(单例)
    ->send($request) # 传入 request,赋值给 Pipeline 的 passable 属性
    ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware) # 传入中间件,这个判断是为了测试时候禁用中间件而写的,把 对应的中间件类传入,赋值给 Pipeline 的 pipes
    ->then($this->dispatchToRouter());
public function then(Closure $destination)
{
    # array_reduce 如果有三个参数,先看第三个,它初始化传入的值
    $pipeline = array_reduce(
        array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination)
    );

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

# 第三个参数传入了dispatchToRouter这个闭包
protected function dispatchToRouter()
{
    return function ($request) {
        $this->app->instance('request', $request);

        return $this->router->dispatch($request);
    };
}
protected function carry()
{
    # $stack -> 闭包,$pipe -> 在这里是中间件的类名
    return function ($stack, $pipe) {
        return function ($passable) use ($stack, $pipe) {
            if (is_callable($pipe)) {

                return $pipe($passable, $stack);
            } elseif (! is_object($pipe)) {
                [$name, $parameters] = $this->parsePipeString($pipe);

                $pipe = $this->getContainer()->make($name);

                $parameters = array_merge([$passable, $stack], $parameters);
            } else {

                $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;
        };
    };
}
$pipeline = array_reduce(
    array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination)
);


# 合并。最终这个 reduce 将形成一个大大的闭包函数,类似递归,一层嵌套一层
array_reduce(
    array_reverse($this->pipes), 
    function ($stack, $pipe) {
        return function ($passable) use ($stack, $pipe) {
            if (is_callable($pipe)) {

                return $pipe($passable, $stack);
            } elseif (! is_object($pipe)) {
                [$name, $parameters] = $this->parsePipeString($pipe);

                $pipe = $this->getContainer()->make($name);

                $parameters = array_merge([$passable, $stack], $parameters);
            } else {

                $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;
        };
    }
    , function ($passable) use ($destination) {
        return function ($passable) {
            $this->app->instance('request', $passable);

            return $this->router->dispatch($passable);
        };
    };
);

源码不愧是源码,很难理解,那么我这里先来个简单的

<?php
# laravel pipeline 的简单实现,pipeline 主要用来对数据进行管道化处理,类似于拦截器,层层过滤数据
# 请求 -> 管道1 —> 管道2 -> 管道3 -> 业务逻辑 -> 后置管道 -> 返回结果

 function runPipeline()
{
    return $result = array_reduce(
        array_reverse([new Handler1, new Handler2, new Handler3, new Handler4]),
        function ($start, $handler) {
            return function () use ($start, $handler) {
                $result =  $handler->hanlde($start);

                return $result;
            };
    }, function () {
        echo "处理核心业务逻辑\n";
    });
}

$result = runPipeline();
$result();

/**
 * 前置管道1
 */
class Handler1
{
    function hanlde($callback)
    {
        echo '处理器1' . "\n";

        return $callback();
    }
}

/**
 * 前置管道2
 */
class Handler2
{
    function hanlde($callback)
    {
        echo '处理器2' . "\n";

        return $callback();
    }
}

/**
 * 后置管道3
 */
class Handler3
{
    function hanlde($callback)
    {
        $result = $callback();

        echo '处理器3' . "\n";

        return $result;
    }
}

/**
 * 前置管道4
 */
class Handler4
{
    function hanlde($callback)
    {
        echo '处理器4' . "\n";

        return $callback();
    }
}

为什么要 array_reverse

# 管道最终的包装之后是这样的,正如 laravel 源码中的 $this->prepareDestination($destination) 是核心逻辑,然后它外面包的是一个有一个的管道,也就是中间件,最先进入的 最后执行,所以取反之后,最先进入的最先执行
function duc() {
    $callback1 = function () {
        echo '处理器1' . "\n";

        $callback2 = function () {
            echo '处理器2' . "\n";

            $callback3 = function () {
                echo "处理核心业务逻辑\n";
            };

            $callback3();
        };

        $callback2();
    };

    $callback1();
}

duc();

前置和后置?

前置和后置其实就是执行顺序问题,只要把上面的echo放到 callback 之后,就能实现后置

function duc() {
    $callback1 = function () {
        echo '处理器1' . "\n";

        $callback2 = function () {

            $callback3 = function () {
                echo "处理核心业务逻辑\n";
            };

            $callback3();
            
            echo '处理器2' . "\n";
        };

        $callback2();
    };

    $callback1();
}

duc();

这个pipeline 在laravel 中中间件处理的时候得到了应用,去看文档你会发现中间件的前置和后置和本文的用法一样

到这里你就应该知道中间件的用法了吧!我们继续看下去,由上文可知,$this->prepareDestination($destination) 才是核心逻辑实现的地方,下面贴代码

# passable 是 request 实例,在 pipeline send 的时候传入的
return function ($passable) {
    $this->app->instance('request', $passable);

    return $this->router->dispatch($passable);
};
public function dispatch(Request $request)
{
    # 为 router  类添加了类属性
    $this->currentRequest = $request;

    # 分发路由
    return $this->dispatchToRoute($request);
}
public function dispatchToRoute(Request $request)
{
    return $this->runRoute($request, $this->findRoute($request));
}
# 这一步是去从你的路由中寻找相应的控制器和方法
# 你可以打印router类,至于框架什么时候解析了你的路由文件,我告诉你,它是在  bootstrap 阶段,运行 BootProviders 的bootstrap方法,调用所有 providers 的 boot 方法时加载的
protected function findRoute($request)
{
    $this->current = $route = $this->routes->match($request);

    # 框架还把其赋值给了 Route,也就是说,你在控制器中用 app(\Illuminate\Routing\Route::class) 拿到是是下图路由数组中的某一个路由对象,比如 / 你拿到的就是下图中 / 对应的键值,注意这里 app(\Illuminate\Routing\Route::class) !== app('route')
    $this->container->instance(Route::class, $route);

    return $route;
}
Router
protected function runRoute(Request $request, Route $route)
{
    $request->setRouteResolver(function () use ($route) {
        return $route;
    });

    # 触发事件,调用对应的 listener
    $this->events->dispatch(new Events\RouteMatched($route, $request));

    return $this->prepareResponse($request,
        $this->runRouteWithinStack($route, $request)
    );
}
protected function runRouteWithinStack(Route $route, Request $request)
{
    $shouldSkipMiddleware = $this->container->bound('middleware.disable') &&
        $this->container->make('middleware.disable') === true;

    $middleware = $shouldSkipMiddleware ? [] : $this->gatherRouteMiddleware($route);

    # ???? wtf 这里怎么还有管道
    # 大家都懂的,核心业务代码就是 then 里面的部分
    return (new Pipeline($this->container))
        ->send($request)
        ->through($middleware)
        ->then(function ($request) use ($route) {
            return $this->prepareResponse(
                $request, $route->run()
            );
        });
}
# run 执行控制器里面的方法
public function run()
{
    $this->container = $this->container ?: new Container;

    try {
        if ($this->isControllerAction()) {
            return $this->runController();
        }

        return $this->runCallable();
    } catch (HttpResponseException $e) {
        return $e->getResponse();
    }
}
# toResponse 转换为相应的返回格式
public static function toResponse($request, $response)
{
    if ($response instanceof Responsable) {
        $response = $response->toResponse($request);
    }

    if ($response instanceof PsrResponseInterface) {
        $response = (new HttpFoundationFactory)->createResponse($response);
    } elseif ($response instanceof Model && $response->wasRecentlyCreated) {
        $response = new JsonResponse($response, 201);
    } elseif (! $response instanceof SymfonyResponse &&
              ($response instanceof Arrayable ||
               $response instanceof Jsonable ||
               $response instanceof ArrayObject ||
               $response instanceof JsonSerializable ||
               is_array($response))) {
        $response = new JsonResponse($response);
    } elseif (! $response instanceof SymfonyResponse) {
        $response = new Response($response);
    }

    if ($response->getStatusCode() === Response::HTTP_NOT_MODIFIED) {
        $response->setNotModified();
    }

    return $response->prepare($request);
}

再回过头来看 handle 方法

public function handle($request)
{
    try {
        $request->enableHttpMethodParameterOverride();

        # 这里的种种操作最后返回的是 Symfony\Component\HttpFoundation\Response 实例
        $response = $this->sendRequestThroughRouter($request);
    } catch (Exception $e) {
        $this->reportException($e);

        $response = $this->renderException($request, $e);
    } catch (Throwable $e) {
        $this->reportException($e = new FatalThrowableError($e));

        $response = $this->renderException($request, $e);
    }

    # 请求结束还触发了事件
    $this->app['events']->dispatch(
        new Events\RequestHandled($request, $response)
    );

    return $response;
}
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

$response = $kernel->handle(
    $request = Illuminate\Http\Request::capture()
);
# 最后把相应返回给客户端
$response->send();

# 处理中间件中的 terminate 方法
$kernel->terminate($request, $response);
/**
* 
* Sends HTTP headers and content.
*
* @return $this
*/
public function send()
{
    $this->sendHeaders();
    $this->sendContent();

    if (\function_exists('fastcgi_finish_request')) {
        fastcgi_finish_request();
    } elseif (!\in_array(\PHP_SAPI, array('cli', 'phpdbg'), true)) {
        static::closeOutputBuffers(0, true);
    }

    return $this;
}

再多讲一句

# 这个阶段你再dd或者dump是不会有任何输出的,因为服务端已经返回了
$kernel->terminate($request, $response);

# 比如 session 的保存和关闭处理
public function terminate($request, $response)
{
    if ($this->sessionHandled && $this->sessionConfigured() && ! $this->usingCookieSessions()) {
        $this->manager->driver()->save();
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,830评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,992评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,875评论 0 331
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,837评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,734评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,091评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,550评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,217评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,368评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,298评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,350评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,027评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,623评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,706评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,940评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,349评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,936评论 2 341

推荐阅读更多精彩内容

  • 利用HTTP协议向服务器传参的几种途径、响应、Cookie、Session、类视图、中间件 注意: 1>Dja...
    Cestine阅读 1,231评论 0 2
  • 先说几句废话,调和气氛。事情的起由来自客户需求频繁变更,伟大的师傅决定横刀立马的改革使用新的框架(created ...
    wsdadan阅读 3,029评论 0 12
  • 知名的 Pipeline 模式 unix 的 pipeline 读取文本内容,并过滤 “hello world”,...
    智勇双全的小六阅读 28,826评论 0 3
  • Laravel是我最熟悉的框架,却一直没认真的梳理过它的源码,甚是遗憾,遂在此梳理一遍laravel的源码。 请求...
    岳凯阅读 792评论 0 1
  • 什么是中间键 对于一个Web应用来说,在一个请求真正处理前,我们可能会对请求做各种各样的判断,然后才可以让它继续传...
    伊Summer阅读 1,658评论 0 3