Laravel核心程式碼學習 -- 控制器

kevinyan發表於2018-05-24

控制器

控制器能夠將相關的請求處理邏輯組成一個單獨的類, 通過前面的路由和中介軟體兩個章節我們多次強調Laravel應用的請求在進入應用後首現會通過Http Kernel裡定義的基本中介軟體

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管道物件將請求傳送通過這些路由上繫結的這些中間鍵,到達目的地後會執行路由繫結的控制器方法然後把執行結果封裝成響應物件,響應物件依次通過後置中介軟體最後返回給客戶端。

下面是剛才說的這些步驟對應的核心程式碼:

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

複製程式碼

所以路由通過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、呼叫控制器方法。

解決method引數依賴

解決方法的引數依賴通過RouteDependencyResolverTrait這一trait負責:

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出類物件。

    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]
        );
    }
複製程式碼

我們之前講服務容器時,裡面講的服務解析解決的是類構造方法的引數依賴,而這裡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原始碼學習裡,歡迎訪問閱讀。

相關文章