分析 Routing(2)

一樣,先從類別圖開始。這次因為相關的類別太多,所以會先以 Router 設定 Controller 以及 Request 如何對應到正確的 Controller 為主,而不會把所有類別都硬塞到這次的圖裡。

@startuml
interface Illuminate\Contracts\Routing\BindingRegistrar
interface Illuminate\Contracts\Routing\Registrar

class Router {
  # current : Route, current route
  # currentRequest : Illuminate\Http\Request
}

Router -> Illuminate\Support\Traits\Macroable
Illuminate\Contracts\Routing\BindingRegistrar <.. Router
Illuminate\Contracts\Routing\Registrar <.. Router
Router o-- RouteCollection: new instance
Router --> Route: new instance
Router o-- Illuminate\Contracts\Events\Dispatcher
Router o-- Illuminate\Container\Container
Router --> Illuminate\Routing\Pipeline
Router --> RouteRegistrar
@enduml

昨天的程式碼再看一下:

$this->app->make('router')
->prefix('api')
->middleware('api')
->namespace($this->namespace)
->group(base_path('routes/api.php'));

$this->app->make('router')
->middleware('web')
->namespace($this->namespace)
->group(base_path('routes/web.php'));

Router::prefix() 的原始碼,會發現它是宣告成 protected 的,很不可思議,因為上面的程式碼是 public 呼叫。其實這是 Magic Method __call() 的關係,來看它是怎麼做的:

public function __call($method, $parameters)
{
// 如果這個方法名稱是有設定 macro 的話,就會呼叫 Macroable::__call()
if (static::hasMacro($method)) {
return $this->macroCall($method, $parameters);
}

// 如果方法名是 middleware 的話
if ($method == 'middleware') {
return (new RouteRegistrar($this))->attribute($method, is_array($parameters[0]) ? $parameters[0] : $parameters);
}

// 如果方法名不是 middleware 的話
return (new RouteRegistrar($this))->attribute($method, $parameters[0]);
}

如果 public 呼叫 prefix() 的話,就會走到最後一行。而類別裡面呼叫的話,則會是宣告 protected 的方法。

事實上,方法名稱重複並不好,因為原本以為不能呼叫,但實際可以,通常不會想到是 __call() 的關係。

RouteRegistrar 是一個輔助類別,可以讓主要類別處理某些事比較容易一點。這個做法與 ContainerContextualBindingBuilder 是一樣的設計。

如果是使用 prefix() 函式觸發 __call() 的話,會得到下面這樣的等價物件:

return (new RouteRegistrar($this))->attribute('prefix', $prefixValue);

RouteRegistrar 的建構子很單純,只是把 Router 找個位置放而已,再來看 attribute()

public function attribute($key, $value)
{
// 不在清單中會丟例外
if (! in_array($key, $this->allowedAttributes)) {
throw new InvalidArgumentException("Attribute [{$key}] does not exist.");
}

// 把 alias 的 key 轉成原本的名稱,alias 只有一個:name 會轉成 as
$this->attributes[Arr::get($this->aliases, $key, $key)] = $value;

// 回傳 RouteRegistrar 實例
return $this;
}

這裡可以知道,當使用 prefix() 時,會回傳 RouteRegistrar 實例,但後面 middleware()namespace() 等,又是如何實現的呢?答案一樣是 Magic Method,在 RouteRegistrar::__call() 裡:

public function __call($method, $parameters)
{
// 如果是 get、post 等,屬於 Router 的方法時,會回頭去呼叫 router 的方法
if (in_array($method, $this->passthru)) {
return $this->registerRoute($method, ...$parameters);
}

// 如果是可設定的屬性的話,就呼叫 attribute() 設定屬性。可以發現這裡的程式碼,其實跟 Router::__call() 非常像
if (in_array($method, $this->allowedAttributes)) {
if ($method == 'middleware') {
return $this->attribute($method, is_array($parameters[0]) ? $parameters[0] : $parameters);
}

return $this->attribute($method, $parameters[0]);
}

// 不符合規則的方法名稱就丟例外
throw new BadMethodCallException(sprintf(
'Method %s::%s does not exist.', static::class, $method
));
}

如果一直設定 attribute 的話,這些屬性都只會存在 RouteRegistrar 實例裡,跟 Router 實例就無法連結上。實際會把 attribute 回寫到 Router 上的方法是 registerRoute()

protected function registerRoute($method, $uri, $action = null)
{
// 如果 $action 不是 array 的話,就依現有的 attributes 生一個出來
if (! is_array($action)) {
$action = array_merge($this->attributes, $action ? ['uses' => $action] : []);
}

// 將產生出來的 $action 用在原本的 Router 方法上
return $this->router->{$method}($uri, $this->compileAction($action));
}

compileAction() 大概看一下:

protected function compileAction($action)
{
// 是 null 就回傳 attributes,不過從 registerRoute() 進來的話,不會是 null
if (is_null($action)) {
return $this->attributes;
}

// 如果是字串或 Closure 就組一個 $action
if (is_string($action) || $action instanceof Closure) {
$action = ['uses' => $action];
}

// 將組出來的 $action 跟 attributes 合併
return array_merge($this->attributes, $action);
}

這裡的程式碼,跟 registerRoute() 一開始做的事沒什麼兩樣,也許有其他理由但目前不清楚。

綜合以上的分析,我們可以知道下面兩段程式碼是等價的:

$this->app->make('router')
->prefix('api')
->middleware('api')
->namespace($this->namespace)
->get('/', function() {
return 'whatever';
});

// -------------------

$this->app->make('router')
->get('/', [
'prefix' => 'api',
'middleware' => ['api'],
'namespace' => $this->namespace,
'uses' => function() {
return 'whatever';
},
]);

會這樣設計,有一個原因是:Laravel 對 action 資訊的定義正是長這樣,但如果直接使用 array 傳遞參數的話,最明顯的問題就是違反最小知識原則(Least Knowledge Principle),因為 array 的格式,等於是曝露資料細節,當 array 規格調整時,將會引發一場災難;相對的這樣設計,雖然程式碼顯得複雜許多,不過帶來的好處就是,使用起來非常直觀,且依賴只有該實例曝露出來的方法,這也是比較容易調整的(如:使用 alias)。

group() 比較複雜了一點,明天再接著繼續看。