Laravel核心代碼學習 -- 控制器

控制器

控制器可以將相關的請求處理邏輯組成一個單獨的類, 經過前面的路由和中間件兩個章節咱們屢次強調Laravel應用的請求在進入應用後首現會經過Http Kernel裏定義的基本中間件git

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 Kernel裏同樣用一個Pipeline管道對象將請求傳送經過這些路由上綁定的這些中間鍵,到達目的地後會執行路由綁定的控制器方法而後把執行結果封裝成響應對象,響應對象依次經過後置中間件最後返回給客戶端。github

下面是剛纔說的這些步驟對應的核心代碼:數組

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是如何爲控制器方法注入正確的參數並調用控制器方法的。bash

解析控制器和方法名

路由運行控制器方法的操做runController首現會解析出路由中對應的控制器名稱和方法名稱。咱們在講路由那一章裏說過路由對象的action屬性都是相似下面這樣的:app

[
	'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];
    }
}

複製代碼

因此路由經過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裏控制器的運行分爲兩步:解決method的參數依賴resolveClassMethodDependencies、調用控制器方法。ui

解決method參數依賴

解決方法的參數依賴經過RouteDependencyResolverTrait這一trait負責:this

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類來對控制器方法進行方向工程, 經過反射對象獲取到參數後會判斷現有參數的類型提示(type hint)是不是一個類對象參數,若是是類對象參數而且在現有參數中沒有相同類的對象那麼就會經過服務容器來make出類對象。spa

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;
        }));
    }
複製代碼

解析出類對象後須要將類對象插入到參數列表中去code

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

咱們以前講服務容器時,裏面講的服務解析解決的是類構造方法的參數依賴,而這裏resolveClassMethodDependencies裏解決的是具體某個方法的參數依賴,它是Laravel對method dependency injection概念的實現。

當路由的參數數組與服務容器構造的類對象數量之和不足以覆蓋控制器方法參數個數時,就要去判斷該參數是否具備默認參數,也就是會執行resolveMethodDependencies方法foreach塊裏的else if分支將參數的默認參數插入到方法的參數列表$parameters中去。

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

調用控制器方法

解決完method的參數依賴後就該調用方法了,這個很簡單, 若是控制器有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裏的邏輯,結果會被轉換成響應對象。而後響應對象會依次通過以前應用過的全部中間件的後置操做,最後返回給客戶端。

本文已經收錄在系列文章Laravel源碼學習裏,歡迎訪問閱讀。

相關文章
相關標籤/搜索