當我想分析 Laravel 是如何做到從 Request -> Response 的解析過程的,發現 Lumen 相對簡單,所以今天從 Lumen 原始碼入手,說一說Request -> Response 的解析過程
載入 Router
我們使用 Lumen 專案時,都是通過建立 route
,將請求的方法 method
、路徑 uri
和執行 action
關聯在一起,用於解析 Request
。
如:
<?php
/*
|--------------------------------------------------------------------------
| Application Routes
|--------------------------------------------------------------------------
|
| Here is where you can register all of the routes for an application.
| It is a breeze. Simply tell Lumen the URIs it should respond to
| and give it the Closure to call when that URI is requested.
|
*/
// 1️⃣
$router->get('/', function () use ($router) {
return "hello yemeishu ".$router->app->version();
});
// 2️⃣
$router->post('data', 'TempController@index');
我先看看 $router
怎麼來的:
/**
* Create a new Lumen application instance.
*
* @param string|null $basePath
* @return void
*/
public function __construct($basePath = null)
{
if (! empty(env('APP_TIMEZONE'))) {
date_default_timezone_set(env('APP_TIMEZONE', 'UTC'));
}
$this->basePath = $basePath;
$this->bootstrapContainer();
$this->registerErrorHandling();
// 這是 Router 引導函式
$this->bootstrapRouter();
}
...
/**
* Bootstrap the router instance.
*
* @return void
*/
public function bootstrapRouter()
{
$this->router = new Router($this);
}
有了 $this->router = new Router($this)
,我們就看 Lumen 是如何裝載 routes
的?
$app->router->group([
'namespace' => 'App\Http\Controllers',
], function ($router) {
require __DIR__.'/../routes/web.php';
});
...
/**
* Register a set of routes with a set of shared attributes.
*
* @param array $attributes
* @param \Closure $callback
* @return void
*/
public function group(array $attributes, \Closure $callback)
{
if (isset($attributes['middleware']) && is_string($attributes['middleware'])) {
$attributes['middleware'] = explode('|', $attributes['middleware']);
}
$this->updateGroupStack($attributes);
call_user_func($callback, $this);
array_pop($this->groupStack);
}
先判斷傳入的 $attributes
是否有中介軟體「middleware
」,有則解析成陣列一併匯入到 $this->groupStack[]
中,具體關聯的函式如下,程式碼簡單就不做分析了:
/**
* Update the group stack with the given attributes.
*
* @param array $attributes
* @return void
*/
protected function updateGroupStack(array $attributes)
{
if (! empty($this->groupStack)) {
$attributes = $this->mergeWithLastGroup($attributes);
}
$this->groupStack[] = $attributes;
}
...
/**
* Merge the given group attributes with the last added group.
*
* @param array $new
* @return array
*/
protected function mergeWithLastGroup($new)
{
return $this->mergeGroup($new, end($this->groupStack));
}
...
/**
* Merge the given group attributes.
*
* @param array $new
* @param array $old
* @return array
*/
public function mergeGroup($new, $old)
{
$new['namespace'] = static::formatUsesPrefix($new, $old);
$new['prefix'] = static::formatGroupPrefix($new, $old);
if (isset($new['domain'])) {
unset($old['domain']);
}
if (isset($old['as'])) {
$new['as'] = $old['as'].(isset($new['as']) ? '.'.$new['as'] : '');
}
if (isset($old['suffix']) && ! isset($new['suffix'])) {
$new['suffix'] = $old['suffix'];
}
return array_merge_recursive(Arr::except($old, ['namespace', 'prefix', 'as', 'suffix']), $new);
}
注:
$this->groupStack[]
主要陣列 keys 包含:namespace
、prefix
、domain
、as
、suffix
,這對下文的分析很有作用。
然後執行 call_user_func($callback, $this)
,即回撥函式:
function ($router) {
require __DIR__.'/../routes/web.php';
}
將 web.php
載入到這個函式裡,進一步得到函式:
function ($router) {
$router->get('/', function () use ($router) {
return "hello yemeishu ".$router->app->version();
});
$router->post('data', 'TempController@index');
}
我們的主角進場了,我們看看這些 get
、post
函式,基本都是一樣的:
public function head($uri, $action)
{
$this->addRoute('HEAD', $uri, $action);
return $this;
}
public function get($uri, $action)
{
$this->addRoute('GET', $uri, $action);
return $this;
}
public function post($uri, $action)
{
$this->addRoute('POST', $uri, $action);
return $this;
}
public function put($uri, $action)
{
$this->addRoute('PUT', $uri, $action);
return $this;
}
public function patch($uri, $action)
{
$this->addRoute('PATCH', $uri, $action);
return $this;
}
public function delete($uri, $action)
{
$this->addRoute('DELETE', $uri, $action);
return $this;
}
public function options($uri, $action)
{
$this->addRoute('OPTIONS', $uri, $action);
return $this;
}
注:這裡可以看出 Router 主要是處理這 7個
method
:head
、get
、post
、put
、patch
、delete
、options
執行的都是
$this->addRoute()
函式:
/**
* Add a route to the collection.
*
* @param array|string $method
* @param string $uri
* @param mixed $action
* @return void
*/
public function addRoute($method, $uri, $action)
{
$action = $this->parseAction($action);
$attributes = null;
if ($this->hasGroupStack()) {
$attributes = $this->mergeWithLastGroup([]);
}
if (isset($attributes) && is_array($attributes)) {
if (isset($attributes['prefix'])) {
$uri = trim($attributes['prefix'], '/').'/'.trim($uri, '/');
}
if (isset($attributes['suffix'])) {
$uri = trim($uri, '/').rtrim($attributes['suffix'], '/');
}
$action = $this->mergeGroupAttributes($action, $attributes);
}
$uri = '/'.trim($uri, '/');
if (isset($action['as'])) {
$this->namedRoutes[$action['as']] = $uri;
}
if (is_array($method)) {
foreach ($method as $verb) {
$this->routes[$verb.$uri] = ['method' => $verb, 'uri' => $uri, 'action' => $action];
}
} else {
$this->routes[$method.$uri] = ['method' => $method, 'uri' => $uri, 'action' => $action];
}
}
我們一步步來解析:
$action = $this->parseAction($action);
...
/**
* Parse the action into an array format.
*
* @param mixed $action
* @return array
*/
protected function parseAction($action)
{
if (is_string($action)) {
return ['uses' => $action];
} elseif (! is_array($action)) {
return [$action];
}
if (isset($action['middleware']) && is_string($action['middleware'])) {
$action['middleware'] = explode('|', $action['middleware']);
}
return $action;
}
將 $action
轉為陣列,如果傳入的引數包含中介軟體,順便也轉為陣列結構。
此方法可以看出,
$action
不僅可以是 string 型別,也可以是陣列型別,可以傳入 key 為:uses
和middleware
如上面例子結果變為:
// 1️⃣
[function () use ($router) {
return "hello yemeishu ".$router->app->version();
}]
// 2️⃣
['uses' => 'TempController@index']
繼續往下看:
if (isset($attributes) && is_array($attributes)) {
if (isset($attributes['prefix'])) {
$uri = trim($attributes['prefix'], '/').'/'.trim($uri, '/');
}
if (isset($attributes['suffix'])) {
$uri = trim($uri, '/').rtrim($attributes['suffix'], '/');
}
$action = $this->mergeGroupAttributes($action, $attributes);
}
$uri = '/'.trim($uri, '/');
這個比較好理解了,只是將「字首」和「字尾」拼接到 $uri
上。
// 1️⃣
$uri = '/';
// 2️⃣
$uri = '/data';
同時,將 $attributes
合併到 $action
。
往下走:
if (isset($action['as'])) {
$this->namedRoutes[$action['as']] = $uri;
}
如果 $action
陣列還傳入 key:as
,則將該 $uri
儲存到命名陣列中,利用別名與 $uri
關聯。
最後處理 $method
了:
if (is_array($method)) {
foreach ($method as $verb) {
$this->routes[$verb.$uri] = ['method' => $verb, 'uri' => $uri, 'action' => $action];
}
} else {
$this->routes[$method.$uri] = ['method' => $method, 'uri' => $uri, 'action' => $action];
}
注:這也可以看出
$method
可以傳入陣列,並且將路由三要素「method
、uri
、action
」存於陣列$routes
中,並用$method.$uri
當 key。
到此,我們基本解讀了 Router
這個類的 416行所有程式碼和功能了。
我們把所有定義的路由資訊都存入 Router
物件中,供 Request
-> Response
使用。
dispatch request
系統的執行,主要就是為了響應各種各樣的 Request
,得到 Response
反饋給請求者。
// Lumen 的入口方法
$app->run();
...
// 直接進入程式碼:Laravel\Lumen\Concerns\RoutesRequests
/**
* Run the application and send the response.
*
* @param SymfonyRequest|null $request
* @return void
*/
public function run($request = null)
{
$response = $this->dispatch($request);
if ($response instanceof SymfonyResponse) {
$response->send();
} else {
echo (string) $response;
}
if (count($this->middleware) > 0) {
$this->callTerminableMiddleware($response);
}
}
// $dispatch 執行函式:
/**
* Dispatch the incoming request.
*
* @param SymfonyRequest|null $request
* @return Response
*/
public function dispatch($request = null)
{
list($method, $pathInfo) = $this->parseIncomingRequest($request);
try {
return $this->sendThroughPipeline($this->middleware, function () use ($method, $pathInfo) {
if (isset($this->router->getRoutes()[$method.$pathInfo])) {
return $this->handleFoundRoute([true, $this->router->getRoutes()[$method.$pathInfo]['action'], []]);
}
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));
}
}
庖丁解牛,我們首要看的是如何利用 parseIncomingRequest()
返回 $method, $pathInfo
的?
list($method, $pathInfo) = $this->parseIncomingRequest($request);
...
/**
* Parse the incoming request and return the method and path info.
*
* @param \Symfony\Component\HttpFoundation\Request|null $request
* @return array
*/
protected function parseIncomingRequest($request)
{
if (! $request) {
$request = Request::capture();
}
$this->instance(Request::class, $this->prepareRequest($request));
return [$request->getMethod(), '/'.trim($request->getPathInfo(), '/')];
}
這裡主要使用 $request->getMethod()
和 $request->getPathInfo()
,這放在對 Request
的分析時再做研究。
我們接著往下看:
try {
// 第1️⃣步,這是最後執行的,暫且最後分析
return $this->sendThroughPipeline($this->middleware, function () use ($method, $pathInfo) {
if (isset($this->router->getRoutes()[$method.$pathInfo])) {
// 第2️⃣步
return $this->handleFoundRoute([true, $this->router->getRoutes()[$method.$pathInfo]['action'], []]);
}
// 第3️⃣步的執行會呼叫到「第2️⃣步」方法,所以我們先研究第3️⃣步
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));
}
「第3️⃣步」的 $this->createDispatcher()->dispatch($method, $pathInfo)
主要是返回陣列結構如下:
<?php
namespace FastRoute;
interface Dispatcher
{
const NOT_FOUND = 0;
const FOUND = 1;
const METHOD_NOT_ALLOWED = 2;
/**
* Dispatches against the provided HTTP method verb and URI.
*
* Returns array with one of the following formats:
*
* [self::NOT_FOUND]
* [self::METHOD_NOT_ALLOWED, ['GET', 'OTHER_ALLOWED_METHODS']]
* [self::FOUND, $handler, ['varName' => 'value', ...]]
*
* @param string $httpMethod
* @param string $uri
*
* @return array
*/
public function dispatch($httpMethod, $uri);
}
我們接著看是如何實現 Dispatcher
的?
/**
* Create a FastRoute dispatcher instance for the application.
*
* @return Dispatcher
*/
protected function createDispatcher()
{
return $this->dispatcher ?: \FastRoute\simpleDispatcher(function ($r) {
foreach ($this->router->getRoutes() as $route) {
$r->addRoute($route['method'], $route['uri'], $route['action']);
}
});
}
這裡的 \FastRoute\simpleDispatcher()
是一個全域性函式:
/**
* @param callable $routeDefinitionCallback
* @param array $options
*
* @return Dispatcher
*/
function simpleDispatcher(callable $routeDefinitionCallback, array $options = [])
{
$options += [
'routeParser' => 'FastRoute\\RouteParser\\Std',
'dataGenerator' => 'FastRoute\\DataGenerator\\GroupCountBased',
'dispatcher' => 'FastRoute\\Dispatcher\\GroupCountBased',
'routeCollector' => 'FastRoute\\RouteCollector',
];
/** @var RouteCollector $routeCollector */
$routeCollector = new $options['routeCollector'](
new $options['routeParser'], new $options['dataGenerator']
);
$routeDefinitionCallback($routeCollector);
return new $options['dispatcher']($routeCollector->getData());
}
這個方法主要利用 new FastRoute\\RouteParser\\Std()
和 new FastRoute\\DataGenerator\\GroupCountBased()
來建立 routeCollector
物件,用於儲存所有 route
:
function ($routeCollector) {
foreach ($this->router->getRoutes() as $route) {
$routeCollector->addRoute($route['method'], $route['uri'], $route['action']);
}
}
我們繼續看 addRoute()
方法:
/**
* Adds a route to the collection.
*
* The syntax used in the $route string depends on the used route parser.
*
* @param string|string[] $httpMethod
* @param string $route
* @param mixed $handler
*/
public function addRoute($httpMethod, $route, $handler)
{
$route = $this->currentGroupPrefix . $route;
$routeDatas = $this->routeParser->parse($route);
foreach ((array) $httpMethod as $method) {
foreach ($routeDatas as $routeData) {
$this->dataGenerator->addRoute($method, $routeData, $handler);
}
}
}
這裡有兩個方法我們可以往下研究:$this->routeParser->parse($route)
解析 $route
這個暫且不表,和 $this->dataGenerator->addRoute($method, $routeData, $handler)
收集路由資訊:
public function addRoute($httpMethod, $routeData, $handler)
{
if ($this->isStaticRoute($routeData)) {
$this->addStaticRoute($httpMethod, $routeData, $handler);
} else {
$this->addVariableRoute($httpMethod, $routeData, $handler);
}
}
這裡主要分成兩種情況,一種是單一路由資料,儲存在陣列 $staticRoutes
中,另一種是正規表示式路由資料,存於 $methodToRegexToRoutesMap
中。我們此時更關心以後怎麼使用這兩個陣列資料。
最後就是建立分發器 FastRoute\\Dispatcher\\GroupCountBased
:
// 其中 `$routeCollector->getData()` 後續繼續研究
return new $options['dispatcher']($routeCollector->getData());
...
class GroupCountBased extends RegexBasedAbstract
{
public function __construct($data)
{
list($this->staticRouteMap, $this->variableRouteData) = $data;
}
protected function dispatchVariableRoute($routeData, $uri)
{
foreach ($routeData as $data) {
if (!preg_match($data['regex'], $uri, $matches)) {
continue;
}
list($handler, $varNames) = $data['routeMap'][count($matches)];
$vars = [];
$i = 0;
foreach ($varNames as $varName) {
$vars[$varName] = $matches[++$i];
}
return [self::FOUND, $handler, $vars];
}
return [self::NOT_FOUND];
}
}
建立了 dispatcher
分配器之後,我們就可以考慮怎麼使用了。
$this->createDispatcher()->dispatch($method, $pathInfo)
分派方法,無非從上面的兩個陣列中去尋找對應 method 和 uri,以獲得 handler
。
<?php
namespace FastRoute\Dispatcher;
use FastRoute\Dispatcher;
abstract class RegexBasedAbstract implements Dispatcher
{
/** @var mixed[][] */
protected $staticRouteMap = [];
/** @var mixed[] */
protected $variableRouteData = [];
/**
* @return mixed[]
*/
abstract protected function dispatchVariableRoute($routeData, $uri);
public function dispatch($httpMethod, $uri)
{
if (isset($this->staticRouteMap[$httpMethod][$uri])) {
$handler = $this->staticRouteMap[$httpMethod][$uri];
return [self::FOUND, $handler, []];
}
$varRouteData = $this->variableRouteData;
if (isset($varRouteData[$httpMethod])) {
$result = $this->dispatchVariableRoute($varRouteData[$httpMethod], $uri);
if ($result[0] === self::FOUND) {
return $result;
}
}
// For HEAD requests, attempt fallback to GET
if ($httpMethod === 'HEAD') {
if (isset($this->staticRouteMap['GET'][$uri])) {
$handler = $this->staticRouteMap['GET'][$uri];
return [self::FOUND, $handler, []];
}
if (isset($varRouteData['GET'])) {
$result = $this->dispatchVariableRoute($varRouteData['GET'], $uri);
if ($result[0] === self::FOUND) {
return $result;
}
}
}
// If nothing else matches, try fallback routes
if (isset($this->staticRouteMap['*'][$uri])) {
$handler = $this->staticRouteMap['*'][$uri];
return [self::FOUND, $handler, []];
}
if (isset($varRouteData['*'])) {
$result = $this->dispatchVariableRoute($varRouteData['*'], $uri);
if ($result[0] === self::FOUND) {
return $result;
}
}
// Find allowed methods for this URI by matching against all other HTTP methods as well
$allowedMethods = [];
foreach ($this->staticRouteMap as $method => $uriMap) {
if ($method !== $httpMethod && isset($uriMap[$uri])) {
$allowedMethods[] = $method;
}
}
foreach ($varRouteData as $method => $routeData) {
if ($method === $httpMethod) {
continue;
}
$result = $this->dispatchVariableRoute($routeData, $uri);
if ($result[0] === self::FOUND) {
$allowedMethods[] = $method;
}
}
// If there are no allowed methods the route simply does not exist
if ($allowedMethods) {
return [self::METHOD_NOT_ALLOWED, $allowedMethods];
}
return [self::NOT_FOUND];
}
}
...
protected function dispatchVariableRoute($routeData, $uri)
{
foreach ($routeData as $data) {
if (!preg_match($data['regex'], $uri, $matches)) {
continue;
}
list($handler, $varNames) = $data['routeMap'][count($matches)];
$vars = [];
$i = 0;
foreach ($varNames as $varName) {
$vars[$varName] = $matches[++$i];
}
return [self::FOUND, $handler, $vars];
}
return [self::NOT_FOUND];
}
以上解析過程比較簡單,就不用解釋了。
得到 handler
後,我們就可以處理 $request
,得到 $response
結果。
/**
* Handle the response from the FastRoute dispatcher.
*
* @param array $routeInfo
* @return mixed
*/
protected function handleDispatcherResponse($routeInfo)
{
switch ($routeInfo[0]) {
case Dispatcher::NOT_FOUND:
throw new NotFoundHttpException;
case Dispatcher::METHOD_NOT_ALLOWED:
throw new MethodNotAllowedHttpException($routeInfo[1]);
case Dispatcher::FOUND:
return $this->handleFoundRoute($routeInfo);
}
}
我們不分析前兩個分支,我們主要考慮 Dispatcher::FOUND
這種情況,即:$this->handleFoundRoute($routeInfo)
/**
* Handle a route found by the dispatcher.
*
* @param array $routeInfo
* @return mixed
*/
protected function handleFoundRoute($routeInfo)
{
$this->currentRoute = $routeInfo;
$this['request']->setRouteResolver(function () {
return $this->currentRoute;
});
$action = $routeInfo[1];
// Pipe through route middleware...
if (isset($action['middleware'])) {
$middleware = $this->gatherMiddlewareClassNames($action['middleware']);
return $this->prepareResponse($this->sendThroughPipeline($middleware, function () {
return $this->callActionOnArrayBasedRoute($this['request']->route());
}));
}
return $this->prepareResponse(
$this->callActionOnArrayBasedRoute($routeInfo)
);
}
我們暫時也不去考慮是否存在「中介軟體」的問題,那麼把目光就鎖定在最後一條語句上了。
/**
* Call the Closure on the array based route.
*
* @param array $routeInfo
* @return mixed
*/
protected function callActionOnArrayBasedRoute($routeInfo)
{
$action = $routeInfo[1];
if (isset($action['uses'])) {
return $this->prepareResponse($this->callControllerAction($routeInfo));
}
foreach ($action as $value) {
if ($value instanceof Closure) {
$closure = $value->bindTo(new RoutingClosure);
break;
}
}
try {
return $this->prepareResponse($this->call($closure, $routeInfo[2]));
} catch (HttpResponseException $e) {
return $e->getResponse();
}
}
到此,我們終於開始進入「Controller」級別的分析了。
先看第一種情況:$this->callControllerAction($routeInfo)
如上面的第2️⃣種情況:
// 2️⃣
['uses' => 'TempController@index']
/**
* Call a controller based route.
*
* @param array $routeInfo
* @return mixed
*/
protected function callControllerAction($routeInfo)
{
$uses = $routeInfo[1]['uses'];
if (is_string($uses) && ! Str::contains($uses, '@')) {
$uses .= '@__invoke';
}
list($controller, $method) = explode('@', $uses);
if (! method_exists($instance = $this->make($controller), $method)) {
throw new NotFoundHttpException;
}
if ($instance instanceof LumenController) {
return $this->callLumenController($instance, $method, $routeInfo);
} else {
return $this->callControllerCallable(
[$instance, $method], $routeInfo[2]
);
}
}
這個對於我們天天寫 Lumen or Laravel 程式碼的我們來說,挺好理解的,通過利用「@」分解 controller
和 method
;再利用 $this->make($controller)
得到 Controller
物件,如果是 LumenController
型別,則需要去判斷是否有中介軟體一個環節。最後都是呼叫 $this->callControllerCallable([$instance, $method], $routeInfo[2])
:
protected function callControllerCallable(callable $callable, array $parameters = [])
{
try {
return $this->prepareResponse(
$this->call($callable, $parameters)
);
} catch (HttpResponseException $e) {
return $e->getResponse();
}
}
...
/**
* Call the given Closure / class@method and inject its dependencies.
*
* @param callable|string $callback
* @param array $parameters
* @param string|null $defaultMethod
* @return mixed
*/
public function call($callback, array $parameters = [], $defaultMethod = null)
{
return BoundMethod::call($this, $callback, $parameters, $defaultMethod);
}
來反射解析類和方法,呼叫方法,返回結果。具體可以詳細研究 illuminate\\container\\BoundMethod
類。
封裝成 Response
結果:
return $this->prepareResponse(
$this->call($callable, $parameters)
);
/**
* Prepare the response for sending.
*
* @param mixed $response
* @return Response
*/
public function prepareResponse($response)
{
if ($response instanceof Responsable) {
$response = $response->toResponse(Request::capture());
}
if ($response instanceof PsrResponseInterface) {
$response = (new HttpFoundationFactory)->createResponse($response);
} elseif (! $response instanceof SymfonyResponse) {
$response = new Response($response);
} elseif ($response instanceof BinaryFileResponse) {
$response = $response->prepare(Request::capture());
}
return $response;
}
起始亦是終,最後把 Response
輸出,回到最開始的 run
方法
public function run($request = null)
{
$response = $this->dispatch($request);
if ($response instanceof SymfonyResponse) {
$response->send();
} else {
echo (string) $response;
}
if (count($this->middleware) > 0) {
$this->callTerminableMiddleware($response);
}
}
總結
到此,我們終於分析了走了一遍較為完整的從 Request
到最後的 Response
的流程。此文結合 Lumen 文件 https://lumen.laravel.com/docs/5.6/routing 來看,效果會更好的。
這過程我們也發現了幾個彩蛋:
彩蛋1️⃣ $router->addRoute($method, $uri, $action)
的 method
可以傳入陣列,如 ['GET', 'POST']
彩蛋2️⃣
if (! method_exists($instance = $this->make($controller), $method)) {
throw new NotFoundHttpException;
}
if ($instance instanceof LumenController) {
return $this->callLumenController($instance, $method, $routeInfo);
} else {
return $this->callControllerCallable(
[$instance, $method], $routeInfo[2]
);
}
可以看出,處理我們 route
的類可以不用繼承「Controller
」,只要依賴注入,能利用 $this->make
解析到的「類」均可。
最後,我們還有很多需要深入研究的內容,如:中介軟體 middleware
、Pipeline
原理、Request
解析、帶有正規表示式的 $uri
是怎麼解析的,等等。