控制器与方法的运行

未匹配的标注

简介

历经 7 章的漫长路途,我们终于走完了路由中间件,总结一下:

[
    // 加解密 Cookies
    'App\Http\Middleware\EncryptCookies',
    // 将队列中的 Cookies 添加到 响应 里
    'Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse',
    // 开启服务端会话
    'Illuminate\Session\Middleware\StartSession',
    // 将储存在 session 中的错误加载到 view 视图中
    'Illuminate\View\Middleware\ShareErrorsFromSession',
    // 验证 CSRF
    'App\Http\Middleware\VerifyCsrfToken',
    // 从请求参数中获取实际模型对象,直接绑定到控制器方法中
    'Illuminate\Routing\Middleware\SubstituteBindings',
    // 请求登录、注册、忘记密码界面时,如果登录了,重定向到首页
    'App\Http\Middleware\RedirectIfAuthenticated',
]

从本章开始,我们就正式进入大家比较关心的地方,就是我们定义的控制器方法,到底是怎么运行的??

路由中间件运行完了,控制器方法运行的入口位置

Illuminate\Routing\Router

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(
                            // 在这里!!,$route->run() 就是进入控制器方法运行的地方。
                            $request, $route->run()
                        );
                    });
}

run

Illuminate\Routing\Route

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();
    }
}

为什么 $this->container 已经赋值过

如下一套路由定义的例子:

web.php

Route::get('/', function () {
    return view('welcome');
});

Route::get('/home', 'HomeController@index')->name('home');

Route::get('/test', function () {
    return config('first.second.test.key');
});

Route::post('/test', 'TestController@postTest');

Route::put('/update', 'TestController@update');

Route::delete('/delete', 'TestController@delete');

路由中定义的 get、post、put、delete、any 等方法实际执行的位置如下:

Illuminate\Routing\Router

public function get($uri, $action = null)
{
    return $this->addRoute(['GET', 'HEAD'], $uri, $action);
}

public function post($uri, $action = null)
{
    return $this->addRoute('POST', $uri, $action);
}

public function put($uri, $action = null)
{
    return $this->addRoute('PUT', $uri, $action);
}

public function patch($uri, $action = null)
{
    return $this->addRoute('PATCH', $uri, $action);
}

public function delete($uri, $action = null)
{
    return $this->addRoute('DELETE', $uri, $action);
}

public function options($uri, $action = null)
{
    return $this->addRoute('OPTIONS', $uri, $action);
}

public function any($uri, $action = null)
{
    return $this->addRoute(self::$verbs, $uri, $action);
}

共同点是都执行了 addRoute 方法

Illuminate\Routing\Router

public function addRoute($methods, $uri, $action)
{
    // createRoute 是重点
    return $this->routes->add($this->createRoute($methods, $uri, $action));
}

着重看 createRoute 方法

Illuminate\Routing\Router

protected function createRoute($methods, $uri, $action)
{
    if ($this->actionReferencesController($action)) {
        $action = $this->convertToControllerAction($action);
    }

    // newRoute 方法是重点
    $route = $this->newRoute(
        $methods, $this->prefix($uri), $action
    );

    if ($this->hasGroupStack()) {
        $this->mergeGroupAttributesIntoRoute($route);
    }

    $this->addWhereClausesToRoute($route);

    return $route;
}

接下来我们来看 newRoute

Illuminate\Routing\Router

protected function newRoute($methods, $uri, $action)
{
    return (new Route($methods, $uri, $action))
                ->setRouter($this)
                // 在这里,嘿嘿
                ->setContainer($this->container);
}

看到 setContainer 方法了吗,我们就进入 Route 类中看看这个 setContainer 方法

Illuminate\Routing\Route

public function setContainer(Container $container)
{
    $this->container = $container;

    return $this;
}

看到没, $this->container = $container; ,所以说 $this->container 已经被赋值过。

路由里面 什么是控制器与方法运行;什么是闭包运行

  • 控制器与方法运行

    Route::get('/home', 'HomeController@index')->name('home');

    这类定义,就属于控制器与方法运行

  • 闭包运行

    Route::get('/', function () {
      return view('welcome');
    });   

    这类定义,就属于闭包运行

那么 控制器与方法运行和闭包运行如何区分的呢

Illuminate\Routing\Route

public function run()
{
    $this->container = $this->container ?: new Container;

    try {
        // isControllerAction 就是区分的判断
        if ($this->isControllerAction()) {
            return $this->runController();
        }

        return $this->runCallable();
    } catch (HttpResponseException $e) {
        return $e->getResponse();
    }
}

那我们来看一下 isControllerAction 方法

protected function isControllerAction()
{
    return is_string($this->action['uses']);
}

$this->action['uses'],是什么,我们看一下调试截图,就清楚了

Laravel

哦,我知道了,如果路由定义是控制器与方法,那么格式就是 控制器@方法 字符串;如果路由定义是闭包,那么格式就是闭包对象,而非字符串。故而区分出控制器与方法定义和闭包定义。

控制器与方法运行

由于教程请求的路由,是控制器与方法,故我们重点来看控制器与方法运行原理

Illuminate\Routing\Route

protected function runController()
{
    return $this->controllerDispatcher()->dispatch(
        // $this:当前 Route 对象
        // $this->getController():控制器实例化的对象(控制器@方法,实例化@前面的控制器)
        // $this->getControllerMethod():路由定义时的方法(控制器@方法,获取@后面的方法名称)
        $this, $this->getController(), $this->getControllerMethod()
    );
}

$this->controllerDispatcher() 返回控制器派遣管理对象

我们看一下控制器派遣管理对象的 dispatch 方法

Illuminate\Routing\ControllerDispatcher

public function dispatch(Route $route, $controller, $method)
{
    // 从控制器方法的参数上,返回所以依赖的服务对象
    $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));
}

方法参数依赖注入的实现

在 dispatch 方法上,我们看到这么一段代码

$parameters = $this->resolveClassMethodDependencies(
    $route->parametersWithoutNulls(), $controller, $method
);

这段代码,就是实现 方法参数依赖注入 的源码,其原理就是利用 PHP 反射类机制,获取类方法参数依赖的类,然后获取依赖类的实例,最后在调用类方法时,将这个实例以实参的方式传入类方法中,使其正确运行。

那我们看一下 resolveClassMethodDependencies 方法的源码吧

Illuminate\Routing\RouteDependencyResolverTrait

/**
* @parameters:前端请求时,传过来的参数对
* @instance:我们自己定义的控制器实例
* @method:我们自己定义的控制器方法名称
*/
protected function resolveClassMethodDependencies(array $parameters, $instance, $method)
{
    // 如果我们定义的控制器里面没有路由定义时的方法,直接返回请求的参数对
    if (! method_exists($instance, $method)) {
        return $parameters;
    }

    return $this->resolveMethodDependencies(
        // 实例化 PHP 内置的反射类 ReflectionMethod
        $parameters, new ReflectionMethod($instance, $method)
    );
}

继续,我们看一下 resolveMethodDependencies 方法

Illuminate\Routing\RouteDependencyResolverTrait

public function resolveMethodDependencies(array $parameters, ReflectionFunctionAbstract $reflector)
{
    // 方法形参计数
    $instanceCount = 0;

    // 直接从请求的参数对中,获取参数值数组
    $values = array_values($parameters);

    // for 循环方法参数的反射类数组,每个 $parameter 代表一个参数的反射对象
    foreach ($reflector->getParameters() as $key => $parameter) {
        // 重点在这里,$parameter 对象里面存储了参数依赖的类全名,通过实例化这个类全名,我们得到了依赖的对象
        $instance = $this->transformDependency(
            $parameter, $parameters
        );

        if (! is_null($instance)) {
            $instanceCount++;

            // 替换 $parameters 请求的参数
            $this->spliceIntoParameters($parameters, $key, $instance);
        } elseif (! isset($values[$key - $instanceCount]) &&
                  $parameter->isDefaultValueAvailable()) {
            $this->spliceIntoParameters($parameters, $key, $parameter->getDefaultValue());
        }
    }

    // 返回替换好的请求的参数
    return $parameters;
}

最后:如果我们自己定义的控制器方法,有默认值,那么悠闲拿取默认值,放入 $parameters 中,下面代码就有所体现啦

Illuminate\Routing\RouteDependencyResolverTrait

protected function transformDependency(ReflectionParameter $parameter, $parameters)
{
    $class = $parameter->getClass();

    if ($class && ! $this->alreadyInParameters($class->name, $parameters)) {
        // $parameter->isDefaultValueAvailable(),表示当前参数有没有默认值,有则返回 true
        return $parameter->isDefaultValueAvailable()
            ? $parameter->getDefaultValue()
            : $this->container->make($class->name);
    }
}

关于 isDefaultValueAvailable 的官方文档-->传送门

本篇如有错误、不当或者需补充的内容,请各位同僚多提宝贵意见。

本文章首发在 LearnKu.com 网站上。

上一篇 下一篇
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 0
发起讨论 查看所有版本


暂无话题~