【Laravel-海賊王系列】第十三章,路由&控制器解析

路由

一個請求如何跨過山和大海來到控制器的地盤。php

註冊路由

這塊代碼是在 Application 的構造函數中加載的web

public function __construct($basePath = null)
{
    ...
    $this->registerBaseServiceProviders();
    ...
}
複製代碼
protected function registerBaseServiceProviders()
{
    ...
    $this->register(new RoutingServiceProvider($this));
    ...
}
複製代碼

展開完整的服務提供者bootstrap

<?php

namespace Illuminate\Routing;

use Illuminate\Support\ServiceProvider;
use Psr\Http\Message\ResponseInterface;
use Zend\Diactoros\Response as PsrResponse;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Bridge\PsrHttpMessage\Factory\DiactorosFactory;
use Illuminate\Contracts\View\Factory as ViewFactoryContract;
use Illuminate\Contracts\Routing\ResponseFactory as ResponseFactoryContract;
use Illuminate\Routing\Contracts\ControllerDispatcher as ControllerDispatcherContract;

class RoutingServiceProvider extends ServiceProvider
{

    public function register()
    {
        $this->registerRouter();
        $this->registerUrlGenerator();
        $this->registerRedirector();
        $this->registerPsrRequest();
        $this->registerPsrResponse();
        $this->registerResponseFactory();
        $this->registerControllerDispatcher();
    }

    protected function registerRouter()
    {
        $this->app->singleton('router', function ($app) {
            return new Router($app['events'], $app);
        });
    }

    protected function registerUrlGenerator()
    {
        $this->app->singleton('url', function ($app) {
            $routes = $app['router']->getRoutes();

            $app->instance('routes', $routes);

            $url = new UrlGenerator(
                $routes, $app->rebinding(
                    'request', $this->requestRebinder()
                ), $app['config']['app.asset_url']
            );

            $url->setSessionResolver(function () {
                return $this->app['session'];
            });

            $url->setKeyResolver(function () {
                return $this->app->make('config')->get('app.key');
            });

            $app->rebinding('routes', function ($app, $routes) {
                $app['url']->setRoutes($routes);
            });

            return $url;
        });
    }

    protected function requestRebinder()
    {
        return function ($app, $request) {
            $app['url']->setRequest($request);
        };
    }

    protected function registerRedirector()
    {
        $this->app->singleton('redirect', function ($app) {
            $redirector = new Redirector($app['url']);

            if (isset($app['session.store'])) {
                $redirector->setSession($app['session.store']);
            }

            return $redirector;
        });
    }

    protected function registerPsrRequest()
    {
        $this->app->bind(ServerRequestInterface::class, function ($app) {
            return (new DiactorosFactory)->createRequest($app->make('request'));
        });
    }

    protected function registerPsrResponse()
    {
        $this->app->bind(ResponseInterface::class, function () {
            return new PsrResponse;
        });
    }

    protected function registerResponseFactory()
    {
        $this->app->singleton(ResponseFactoryContract::class, function ($app) {
            return new ResponseFactory($app[ViewFactoryContract::class], $app['redirect']);
        });
    }

    protected function registerControllerDispatcher()
    {
        $this->app->singleton(ControllerDispatcherContract::class, function ($app) {
            return new ControllerDispatcher($app);
        });
    }
}

複製代碼

後面在使用中會涉及這裏註冊的對象,紅框內就是註冊的綁定關係。api

啓動在這裏並無完成,這僅僅是啓動系統的基礎路由,在 app.php 中還有一個路由服務提供者 RouteServiceProvider數組

<?php

namespace App\Providers;

use Illuminate\Support\Facades\Route;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;

class RouteServiceProvider extends ServiceProvider
{
    protected $namespace = 'App\Http\Controllers';

    public function boot()
    {
        // "boot() 方法是在服務提供者全部 register() 方法執行完成以後在統一執行的"
        
        // "這段代碼最後會調用 $this->map();"
        parent::boot();
    }

    public function map()
    {
        $this->mapApiRoutes();

        $this->mapWebRoutes();

    }

    // "這一塊的邏輯很是複雜就不展開了,主要功能就是優先加載 cache/routes.php,若是不存在 則從給定的路徑加載路由文件"
   
    protected function mapWebRoutes()
    {
        Route::middleware('web')
             ->namespace($this->namespace)
             ->group(base_path('routes/web.php')); 
    }

    protected function mapApiRoutes()
    {
        Route::prefix('api')
             ->middleware('api')
             ->namespace($this->namespace)
             ->group(base_path('routes/api.php'));
    }
}

複製代碼

內核啓動

註冊完成以後就是開始處理,是從內核的 handle() 方法開始處理請求緩存

protected function sendRequestThroughRouter($request)
{
    ...

    return (new Pipeline($this->app))
                ->send($request)
                ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
                ->then($this->dispatchToRouter());
}
複製代碼

這段代碼在 【Laravel-海賊王系列】第七章,Pipeline 類解析 解析過了 不瞭解執行邏輯請先看上一篇哦~bash

這裏會在運行完中間件以後最後運行 $this->dispatchToRouter() 這個方法。session

$this->router 對象是在內核的構造函數注入的 \Illuminate\Routing\Router 對象閉包

protected function dispatchToRouter()
{
    return function ($request) {
        $this->app->instance('request', $request);
        
        return $this->router->dispatch($request);
    };
}
複製代碼

那麼咱們接着看 dispatch 方法app

public function dispatch(Request $request)
{
    $this->currentRequest = $request;

    return $this->dispatchToRoute($request);
}
複製代碼

轉發一個請求給路由返回一個響應對象

public function dispatchToRoute(Request $request)
{
    return $this->runRoute($request, $this->findRoute($request));
}
複製代碼

找到路由

個人理解:router 表明路由器,route 則是表明一次路由的對象,

全部路由器的功能就是執行,派發路由對象。因此咱們須要先經過請求來拿到一個路由對象

protected function findRoute($request)
{
    $this->current = $route = $this->routes->match($request);

    // "綁定最新的 $route 對象到容器"
    
    $this->container->instance(Route::class, $route);
    
    // "返回路由"
    return $route;
}
複製代碼

繼續分析 $this->routes->match($request);,

這裏的 $this->routes 是構造函數注入的 Illuminate\Routing\RouteCollection 對象

public function match(Request $request)
{

    $routes = $this->get($request->getMethod()); 

    $route = $this->matchAgainstRoutes($routes, $request);

    if (! is_null($route)) {
        return $route->bind($request);
    }

    $others = $this->checkForAlternateVerbs($request);

    if (count($others) > 0) {
        return $this->getRouteForMethods($request, $others);
    }

    throw new NotFoundHttpException;
}
複製代碼

$routes 對象這裏面的值來自與路由緩存文件或者路由文件解析結果

繼續看 $route = $this->matchAgainstRoutes($routes, $request); 執行結果從請求中匹配對應路由並返回

若是沒有匹配的路由則使用請求方法之外的方法繼續匹配

public static $verbs = ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'];

protected function checkForAlternateVerbs($request)
{
    $methods = array_diff(Router::$verbs, [$request->getMethod()]);

    $others = [];

    foreach ($methods as $method) {
        if (! is_null($this->matchAgainstRoutes($this->get($method), $request, false))) {
            $others[] = $method;
        }
    }

    return $others;
}
複製代碼

執行完成返回 $other 數組,若是仍是沒有則拋出throw new NotFoundHttpException;

這裏不詳細敘述了,若是匹配成功咱們將獲得一個 Illuminate\Routing\Route 對象傳遞下去。

派發路由

當咱們獲得路由對象以後就是派發它了,根據給定的路由返回響應對象

protected function runRoute(Request $request, Route $route)
{
    // "將這個閉包設置到 request 對象的 $this->routeResolver 成員上"
    
    $request->setRouteResolver(function () use ($route) {
        return $route;
    });

    // "執行路由匹配的事件,框架剛啓動的時候這裏什麼都不作"
    $this->events->dispatch(new Events\RouteMatched($route, $request));

    return $this->prepareResponse($request,
        $this->runRouteWithinStack($route, $request)
    );
}
複製代碼

獲取響應

執行到這裏就已經到了最後的部分了

return $this->prepareResponse($request,
        $this->runRouteWithinStack($route, $request)
    );
複製代碼

這個方法就是將 $request$response 根據裏面的屬性封裝好數據返回而已。

public function prepareResponse($request, $response)
{
    return static::toResponse($request, $response);
}
複製代碼

重點看 $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()
                        );
                    });
}
複製代碼

又到了這種用法,不理解執行邏輯請看第七章,

根據 Pipeline 的使用原理,咱們在經過全部 $middleware

以後會將 $requeset 傳遞給閉包來結束

因此這是終點!

function ($request) use ($route) {
                        return $this->prepareResponse(
                            $request, $route->run()
                        );
                    }
複製代碼

剛纔說過了 $this->prepareResponse() 這個方法沒什麼亮點就是

將請求和響應對象封裝返回,全部咱們應該知道了,$route->run() 將返回 response 對象!

控制器閃亮登場

來吧,經歷了無數使人髮指的封裝但願後面一片坦途,run()

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

總算看到了 runController() 方法了,想必路由跨過山和大海最總的歸宿也到這兒了

$this->isControllerAction() 是判斷路由是閉包仍是字符串

若是是字符串向上圖紅框中的內容則執行

protected function runController()
{
    return $this->controllerDispatcher()->dispatch(
        $this, $this->getController(), $this->getControllerMethod()
    );
}
複製代碼

這裏是調用 Illuminate\Routing\ControllerDispatcherdispatch 方法

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

callAction 來自全部控制器基礎的 Illuminate\Routing\Controller

public function callAction($method, $parameters)
{
    return call_user_func_array([$this, $method], $parameters);
}
複製代碼

沒什麼好講的其實就是調用控制器對應的方法。

若是路由是閉包形式,則直接抽取路由對象中的閉包進行調用

protected function runCallable()
{
    $callable = $this->action['uses'];

    // "經過容器抽取依賴的參數傳入閉包運行"
    return $callable(...array_values($this->resolveMethodDependencies(
        $this->parametersWithoutNulls(), new ReflectionFunction($this->action['uses'])
    )));
}
複製代碼

結語

總算結束了,Laravel 路由在啓動階段註冊了很是多的類,

1.Application 構造階段 $this->register(new RoutingServiceProvider($this));

2.Kernel handle() bootstrap() 階段加載服務提供者的時候包含了 App\Providers\RouteServiceProvider::class,

這兩個階段註冊加上加載的邏輯是很是複雜,可是目的也很簡單從就是從路由文件轉成路由對象的過程,沒有力氣分析進去。

其餘的就是最後一直調用到控制器的過程,其中最後的 resolveClassMethodDependenciesresolveMethodDependencies

也是很是值得研究的代碼。

相關文章
相關標籤/搜索