原本預定要看 middleware,但因為發生忘了帶充電器的蠢事,沒辦法用自己習慣的筆電,所以換講比較簡單的 Marcoable。
先來個大哉問。一般最先想到的就是繼承(extends),Carbon 正是一個非常好的例子。再來可能就會從設計著手,比方說使用 strategy pattern 或 pluggable adapter pattern。
繼承確實能做到擴展,但它有兩個限制,第一:它是靜態的;第二:不支援多重繼承。平常使用並不會有太大問題,但假使想引用第三方擴展套件時,如果第三方使用繼承擴展功能,因為這兩個限制,使得開發者必須改繼承第三方套件,才可實作自己的擴展,這是非常不便的。
Laravel 實作了一套動態擴展功能的機制,讓開發者跟第三方套件都可以動態為既有類別加功能,下面是一個簡單的範例:
class Foo
{
use Marcoable;
private $value = 'something';
public function setValue($v)
{
$this->value = $v;
}
}
Foo::macro('hello', function () {
return 'world';
});
Foo::macro('getValue', function () {
return $this->value;
});
Foo::hello(); // world
(new Foo())->getValue(); // something
今天就來分析這個神奇的功能吧。
marco()
的定義其實很單純,就是設定個值而已:
public static function macro($name, $macro)
{
static::$macros[$name] = $macro;
}
關鍵是在魔術方法 __call()
與 __callStatic()
的實作:
public static function __callStatic($method, $parameters)
{
// 找不到就丟例外
if (! static::hasMacro($method)) {
throw new BadMethodCallException(sprintf(
'Method %s::%s does not exist.', static::class, $method
));
}
// 如果是 Closure 就 bind static class 給它,讓它能存取得到靜態屬性
if (static::$macros[$method] instanceof Closure) {
return call_user_func_array(Closure::bind(static::$macros[$method], null, static::class), $parameters);
}
return call_user_func_array(static::$macros[$method], $parameters);
}
public function __call($method, $parameters)
{
// 找不到就丟例外
if (! static::hasMacro($method)) {
throw new BadMethodCallException(sprintf(
'Method %s::%s does not exist.', static::class, $method
));
}
$macro = static::$macros[$method];
// 如果是 Closure 就 bind 目前的實例給它,讓它能存取得到實例的屬性
if ($macro instanceof Closure) {
return call_user_func_array($macro->bindTo($this, static::class), $parameters);
}
return call_user_func_array($macro, $parameters);
}
Macroable 其實就這麼單純,而 Laravel 在設計上,因為有的物件有它自己 __call()
的方法,如 Router,為了避免衝突,它會這樣寫:
// 換個方法名稱
use Macroable {
__call as macroCall;
}
public function __call($method, $parameters)
{
// 如果有設定 marco,會優先呼叫 marco
if (static::hasMacro($method)) {
return $this->macroCall($method, $parameters);
}
// 處理自己的 __call() 邏輯
}
在目前追過的程式碼中,都是會以 marco 優先,然後才處理自己的 __call()
直接來看原始碼,再來看如何使用:
public static function mixin($mixin)
{
// 透過反射,取得反射方法的實例,主要是取得 public 與 protected 方法
$methods = (new ReflectionClass($mixin))->getMethods(
ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_PROTECTED
);
// 批次把所有要混入的方法使用 macro 加入
foreach ($methods as $method) {
// 先改成可以存取
$method->setAccessible(true);
// 參數 name 會是方法名稱,參數 macro 則是取得方法執行過後的結果。
static::macro($method->name, $method->invoke($mixin));
}
}
從原始碼分析可以得知,如果想把一開始的使用範例改用 mixin()
的話,寫法如下:
class Foo
{
use Marcoable;
private $value = 'something';
public function setValue($v)
{
$this->value = $v;
}
}
class Bar
{
public function hello()
{
return function () {
return 'world';
};
}
public function getValue()
{
return function () {
return $this->value;
};
}
}
Foo::mixin(new Bar());
Foo::hello(); // world
(new Foo())->getValue(); // something
如果想要為多個 Marcoable 的物件,加入一樣的實作時,使用 mixin()
會是更加簡單的方法。
因為整個過程是動態加入方法,而不是靜態的定義,所以這樣的事就有辦法達成:有兩個第三方套件會為 Router 加入自定義的實作,而應用程式也有為 Router 加入不一樣的實作,並覆寫第三方套件的實作。
這也是筆者認為 Laravel 神奇設計的其中之一。