生命周期 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 协议》,转载必须注明作者和本文链接
日拱一卒
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!
未填写
文章
93
粉丝
85
喜欢
153
收藏
121
排名:71
访问:11.4 万
私信
所有博文
社区赞助商