在业务开发过程中很少有遇到需要动态扩展类方法的场景, 但在框架开发中, 有时候就需要提供这样的特性, 以便开发者实现一些高级的功能.

尽管我现在没有这样的高级需求, 但是简单看了一下 Laravel 源码对此的一种实现方式.

trait Macroable
{
    protected static $macros = [];

    public static function macro($name, $macro)
    {
        static::$macros[$name] = $macro;
    }
    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);
    }
}

原理上说起来非常简单, 在 Laravel 中利用了 PHP 的 __call()__callStatic() 两个魔术方法, 从中读取一个静态的属性 $macros, 并尝试用 call_user_func_array() (call_user_func() 也可以实现一样的功能, 二者只是接收参数方式略有差异) 调用该方法.

据此, 一个 Macroable 特征需要定义至少三个方法: macro(), __call()__callStatic(), 通过第一个方法 (public) 动态地更改静态属性 $macros, 并在魔术方法中尝试调用.

要特别注意的一点是, 动态添加闭包方法时, 进行了 Closure::bind()$macro->bindTo() 两个额外的操作, 绑不绑定作用域的区别在于:

require 'laravel/vendor/autoload.php';

use Illuminate\Support\Traits\Macroable;

class ClassB
{
    use Macroable;

    public $member = 'B';
}

class ClassA
{
    public $member = 'A';

    public function demo() {
        $b = new ClassB();

        ClassB::macro('extraMethod', function () {
            return $this->member;
        });

        return $b->extraMethod();
    }
}

echo (new ClassA())->demo();

没有绑定作用域的情况下, php demo.php 输出 'A', 重新绑定作用域后, php demo.php 输出 'B'. 上面可见我们扩展的是 ClassB 类, 所以期望的输出应该是 'B', 因此 bind() 的操作是不可少的.

Laravel 的 Macroable 特征还额外提供了一个 mixin() 方法, 用于将另一个类 (对象) 的方法全部混入 (mixin) 到一个类中:

trait Macroable
{
    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));
        }
    }
}

了解过 PHP 反射 (Reflection) 机制的话, 这个方法的实现非常易懂 (即使不了解反射应该也很好理解), 这个方法很好地体现了反射的强大功能.