原本預定要看 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(); (new Foo())->getValue();
|
今天就來分析這個神奇的功能吧。
分析 marco()
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 )); }
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];
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) { if (static::hasMacro($method)) { return $this->macroCall($method, $parameters); }
}
|
在目前追過的程式碼中,都是會以 marco 優先,然後才處理自己的 __call()
分析 mixin()
直接來看原始碼,再來看如何使用:
public static function mixin($mixin) { $methods = (new ReflectionClass($mixin))->getMethods( ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_PROTECTED );
foreach ($methods as $method) { $method->setAccessible(true); 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(); (new Foo())->getValue();
|
如果想要為多個 Marcoable 的物件,加入一樣的實作時,使用 mixin()
會是更加簡單的方法。
因為整個過程是動態加入方法,而不是靜態的定義,所以這樣的事就有辦法達成:有兩個第三方套件會為 Router 加入自定義的實作,而應用程式也有為 Router 加入不一樣的實作,並覆寫第三方套件的實作。
這也是 Laravel 神奇設計的其中之一。