iT邦幫忙

2019 iT 邦幫忙鐵人賽

0
Software Development

Laravel 原始碼分析系列 第 43

分析 Lumen Application--dispatch() 上篇

Lumen 在處理跟 Request 相關的程式,都放在 RoutesRequests 這個 trait 裡,包括今天要看的 dispatch()

public function dispatch($request = null)
{
    // 解析 Request,並取得 method 與 pathInfo
    list($method, $pathInfo) = $this->parseIncomingRequest($request);

    try {
        // 啟動程式,這跟 Laravel Application boot() 大同小異
        $this->boot();

        // 使用 pipeline 
        return $this->sendThroughPipeline($this->middleware, function () use ($method, $pathInfo) {
            // 取得所有 route 並看看對應的 method + pathInfo 會不會找得到
            if (isset($this->router->getRoutes()[$method.$pathInfo])) {
                // 找得到就處理它
                return $this->handleFoundRoute([true, $this->router->getRoutes()[$method.$pathInfo]['action'], []]);
            }

            // 找不到就交由 FastRoute 處理
            return $this->handleDispatcherResponse(
                $this->createDispatcher()->dispatch($method, $pathInfo)
            );
        });
    } catch (Exception $e) {
        return $this->prepareResponse($this->sendExceptionToHandler($e));
    } catch (Throwable $e) {
        return $this->prepareResponse($this->sendExceptionToHandler($e));
    }
}

這段程式碼,除了 FastRoute 外,其他都是之前有分析過的橋段,如 boot() 裡,會對所有 ServiceProvider 呼叫 boot();或是 Pipeline 操作等。

這段程式碼,其中有幾個地方是我們可以再深入追下去的,如:

  • parseIncomingRequest() 如何解析出 method 或 pathInfo,以及後面為何 $this->router->getRoutes() 有可能會找不到
  • handleFoundRoute() 如何處理找到的 route
  • createDispatcher() 如何產生 FastRoute,以及 handleDispatcherResponse() 如何使用
  • sendExceptionToHandler() 如何產生
  • prepareResponse() 如何建置 Response

parseIncomingRequest()

protected function parseIncomingRequest($request)
{
    // 如果 request 是 null,就使用 Lumen Request 依環境建置出來
    if (! $request) {
        $request = LumenRequest::capture();
    }

    // 建完立馬存 container,prepareRequest() 下面會提到
    $this->instance(Request::class, $this->prepareRequest($request));

    // 解析出 Request 後,就能輕鬆拿到 method 或 pathInfo 了
    return [$request->getMethod(), '/'.trim($request->getPathInfo(), '/')];
}

protected function prepareRequest(SymfonyRequest $request)
{
    // 如果 request 是 SymfonyRequest,則重建一個新的 Laravel Request
    if (! $request instanceof Request) {
        $request = Request::createFromBase($request);
    }

    // 上面要這樣做的原因是,這裡要設定 UserResolver 與 RouteResolver
    $request->setUserResolver(function ($guard = null) {
        return $this->make('auth')->guard($guard)->user();
    })->setRouteResolver(function () {
        return $this->currentRoute;
    });

    return $request;
}

從原始碼可以知道,從 Request 即可拿到很多資訊了。

handleFoundRoute()

有了 $routeInfo,就能拿來做處理,它的長像如下:

$routeInfo = [true, $this->router->getRoutes()[$method.$pathInfo]['action'], []]

handleFoundRoute() 的原始碼:

protected function handleFoundRoute($routeInfo)
{
    // 注意,這裡的 currentRoute 屬性,剛好是上面 RouteResolver 的回傳
    $this->currentRoute = $routeInfo;

    // 只是搞不懂這裡為何又要再重覆一次設定 RouteResolver,呼叫的順序確認過一定會是 parseIncomingRequest() 後才 handleFoundRoute()
    $this['request']->setRouteResolver(function () {
        return $this->currentRoute;
    });

    // 這會從 Router 取得對應 Route 裡,拿到 action
    $action = $routeInfo[1];

    // Pipe through route middleware...
    if (isset($action['middleware'])) {
        // 有 middleware 的話,就集合起來
        $middleware = $this->gatherMiddlewareClassNames($action['middleware']);

        // 使用另外一組 Pipeline 處理
        return $this->prepareResponse($this->sendThroughPipeline($middleware, function () {
            return $this->callActionOnArrayBasedRoute($this['request']->route());
        }));
    }

    // 沒 middleware 就直接處理
    return $this->prepareResponse(
        $this->callActionOnArrayBasedRoute($routeInfo)
    );
}

這裡使用另一組 Pipeline 的方法與 Laravel 是一樣的,可以參考解析 Middleware 的實作細節

而最後實際呼叫的方法寫在 callActionOnArrayBasedRoute() 裡。

protected function callActionOnArrayBasedRoute($routeInfo)
{
    $action = $routeInfo[1];

    // 有 uses 代表是 Controller,使用 Controller 的方法呼叫
    if (isset($action['uses'])) {
        return $this->prepareResponse($this->callControllerAction($routeInfo));
    }

    // 如果有找到 Closure 就 bind 到 RoutingClosure
    foreach ($action as $value) {
        if ($value instanceof Closure) {
            $closure = $value->bindTo(new RoutingClosure);
            break;
        }
    }

    try {
        // 使用 Container::call() 呼叫 Closure
        return $this->prepareResponse($this->call($closure, $routeInfo[2]));
    } catch (HttpResponseException $e) {
        return $e->getResponse();
    }
}

今天先告一段落,明天繼續看剩下三個方法。


上一篇
分析 bootstrap 流程--Lumen 篇
下一篇
分析 Lumen Application--dispatch() 下篇
系列文
Laravel 原始碼分析46

尚未有邦友留言

立即登入留言