iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 15
1

今天要接著來看,是如何設定各式各樣的 route 了。先來看 get()post() 的原始碼:

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

這裡可以知道,實際做事的是 addRoute()

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

RouteCollection::add() 是新增一筆 Route 並把該 Route 實例回傳。這樣的設計是為了讓後面的 fluent pattern 可以更加直觀:

Route::get('/', function() {
    return 'whatever';
})->name('some-name');

createRoute() 則是建立 Route 實例:

protected function createRoute($methods, $uri, $action)
{
    // 如果 $action 是 controller@method 的寫法的話,把它轉成 Controller Action
    if ($this->actionReferencesController($action)) {
        $action = $this->convertToControllerAction($action);
    }

    // 產生實例
    $route = $this->newRoute(
        $methods, $this->prefix($uri), $action
    );

    // 如果昨天的 groupStack 有東西,就將設定寫到 route 裡
    if ($this->hasGroupStack()) {
        $this->mergeGroupAttributesIntoRoute($route);
    }

    // 把 where 設定寫入 route
    $this->addWhereClausesToRoute($route);

    return $route;
}

順帶一提,可以發現 Laravel 在撰寫主要邏輯時,都會寫的比較白話;當追查後面實作才會覺得比較「技術」。

actionReferencesController() 的任務是確認 Action 是不是指向 Controller:

protected function actionReferencesController($action)
{
    // 當不是 Closure ,而且是 string 或者是 ['uses' => string] 的話,就假定它是 Controller
    if (! $action instanceof Closure) {
        return is_string($action) || (isset($action['uses']) && is_string($action['uses']));
    }

    return false;
}

轉換成 array 是呼叫 convertToControllerAction()

protected function convertToControllerAction($action)
{
    // 最終還是轉 array
    if (is_string($action)) {
        $action = ['uses' => $action];
    }

    // 如果 group stack 有東西的話,那 group 設定的 namespace 必須要加到 Controller 的前面
    if (! empty($this->groupStack)) {
        $action['uses'] = $this->prependGroupNamespace($action['uses']);
    }

    // 複製一份給 controller key
    $action['controller'] = $action['uses'];

    return $action;
}

prependGroupNamespace() 裡面有一段判斷可以看一鑑:

return isset($group['namespace']) && strpos($class, '\\') !== 0
        ? $group['namespace'].'\\'.$class : $class;

isset($group['namespace']) 應該不用多解釋,有趣的地方是 strpos($class, '\\') !== 0,這代表如果 Controller 給絕對路徑的 namespace 的話,是不會受到 group 的 namespace 影響的。假設我有一個 Controller,完整類別名為 \App\Http\Controllers\HelloController 則下面兩個是等價的:

文件其實沒有講到這個用法,要翻原始碼才會知道

// 假設預設 namespace 是 App\Http\Controllers

Route::get('foo', 'HelloController@method');
Route::get('foo', '\App\Http\Controllers\HelloController@method');

準備好 Action 後,就可以建構 Route 實例了。建構完 Route,會確認有沒有 group stack,有的話就呼叫 $this->mergeGroupAttributesIntoRoute() 合併 group stack 設定到 Route 實例裡

protected function mergeGroupAttributesIntoRoute($route)
{
    // 先取得既有 Action 再跟最後一個 group 設定合併,然後再設定回 Route 實例
    $route->setAction($this->mergeWithLastGroup($route->getAction()));
}

最後,呼叫 addWhereClausesToRoute() 把 global where 設定加進 Route 實例裡。

protected function addWhereClausesToRoute($route)
{
    $route->where(array_merge(
        $this->patterns, $route->getAction()['where'] ?? []
    ));

    return $route;
}

這裡其實就是實作 Global Constraints 的功能。為何下面的 pattern() 設定會變成全域有效,就是上面這段程式碼實作出來的。

Route::pattern('id', '[0-9]+');

Route::get('user/{id}/{name}', function ($id, $name) {
    //
})->where(['name' => '[a-z]+']);

同時也可以知道 Route 所設定的 where 是 array 多筆的形式。

到目前為止,Router 跟設定有關的程式碼都看的差不多了,我們可以發現 Router 本身跟設定相關最核心的任務,其實就是產生 Action 並建構 Route 實例。而 Route 實例也不存放在 Router,而是放在 RouteCollection。這就是標準的 facade pattern,所有控制後面角色行為的互動,都可以靠 facade,也就是 Router 來處理;而且,facade pattern 的一個特色是,因為這三天已經知道必要的參數是哪些了,所以是可以繞過 Router 自己來處理的。

明天再來繼續看 Route 如何存放與處理 Action 的資訊。


上一篇
分析 Routing(3)
下一篇
分析 Routing(5)
系列文
Laravel 原始碼分析46

尚未有邦友留言

立即登入留言