深入淺出 Laravel Macroable

心智極客發表於2019-11-04

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 來的直觀方便。

點選 連結,免費加入心智極客的知識星球分享群,共同成長。