Laravel Macroable

鲁靖
2023-12-01

一、预备知识

Laravel Macroable 的使用需要对php闭包的相关知识有所了解,可以参看文档 php 闭包

二、代码分析

Laravel Macroable 源码对应 \Illuminate\Support\Traits\Macroable 这个trait 文件,这里将其代码剥离出来单独运行、测试,以便研究。
其源码如下

// Macroable.php
<?php
 
trait Macroable {
    /**
     * The registered string macros.
     * 变量是一个数组
     * 键为方法名,值为闭包,对应此方法的具体实现
     * @var array
     */
    protected static $macros = [];

    /**
     * Register a custom macro.
     * 注册方法,保存到 静态变量 $macros 中
     * @param  string  $name
     * @param  object|callable  $macro
     * @return void
     */
    public static function macro($name, $macro)
    {
        static::$macros[$name] = $macro;
    }

    /**
     * Mix another object into the class.
     *
     * @param  object  $mixin
     * @param  bool  $replace 如果方法已经存在是否覆盖,true表示覆盖,false表示不覆盖,
     *         true 意味着生效的是第一次注册的闭包,false 则意味着生效的是最后一次注册的闭包
     * @return void
     *
     * @throws \ReflectionException
     */
    public static function mixin($mixin, $replace = true)
    {
        // 获取 $mixin 对象的所有公有和保护方法
        $methods = (new ReflectionClass($mixin))->getMethods(
            ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_PROTECTED
        );

        foreach ($methods as $method) {
            // 依次激活该对象的每个方法,并将每个方法返回的匿名函数注册到 $macros 中
            if ($replace || ! static::hasMacro($method->name)) {
                $method->setAccessible(true);
                static::macro($method->name, $method->invoke($mixin));
            }
        }
    }

    /**
     * Checks if macro is registered.
     * 判断方法是否存在
     * @param  string  $name
     * @return bool
     */
    public static function hasMacro($name)
    {
        return isset(static::$macros[$name]);
    }

    /**
     * Dynamically handle calls to the class.
     *
     * @param  string  $method
     * @param  array  $parameters
     * @return mixed
     *
     * @throws \BadMethodCallException
     */
    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);
    }

    /**
     * Dynamically handle calls to the class.
     *
     * @param  string  $method
     * @param  array  $parameters
     * @return mixed
     *
     * @throws \BadMethodCallException
     */
    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);
    }
}

其中 macro、hasMacro 方法比较简单,重点是 __callStatic、__call 和 mixin 方法,下面分别进行分析,

2.1 __callStatic 和 __call

先看一个用法示例

require __DIR__.DIRECTORY_SEPARATOR.'Macroable.php';

class MacroTest
{
    // 引入 Macroable trait
    use Macroable;
}

// 注册一个名为 addedJoin 的方法,
// 作用是把多个字符串拼成一个字符串,字符串之间用 '-' 隔开
MacroTest::macro('addedJoin', function(...$string){
    return implode('-', $string);
});

// 注册后,可以以静态方法的方式调用 addedJoin 方法
// 好像 MacroTest 类增加了一个静态方法
echo MacroTest::addedJoin('a','b','c'),"\n";

// MacroTest 对象也可以调用 addedJoin 方法
// 好像 MacroTest 类增加了一个普通的public 成员函数
$obj = new MacroTest();
echo $obj->addedJoin('a','b','c'),"\n";

必须先注册方法的闭包,否则会报异常,整个实现实际上归功于 Macroable 中定义的两个魔术方法:__callStatic 和 __call,PHP中如果调用的方法不存在,会触发调用相应的魔术方法,静态方法对应__callStatic方法,普通方法对应 __call方法。阅读代码会发现 Macroable 中两个魔术方法的实现是类似的,都是先根据方法名找到注册的闭包,然后进行绑定,最后调用绑定后的闭包。

虽然注册的方法同时支持静态和非静态两种调用方式,但具体应该怎么调用实际上取决于闭包的具体实现,如果注册的方法使用了 $this 那么显然只能通过类对象调用。

2.2 mixin

mixin 用于一次性把指定对象的所有公有和保护方法加到另一个类中,需要注意的是其中的注册语句

                static::macro($method->name, $method->invoke($mixin))

$method->invoke($mixin) 这一句意味着注册是调用了对象相应方法的返回值,这意味着被添加的对象所有的公有、保护方法都需要返回一个闭包,这其实是一个很大的限制,因为这意味着公有的构造函数和返回对象本身的方法都不能有,也就是这个类不能产生对象,只能包含静态成员和返回闭包的接口。

class ObjNotOk
{
    public function notReturnClosure(... $string)
    {
        // 这样的类对象不能用于mixin方法加到另一个类
        return implode('-', $string);
    }
}    

下面是可用于mixin的类示例

<?php
require __DIR__ . DIRECTORY_SEPARATOR . 'Macroable.php';

class MacroTest
{
    use Macroable;
}

class Str
{
    public function join()
    {
        return function (...$string) {
            return implode('-', $string);
        };
    }

    public function split()
    {
        return function (string $string) {
            return explode('-', $string);
        };
    }
}

MacroTest::mixin(new Str(100), true);

echo MacroTest::join('a', 'b', 'c'), "\n";
print_r(MacroTest::split('a-b-c'));

参考

https://blog.csdn.net/larance001/article/details/121479499

 类似资料:

相关阅读

相关文章

相关问答