生命周期 9--如何执行路由对应的方法并返回待处理的结果
前言
上一节已经分析了《request对象是如何通过路由中间件的》,本文分析一下「如何执行路由对应的方法并返回待处理的结果」
代码位置
vendor/laravel/framework/src/Illuminate/Routing/Route.php:163
,在route对象的run()方法中
具体分析
route对象的run()方法:
/**
* Run the route action and return the response.
*
* @return mixed
*/
public function run()
{
//获取服务容器
$this->container = $this->container ?: new Container;
try {
//如果路由的action是控制器
if ($this->isControllerAction()) {
//就执行该控制器的方法并返回结果
return $this->runController();
}
//如不是控制器,就执行闭包的方法并返回结果
return $this->runCallable();
} catch (HttpResponseException $e) {
return $e->getResponse();
}
}
执行控制器方法
$this->runController()
/**
* Run the route action and return the response.
*
* @return mixed
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
*/
protected function runController()
{
//使用controllerDispatcher来发送request到控制器方法
return $this->controllerDispatcher()->dispatch(
$this, $this->getController(), $this->getControllerMethod()
);
}
这里的getController()和getControllerMethod()前面都已经执行过了,这里直接获取即可。
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)
{
//解析控制器类方法中type-hinted的参数依赖
$parameters = $this->resolveClassMethodDependencies(
$route->parametersWithoutNulls(), $controller, $method
);
//如果控制器有callAction方法,则callAction方法去执行
if (method_exists($controller, 'callAction')) {
return $controller->callAction($method, $parameters);
}
//否则,就直接执行控制器的方法
return $controller->{$method}(...array_values($parameters));
}
解析控制器类方法中type-hinted的参数依赖。
* Resolve the object method's type-hinted dependencies.
*
* @param array $parameters
* @param object $instance
* @param string $method
* @return array
*/
protected function resolveClassMethodDependencies(array $parameters, $instance, $method)
{
//如果控制器中有此方法,则直接返回$parameters
if (! method_exists($instance, $method)) {
return $parameters;
}
//如果没有此方法,解析出??
return $this->resolveMethodDependencies(
$parameters, new ReflectionMethod($instance, $method)
);
}
为了说明这个问题,要运行的控制器方法中必须有依赖注入的参数),我这里就选择了show方法。
public function show(User $user)
{
return view('users.show', compact("user"));
}
当前我运行的请求的路由uri是users/{user}
,对应的controller action为App\Http\Controllers\UsersController@show
。这些都是创建资源路由时自动生成的。
再回到\Illuminate\Routing\ControllerDispatcher::dispatch
中:$route->parametersWithoutNulls()
:
/**
* Get the key / value list of parameters without null values.
*
* @return array
*/
public function parametersWithoutNulls()
{
//将route对象绑定的参数取出,去掉null
return array_filter($this->parameters(), function ($p) {
return ! is_null($p);
});
}
$this->parameters()
:
/**
* Get the key / value list of parameters for the route.
*
* @return array
*
* @throws \LogicException
*/
public function parameters()
{
//取出绑定的参数数组
if (isset($this->parameters)) {
return $this->parameters;
}
throw new LogicException('Route is not bound.');
}
当前上下文,我这个绑定的参数为:
['user'=>{App\Models\User}]
那我这个参数是如何绑定的呢?
经过查找,可以发现我们在匹配路由的时候就运行了这个bind方法:
public function match(Request $request)
{
$routes = $this->get($request->getMethod());
$route = $this->matchAgainstRoutes($routes, $request);
//找到合适的路由后,进行参数绑定!
if (! is_null($route)) {
return $route->bind($request);
}
...
}
而bind方法中就进行了参数绑定:
public function bind(Request $request)
{
//将当前路由(route对象)编译为一个Symfony CompiledRoute instance
//并赋值给route对象的compiled属性,后面会多次用到这个属性。
$this->compileRoute();
//创建一个RouteParameterBinder,并从URI的path和host中获得匹配的参数
$this->parameters = (new RouteParameterBinder($this))
->parameters($request);
return $this;
}
RouteParameterBinder的parameters方法:
/**
* Get the parameters for the route.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function parameters($request)
{
//从URI的path部分获取参数
$parameters = $this->bindPathParameters($request);
//如果route对象的compiled属性保存的Symfony CompiledRoute instance
//有hostRegex属性,那么就会去URI的host部分去获取参数,然后合并到前面的参数中
if (! is_null($this->route->compiled->getHostRegex())) {
$parameters = $this->bindHostParameters(
$request, $parameters
);
}
return $this->replaceDefaults($parameters);
}
从URI的path部分获取参数:
/**
* Get the parameter matches for the path portion of the URI.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
protected function bindPathParameters($request)
{
//当前path为"/users/7"
$path = '/'.ltrim($request->decodedPath(), '/');
//这个正则匹配成功
preg_match($this->route->compiled->getRegex(), $path, $matches);
return $this->matchToKeys(array_slice($matches, 1));
}
经过debug:$this->route->compiled->getRegex()
为"#^/users/(?P[^/]++)$#sDu"
(?P<user>[^/]++)
这种写法是命名子组,可以供后面引用。语法有这三种:(?'name'ABC) (?P<name>ABC) (?<name>ABC)
因此,preg_match($this->route->compiled->getRegex(), $path, $matches)
得到一个$matches数组:
array (
0 => '/users/7',
'user' => '7',
1 => '7',
)
array_slice($matches, 1)
得到:
array (
'user' => '7',
0 => '7',
)
$this->matchToKeys(array_slice($matches, 1))
/**
* Combine a set of parameter matches with the route's keys.
*
* @param array $matches
* @return array
*/
protected function matchToKeys(array $matches)
{
//如果当前路由的参数名的数组为空,就返回[]
if (empty($parameterNames = $this->route->parameterNames())) {
return [];
}
//否则,就反转后,使用键名与传入的$matches数组比较计算出数组的交集
$parameters = array_intersect_key($matches, array_flip($parameterNames));
return array_filter($parameters, function ($value) {
return is_string($value) && strlen($value) > 0;
});
}
由于当前路由已经有了参数名称(具体怎么来的下面说),$parameters
为['user'=>'7']
,然后使用array_filter过滤掉空值,最后返回给\Illuminate\Routing\RouteParameterBinder::parameters
方法中的$parameters
为['user'=>'7']
if (! is_null($this->route->compiled->getHostRegex())) {
由于当前CompiledRoute对象的hostRegex为null,因此就直接到了
$this->replaceDefaults($parameters)
将null参数替换为它们的默认值,由于前面已经过滤掉空值,这里实际上没有运行进去了。
* Replace null parameters with their defaults.
*
* @param array $parameters
* @return array
*/
protected function replaceDefaults(array $parameters)
{
//遍历传入参数数组,对每个参数值判断
//如果有值,则保持原样;否则,取出路由对象定义的默认值。
foreach ($parameters as $key => $value) {
$parameters[$key] = $value ?? Arr::get($this->route->defaults, $key);
}
//遍历路由对象的默认值,并强行添加到返回的参数数组中
foreach ($this->route->defaults as $key => $value) {
if (! isset($parameters[$key])) {
$parameters[$key] = $value;
}
}
return $parameters;
}
好了,上面这么多只是为了分析路由绑定的参数是怎么来的。但我们现在绑定的参数是['user'=>'7']
, 跟['user'=>{App\Models\User}]
不一样啊?
回忆一下,《request对象是如何通过路由中间件的》 中我们穿过了 Illuminate\\Routing\\Middleware\\SubstituteBindings
中间件,其handle方法为:
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
//替换路由显式绑定
$this->router->substituteBindings($route = $request->route());
//替换路由隐式绑定
$this->router->substituteImplicitBindings($route);
return $next($request);
}
替换路由显式绑定(我们前面并没有明确申明,因此这里大概了解一下即可)
/**
* Substitute the route bindings onto the route.
*
* @param \Illuminate\Routing\Route $route
* @return \Illuminate\Routing\Route
*/
public function substituteBindings($route)
{
//遍历路由参数
foreach ($route->parameters() as $key => $value) {
//如果在RouteServiceProvider中已申明了显式绑定
//那么就会去调用$this->binders中定义好的回调函数进行model的参数绑定
if (isset($this->binders[$key])) {
$route->setParameter($key, $this->performBinding($key, $value, $route));
}
}
return $route;
}
替换路由隐式绑定(这个是重点,常用)
/**
* Substitute the implicit Eloquent model bindings for the route.
*
* @param \Illuminate\Routing\Route $route
* @return void
*/
public function substituteImplicitBindings($route)
{
ImplicitRouteBinding::resolveForRoute($this->container, $route);
}
/**
* Resolve the implicit route bindings for the given route.
*
* @param \Illuminate\Container\Container $container
* @param \Illuminate\Routing\Route $route
* @return void
*/
public static function resolveForRoute($container, $route)
{
//获得路由参数 ['user'=>'7']
$parameters = $route->parameters();
//获得实现了UrlRoutable接口的路由签名参数,
//(实际上是路由action对应的控制器方法的参数)
//{'name'=>'user'},因为'App\Models\User'是model类的子类,
//而model类实现了UrlRoutable接口
foreach ($route->signatureParameters(UrlRoutable::class) as $parameter) {
//如果控制器方法的参数名称不在当前的路由参数中,就跳过此次循环
if (! $parameterName = static::getParameterName($parameter->name, $parameters)) {
continue;
}
//获取到路由参数值
$parameterValue = $parameters[$parameterName];
//如果路由参数值为实现了UrlRoutable接口的实例,直接跳过本次循环
if ($parameterValue instanceof UrlRoutable) {
continue;
}
//根据反射参数获取全类名,并解析出App\Models\User实例,此实例没有属性
$instance = $container->make($parameter->getClass()->name);
//根据路由参数值,去App\Models\User实例查找出对应的具体实例,此实例有属性,id=7
if (! $model = $instance->resolveRouteBinding($parameterValue)) {
throw (new ModelNotFoundException)->setModel(get_class($instance));
}
//绑定路由参数,也就是['user'=>{App\Models\User}(id=7)]
$route->setParameter($parameterName, $model);
}
}
好了,经过前面的这么长的铺垫,终于可以返回到\Illuminate\Routing\ControllerDispatcher::dispatch
中:$route->parametersWithoutNulls()
:
/**
* Get the key / value list of parameters without null values.
*
* @return array
*/
public function parametersWithoutNulls()
{
//将route对象绑定的参数取出,去掉null
return array_filter($this->parameters(), function ($p) {
return ! is_null($p);
});
}
上面这个返回我们绑定好的路由参数:['user'=>{App\Models\User}(id=7)]
返回到\Illuminate\Routing\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));
}
\Illuminate\Routing\Controller::callAction
:
/**
* Execute an action on the controller.
*
* @param string $method
* @param array $parameters
* @return \Symfony\Component\HttpFoundation\Response
*/
public function callAction($method, $parameters)
{
return call_user_func_array([$this, $method], $parameters);
}
执行上面的语句,也就是运行UsersController的 show方法,传入的$user就是我们前面绑定的['user'=>{App\Models\User}(id=7)]
public function show(User $user)
{
//返回获得的视图内容
return view('users.show', compact("user"));
}
上面调用了\Illuminate\View\Factory::make
方法:
/**
* Get the evaluated view contents for the given view.
*
* @param string $view
* @param \Illuminate\Contracts\Support\Arrayable|array $data
* @param array $mergeData
* @return \Illuminate\Contracts\View\View
*/
public function make($view, $data = [], $mergeData = [])
{
//找到指定视图的绝对路径
$path = $this->finder->find(
$view = $this->normalizeName($view)
);
// Next, we will create the view instance and call the view creator for the view
// which can set any data, etc. Then we will return the view instance back to
// the caller for rendering or performing other view manipulations on this.
$data = array_merge($mergeData, $this->parseData($data));
//创建一个视图实例,然后调用callCreator方法处理,最后返回出去准备渲染。
return tap($this->viewInstance($view, $path, $data), function ($view) {
$this->callCreator($view);
});
}
\Illuminate\View\Factory::viewInstance
:
/**
* Create a new view instance from the given arguments.
*
* @param string $view
* @param string $path
* @param \Illuminate\Contracts\Support\Arrayable|array $data
* @return \Illuminate\Contracts\View\View
*/
protected function viewInstance($view, $path, $data)
{
return new View($this, $this->getEngineFromPath($path), $view, $path, $data);
}
callCreator方法是触发一个creating: users.show
事件,自动完成一些事先定义好的操作。由于我们并没有定义listener,因此什么都没有做。
/**
* Call the creator for a given view.
*
* @param \Illuminate\Contracts\View\View $view
* @return void
*/
public function callCreator(ViewContract $view)
{
//触发`creating: users.show`事件
$this->events->dispatch('creating: '.$view->name(), [$view]);
}
因此,$route->run()
实际上仅仅是返回了一个视图对象而已,接下来就是如何去渲染准备response的工作了。
return $this->prepareResponse(
$request, $route->run()
);
执行闭包方法
由于一般都不会直接写在闭包中,这里就不分析了,有了的话,应该也是类似的。
遗留的两个问题
$this->route->compiled
这个CompiledRoute对象是怎么来的?
这个是在request匹配路由的时候就创建了。可以看到后面在路由参数绑定时用到的regex也是在这个时候就已经设置好了。
public function __construct(string $staticPrefix, string $regex, array $tokens, array $pathVariables, string $hostRegex = null, array $hostTokens = array(), array $hostVariables = array(), array $variables = array())
{
$this->staticPrefix = $staticPrefix;
$this->regex = $regex;
$this->tokens = $tokens;
$this->pathVariables = $pathVariables;
$this->hostRegex = $hostRegex;
$this->hostTokens = $hostTokens;
$this->hostVariables = $hostVariables;
$this->variables = $variables;
}
传入的这些参数是在\Symfony\Component\Routing\RouteCompiler::compile
方法中设置的,这里就不说了。
public static function compile(Route $route)
{
$hostVariables = array();
$variables = array();
$hostRegex = null;
$hostTokens = array();
if ('' !== $host = $route->getHost()) {
$result = self::compilePattern($route, $host, true);
$hostVariables = $result['variables'];
$variables = $hostVariables;
$hostTokens = $result['tokens'];
$hostRegex = $result['regex'];
}
$path = $route->getPath();
$result = self::compilePattern($route, $path, false);
$staticPrefix = $result['staticPrefix'];
$pathVariables = $result['variables'];
foreach ($pathVariables as $pathParam) {
if ('_fragment' === $pathParam) {
throw new \InvalidArgumentException(sprintf('Route pattern "%s" cannot contain "_fragment" as a path parameter.', $route->getPath()));
}
}
$variables = array_merge($variables, $pathVariables);
$tokens = $result['tokens'];
$regex = $result['regex'];
return new CompiledRoute(
$staticPrefix,
$regex,
$tokens,
$pathVariables,
$hostRegex,
$hostTokens,
$hostVariables,
array_unique($variables)
);
}
$this->route->parameterNames()
里面返回的路由参数名称数组是怎么来的?
这个也是在路由匹配request的时候就设置了。
/**
* Get all of the parameter names for the route.
*
* @return array
*/
public function parameterNames()
{
if (isset($this->parameterNames)) {
return $this->parameterNames;
}
return $this->parameterNames = $this->compileParameterNames();
}
/**
* Get the parameter names for the route.
*
* @return array
*/
protected function compileParameterNames()
{
preg_match_all('/\{(.*?)\}/', $this->getDomain().$this->uri, $matches);
return array_map(function ($m) {
return trim($m, '?');
}, $matches[1]);
}
小结
- 通过本节分析,我们可以知道在执行路由对应的方法时,是如何进入对应的控制器方法中的。
- 而且本节还详细分析了路由隐式绑定背后的完成原理,分析了显式绑定和隐式绑定的代码,深刻理解了二者的区别。
- 由于匹配路由时会生成很多不同的信息,比如
/login
和/users/7
两个就大不相同,因此,本节还补充分析了路由绑定参数和控制器方法参数注入的代码。 - 不管如何分析,都是在PHP基础上进行的,而且涉及到的知识都不复杂。
本作品采用《CC 协议》,转载必须注明作者和本文链接
推荐文章: