像所有web框架一樣,Laravel的核心也是一個讀取HTTP請求,返回應答內容的類:Kernel,該類繼承自Symfony的Kernel類;
在Kernel生成Response的過程的各個階段都會有各種事件Event觸發,用來控制整個請求的生命週期,類似於鉤子機制。
Kernel的入口handle方法
/**
* Handle an incoming HTTP request.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function handle($request)
{
try {
$request->enableHttpMethodParameterOverride();
$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;
}
主要邏輯部分 sendRequestThroughRouter
/**
* Send the given request through the middleware / router.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
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());
}
//...
/**
* Get the route dispatcher callback.
*
* @return \Closure
*/
protected function dispatchToRouter()
{
return function ($request) {
$this->app->instance('request', $request);
return $this->router->dispatch($request);
};
}
接下來到Router類
/**
* Dispatch the request to the application.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse
*/
public function dispatch(Request $request)
{
$this->currentRequest = $request;
return $this->dispatchToRoute($request);
}
/**
* Dispatch the request to a route and return the response.
*
* @param \Illuminate\Http\Request $request
* @return mixed
*/
public function dispatchToRoute(Request $request)
{
return $this->runRoute($request, $this->findRoute($request));
}
/**
* Find the route matching a given request.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Routing\Route
*/
protected function findRoute($request)
{
$this->current = $route = $this->routes->match($request);
$this->container->instance(Route::class, $route);
return $route;
}
/**
* Return the response for the given route.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Routing\Route $route
* @return mixed
*/
protected function runRoute(Request $request, Route $route)
{
$request->setRouteResolver(function () use ($route) {
return $route;
});
$this->events->dispatch(new Events\RouteMatched($route, $request));
return $this->prepareResponse($request,
$this->runRouteWithinStack($route, $request)
);
}
/**
* Run the given route within a Stack "onion" instance.
*
* @param \Illuminate\Routing\Route $route
* @param \Illuminate\Http\Request $request
* @return mixed
*/
protected function runRouteWithinStack(Route $route, Request $request)
{
$shouldSkipMiddleware = $this->container->bound('middleware.disable') &&
$this->container->make('middleware.disable') === true;
$middleware = $shouldSkipMiddleware ? [] : $this->gatherRouteMiddleware($route);
return (new Pipeline($this->container))
->send($request)
->through($middleware)
->then(function ($request) use ($route) {
return $this->prepareResponse(
$request, $route->run()
);
});
}
到這裡,感覺好像又跟最初的Kernel::sendRequestThroughRouter 方法裡面那個管道的用法非常類似了,只有最後一步then的引數不同。那麼為什麼要走兩次中介軟體呢?前者Kernel裡走的是通用中介軟體,而後者Router裡面走的是特定路由Route的中介軟體,即分別為下面的middleware和middlewareGroups對應的中介軟體class。
protected $middleware = [
\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
\App\Http\Middleware\TrustProxies::class,
];
/**
* The application's route middleware groups.
*
* @var array
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
// \Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [
'throttle:60,1',
'bindings',
],
];
最後 $route->run()方法,跳到路由類Route的run方法,開始真正走到執行控制器方法。
/**
* Run the route action and return the response.
*
* @return mixed
*/
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();
}
}
這裡看到,使用控制器方法跟直接使用回撥函式,分別需要執行不同的方法runController和runCallable(判斷控制器方法還是回撥函式邏輯很簡單,路由配置的uses值為string就認為是控制器方法),接下來需要解決注入控制器方法或者匿名回撥函式的引數依賴問題。
-
可以先從實現上比較簡單的匿名回撥函式的引數依賴看起。
/** * Run the route action and return the response. * * @return mixed */ protected function runCallable() { $callable = $this->action['uses']; return $callable(...array_values($this->resolveMethodDependencies( $this->parametersWithoutNulls(), new ReflectionFunction($this->action['uses']) ))); }
parametersWithoutNulls()返回Route類parameters屬性過濾掉NULL值之後的陣列,而parameters屬性陣列是在RouteCollection::match方法中透過Route::bind方法初始化的。 Router的$routes屬性即RouteCollection的一個例項,在RoutingServiceProvider中透過Router的setter初始化。
這裡的$this->parametersWithoutNulls()取自的$parameters僅僅是路由中定義的{}引數,並非query_string所有url引數,例如/user/{id} 訪問/user/1?age=22,那麼:
$parameters = ['id' => 1]
$route = $this->matchAgainstRoutes($routes, $request);
if (! is_null($route)) {
return $route->bind($request);
}
回到runCallable()的話題,resolveMethodDependencies是RouteDependencyResolverTrait的一個Trait方法
/**
* Resolve the given method's type-hinted dependencies.
*
* @param array $parameters
* @param \ReflectionFunctionAbstract $reflector
* @return array
*/
public function resolveMethodDependencies(array $parameters, ReflectionFunctionAbstract $reflector)
{
$instanceCount = 0;
$values = array_values($parameters);
foreach ($reflector->getParameters() as $key => $parameter) {
$instance = $this->transformDependency(
$parameter, $parameters
);
if (! is_null($instance)) {
$instanceCount++;
$this->spliceIntoParameters($parameters, $key, $instance);
} elseif (! isset($values[$key - $instanceCount]) &&
$parameter->isDefaultValueAvailable()) {
$this->spliceIntoParameters($parameters, $key, $parameter->getDefaultValue());
}
}
return $parameters;
}
/**
* Attempt to transform the given parameter into a class instance.
*
* @param \ReflectionParameter $parameter
* @param array $parameters
* @return mixed
*/
protected function transformDependency(ReflectionParameter $parameter, $parameters)
{
$class = $parameter->getClass();
// If the parameter has a type-hinted class, we will check to see if it is already in
// the list of parameters. If it is we will just skip it as it is probably a model
// binding and we do not want to mess with those; otherwise, we resolve it here.
if ($class && ! $this->alreadyInParameters($class->name, $parameters)) {
return $parameter->isDefaultValueAvailable()
? $parameter->getDefaultValue()
: $this->container->make($class->name);
}
}
程式碼註釋裡面解釋了為什麼type-hint的需要注入的引數,需要檢查路由引數中是否已經存在:可能存在model-binding引數;
需要注意的是,回撥函式(控制器方法)反射引數也是包含路由引數名的,且先後順序一致,所以$this->spliceIntoParameters($parameters, $key, $instance);
才能把引數插入到正確的位置。
-
Route::runController
/** * Run the route action and return the response. * * @return mixed * * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException */ protected function runController() { return $this->controllerDispatcher()->dispatch( $this, $this->getController(), $this->getControllerMethod() ); }
例項化並呼叫了ControllerDispatcher::dispatch方法
/** * Dispatch a request to a given controller and method. * * @param \Illuminate\Routing\Route $route * @param mixed $controller * @param string $method * @return mixed */ public function dispatch(Route $route, $controller, $method) { $parameters = $this->resolveClassMethodDependencies( $route->parametersWithoutNulls(), $controller, $method ); if (method_exists($controller, 'callAction')) { return $controller->callAction($method, $parameters); } return $controller->{$method}(...array_values($parameters)); }
類似於路由回撥函式呼叫,值得注意的是,控制器可以有一個callAction($method, $parameters)方法,用來自定義對控制器方法進行修改。
Laravel的Kernel是核心流程,而容器+外掛(ServiceProvider)實現了Laravel最為強大的功能,提供了它無與倫比的可擴充套件性。
本作品採用《CC 協議》,轉載必須註明作者和本文連結