Laravel 提供的
Macroable
可以在不改變類結構的情況為其擴充套件功能,本文將教你從零開始構建一個Macroable
。
Macroable
的核心是基於匿名函式的繫結功能,先來回顧下匿名函式的繫結功能。
預備知識
PHP 可通過匿名函式的繫結功能來擴充套件類或者例項的功能。
定義類
class Foo
{
}
定義匿名函式
$join = function(...$string){
return implode('-', $string);
}
使用 bindTo
為類的例項新增 join
功能
$foo = new Foo();
$bindFoo = $join->bindTo($foo, Foo::class);
$bindFoo('a', 'b', 'c'); // "a-b-c"
PHP 7 之後引入了 call
方法更高效的實現了該功能
$foo = new Foo();
$join->call($foo, 'a', 'b', 'c'); // "a-b-c"
對於本例而言,使用 bind
方法進行靜態繫結更貼合實際場景
$bindClass = \Closure::bind($join, null, Foo::class);
$bindClass('a', 'b', 'c'); // "a-b-c"
如果還沒看懂的話,可以參考我之前寫的 PHP 核心特性 - 匿名函式。
通過匿名函式擴充套件類的功能
瞭解了匿名函式的繫結功能後,就可以對其進行簡單的封裝了。首先,定義一個陣列用來儲存要新增的功能列表
<?php
trait Macroable {
// 儲存要擴充套件的功能
protected static $macros = [];
// 新增要擴充套件功能
public static function macro($name, $macro)
{
static::$macros[$name] = $macro;
}
}
macros
屬性儲存了要新增的功能名及實現,在類中使用該 Trait
class Foo
{
use Macroable;
}
新增 join
功能
Foo::macro('join', function(...$string){
return implode('-', $string);
});
join
功能及對應的實現已經儲存到了 macros
陣列中。接下來是呼叫 join
方法
Foo::join('a', 'b', 'c')
由於 Foo
中的 join
靜態方法不存在,會自動將方法名和引數轉發到 __callStatic
魔術方法中。因此,在魔術方法中手動呼叫繫結的匿名函式即可
public static function __callStatic($name, $parameters)
{
// 獲取匿名函式
$macro = static::$macros[$name];
// 繫結到類
$bindClass = \Closure::bind($macro, null, static::class);
// 呼叫並返回撥用結果
return $bindClass(...$parameters);
}
測試
echo Foo::join('a', 'b', 'c'); // a-b-c
動態擴充套件與靜態擴充套件的實現原理完全一樣
public function __call($name, $parameters)
{
// 獲取匿名函式
$macro = static::$macros[$name];
// 呼叫並返回撥用結果
return $macro->call($this, ...$parameters);
}
測試
$foo = new Foo();
echo $foo->join('a', 'b', 'c'); // 'a-b-c'
通過物件例項來擴充套件類的功能
之前,我們通過匿名函式的方式擴充套件類的功能
Foo::macro('join', function(...$string){
return implode('-', $string);
});
現在,我們考慮如何通過物件的方式來實現同樣的功能。首先,將匿名函式改造成類
final class Join
{
public function __invoke(...$string)
{
return implode('-', $string);
}
}
當以函式的方式呼叫該類時,就會啟用 __invoke
方法
$join = new Join();
$join('a', 'b', 'c'); // a-b-c
現在,將 Join
的例項新增到類中,實現同樣的效果
Foo::macro('join', new Join());
只需要對原有的 __callStatic
方法增加一層判斷即可。如果是匿名函式則繫結該匿名函式並呼叫,如果是物件則以函式的方式呼叫物件,啟用物件的 __invoke
方法。
public function __call($name, $parameters)
{
$macro = static::$macros[$name];
if($macro instanceof Closure){
return $macro->call($this, ...$parameters);
}
return $macro(...$parameters);
}
public static function __callStatic($name, $parameters)
{
$macro = static::$macros[$name];
// 閉包
if($macro instanceof Closure){
$bindClass = \Closure::bind($macro, null, static::class);
return $bindClass(...$parameters);
}
// 物件例項,則啟用該物件
return $macro(...$parameters);
}
測試
Foo::join('a', 'b', 'c'); // a-b-c
同時擴充套件多個方法
最後,Laravel 的 Macroable
還實現了同時擴充套件多個方法。
原理其實很簡單,將功能類似的方法定義在一個類中
final class Str
{
public function join()
{
// 返回匿名函式
return function(...$string){
return implode('-', $string);
};
}
public function split()
{
// 返回匿名函式
return function(string $string){
return explode('-', $string);
};
}
}
每個方法都返回了匿名函式,我們只需要將每個匿名函式新增到 $macros
列表中即可,只需要用到 PHP 的反射功能即可實現。
public static function mixin($mixin)
{
// 通過反射獲取物件的 ReflectionMethod 列表
$methods = (new \ReflectionClass($mixin))->getMethods(
\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED
);
// 遍歷 ReflectionMethod 列表,依次儲存到 $macros 中
foreach ($methods as $method) {
$method->setAccessible(true);
// 依次啟用該物件的每個方法,每個方法返回的匿名函式剛好儲存在 $macros 中
static::macro($method->name, $method->invoke($mixin));
}
}
測試
Foo::mixin(new Str());
Foo::join('a', 'b', 'c');
Foo::split('a-b-c');
當然,這個功能沒多大作用,還不如直接用 Trait
來的直觀方便。
點選 連結,免費加入心智極客的知識星球分享群,共同成長。