Code前端首页关于Code前端联系我们

解读Laravel核心源码:Controller

terry 2年前 (2023-09-24) 阅读数 55 #后端开发

控制器可以将相关的请求处理逻辑封装到一个单独的类中。在前面两章路由和中间件中,我们经常强调来自Laravel应用程序的请求进入应用程序后会被传递。 Http 内核中定义的基本中间件。

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,
];

接下来,Http Kernel会通过dispatchToRoute将请求对象传递给路由对象进行处理。路由对象收集绑定到路由的中间件,然后像上面的 Http 内核一样使用 Pipeline 管道对象。该请求是通过与这些路由关联的中间密钥发送的。到达目的地后,执行路由绑定的控制器方法,并将执行结果封装到响应对象中。响应对象经过后置中间件,最终返回给客户端。

下面是刚才提到的步骤对应的核心代码:

namespace Illuminate\Foundation\Http;class Kernel implements KernelContract{    protected function dispatchToRouter()
    {        return function ($request) {            $this->app->instance('request', $request);            return $this->router->dispatch($request);
        };
    }
}namespace Illuminate\Routing;class Router implements RegistrarContract, BindingRegistrar{    
    public function dispatch(Request $request)
    {        $this->currentRequest = $request;        return $this->dispatchToRoute($request);
    }    
    public function dispatchToRoute(Request $request)
    {        return $this->runRoute($request, $this->findRoute($request));
    }    
    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)
        );
    }    
    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()
                        );
                    });
    
    }
}namespace Illuminate\Routing;class 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();
        }
    }

}

我们在上一篇文章中详细讲解了Pipeline、中间件和路由的原理。接下来,让我们看看请求最终何时找到路由。 Laravel 是如何在对应的控制器方法之后,将合适的参数注入到控制器方法中,并调用控制器方法的。

解析控制器和方法名称

路由运行控制器方法如何工作runController现在解析了路由中相应的控制器名称和方法名称。在路由章节中,我们说过路由对象的action属性如下:

[    'uses' => 'App\Http\Controllers\SomeController@someAction',    'controller' => 'App\Http\Controllers\SomeController@someAction',    'middleware' => ...
]

class Route{    protected function isControllerAction()
    {        return is_string($this->action['uses']);
    }    protected function runController()
    {        return (new ControllerDispatcher($this->container))->dispatch(            $this, $this->getController(), $this->getControllerMethod()
        );
    }    
    public function getController()
    {        if (! $this->controller) {
            $class = $this->parseControllerCallback()[0];            $this->controller = $this->container->make(ltrim($class, '\\'));
        }        return $this->controller;
    }    
    protected function getControllerMethod()
    {        return $this->parseControllerCallback()[1];
    }    
    protected function parseControllerCallback()
    {        return Str::parseCallback($this->action['uses']);
    }
}class Str{    //解析路由里绑定的控制器方法字符串,返回控制器和方法名称字符串构成的数组
    public static function parseCallback($callback, $default = null)
    {        return static::contains($callback, '@') ? explode('@', $callback, 2) : [$callback, $default];
    }
}

所以Routing使用方法parseCallback来解析Uses配置项中的控制器字符串。以数组形式返回,数组中的第一项是控制器的名称,第二项是方法的名称。检索控制器和方法名称字符串后,路由对象将其自身、控制器和方法名称传递给类 Illuminate\Routing\ControllerDispatcher,该类由 ControllerDispatcher 完成。调用控制器方法。我们来详细看看ControllerDispatcher是如何调用控制器方法的。

class ControllerDispatcher{    use RouteDependencyResolverTrait;    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));
    }
}

从上面可以明显看出,ControllerDispatcher中控制器的运行分为两步:解析方法resolveClassMethodDependencies和调用控制器方法。

解决方法参数依赖关系

通过 RouteDependencyResolverTrait解决方法参数依赖关系此 属性 负责:

trait RouteDependencyResolverTrait
{    protected function resolveClassMethodDependencies(array $parameters, $instance, $method)
    {        if (! method_exists($instance, $method)) {            return $parameters;
        }        
        
        return $this->resolveMethodDependencies(
            $parameters, new ReflectionMethod($instance, $method)
        );
    }    //参数为路由参数数组$parameters(可为空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;
    }
    
}

应用于 PHP求解 的方法参数依赖性时的反射类ReflectionMethod用于对控制器方法执行方向工程。通过反射对象获取到参数后,判断现有参数的类型提示(typehint)是否为类对象参数。如果它是一个类对象参数并且它是。如果现有参数中没有相同类的对象,则类对象将通过服务容器make

    protected function transformDependency(ReflectionParameter $parameter, $parameters)
    {
        $class = $parameter->getClass();        if ($class && ! $this->alreadyInParameters($class->name, $parameters)) {            return $parameter->isDefaultValueAvailable()
                ? $parameter->getDefaultValue()
                : $this->container->make($class->name);
        }
    }    
    protected function alreadyInParameters($class, array $parameters)
    {        return ! is_null(Arr::first($parameters, function ($value) use ($class) {            return $value instanceof $class;
        }));
    }

解析完类对象后,需要将类对象插入到参数列表中

    protected function spliceIntoParameters(array &$parameters, $offset, $value)
    {
        array_splice(
            $parameters, $offset, 0, [$value]
        );
    }

之前我们讲服务容器时,我们讲的服务解析方案是类构造方法的参数依赖,但是这里solveClassMethodDependency中的解析就是具体的解决方案。给定方法的参数依赖是 Laravel 对方法依赖注入概念的实现。

当路由参数数组与服务容器构造的类对象数量之和不足以覆盖控制器方法参数数量时,需要判断该参数是否有默认参数,即: resolveMethodDependencies 块methodforeach中的分支else if在parameters中插入方法$的默认参数

} elseif (! isset($values[$key - $instanceCount]) &&
    $parameter->isDefaultValueAvailable()) {    $this->spliceIntoParameters($parameters, $key, $parameter->getDefaultValue());
}

调用控制器方法

方法的参数依赖关系解决后,就可以调用该方法了。这很简单。如果控制器有 callAction 方法,则调用 callAction 方法。否则,直接调用该方法。

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

运行并得到结果后,按照上面runRouteWithinStack中的逻辑将结果转换为响应对象。然后响应对象经过所有先前应用的中间件的后处理,最后返回给客户端。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门