Laravel中的Macroable trait原理

Laravel 提供的 Macroable 可以在不改变类结构的情况为其扩展功能。

基本原理

利用PHP中的匿名函数可以动态绑定作用域来扩展类或者实例的功能。

定义类:

class A
{
}

定义匿名函数:


$sum = function (...$i) {
    $sum = 0;
    foreach ($i as $n) {
        $sum += $n;
    }
    return $sum;
};

使用匿名函数基类Closure的三个方法bindTobindcall 为类的实例添加 sum 功能


$ASum = $sum->bindTo(new A(), A::class);

$ASum(1, 2, 3);  // 6

// or
// $sum->call(new A(), 1, 2, 3);

// or 静态绑定
// $staticASum = \Closure::bind($sum, null, A::class);
// $staticASum(1, 2, 3);

Laravel中的 Macroable trait的实现

Illuminate\Support\Traits\Macroable是对上述基本原理的封装,$macros属性数组用来保存用户添加的扩展功能

trait Macroable
{
    // 保存用户添加的扩展功能
    protected static $macros = [];

    // 新注册一个扩展功能
    public static function macro($name, $macro)
    {
        static::$macros[$name] = $macro;
    }
}

当需要为特定类添加扩展功能时,添加Macroable trait

class A
{
    use Macroable;
}

添加sum功能到 A

A::macro("sum", function (...$i) {
    $sum = 0;
    foreach ($i as $n) {
        $sum += $n;
    }
    return $sum;
});

调用方法如下

A::sum(1,2,3);

// or
(new A())->sum(1, 2, 3);

调用上述A不存在的方法需要使用魔术方法__callStatic__call

public static function __callStatic($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) {
        $macro = $macro->bindTo(null, static::class);
    }

    return $macro(...$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) {
        $macro = $macro->bindTo($this, static::class);
    }

    return $macro(...$parameters);
}

上面两个方法同时支持扩展匿名函数和类实例对象,只需要将匿名函数换成支持函数调用的类即可

class Sum
{
    public function __invoke(int ...$i): int
    {
        $sum = 0;
        foreach ($i as $n) {
            $sum += $n;
        }
        return $sum;
    }
}

这样使用

A::macro("sum", new Sum());
A::sum(1,2,3);

// or
(new A())->sum(1, 2, 3);

mixin 注入对象的多个方法

mixin静态方法通过反射机制注入对象的多个非私有方法

public static function mixin($mixin, $replace = true)
{
    $methods = (new ReflectionClass($mixin))->getMethods(
        ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_PROTECTED
    );

    foreach ($methods as $method) {
        if ($replace || ! static::hasMacro($method->name)) {
            $method->setAccessible(true);
            static::macro($method->name, $method->invoke($mixin));
        }
    }
}

举例

class Math
{
    public function sum(): Closure
    {
        return function (int ...$i) {
            $sum = 0;
            foreach ($i as $n) {
                $sum += $n;
            }
            return $sum;
        };
    }

    public function minus(): Closure
    {
        return function (int $a, $b) {
            return $a - $b;
        };
    }
}

A::mixin(new Math());

echo A::sum(1, 2, 3);  //6
echo A::minus(20, 1);  //19

最后给类加上标签方便IDE代码提示

use Illuminate\Support\Traits\Macroable;

/**
 * @method static int sum(int ...$i)
 * @method int sum(int ...$i)
 * @method static int minus(int $a, $b)
 */
class A
{
    use Macroable;
}

Last modified on 2023-04-06

Comments Disabled.