Laravel 中,Collection 类本身实现了不少方法,比如 sumgroupBy 等,若想给这个类加上一些自定义的方法,有如下方案: 方法一:通过修改 Collection 类的源码,在其中加入自定义方法的代码。但是这样就要修改框架源码,并且升级框架源码后,要把自定义方法重新在框架源码中加上,比较麻烦。 方法二:通过 Macroable,在不修改框架源码的情况下,动态地把自定义方法加到 Collection 中。

本文主要介绍 Macroable 相关的知识。

Macroable 用法

Macroable 共有 5 个方法:macrominixhasMacro__callStatic__call ,其中后两个为魔术方法。接下来通过代码示例来了解这几个方法的用法

首先定义两个类:CarPlane,后面的示例都是以这两个类为基础的。 Car 类中引用了 Macroable 这个 trait,而 Plane 类中的 fly 方法,返回的是一个闭包。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php

class Car
{
    use Macroable;

    private $name = 'php';

    public function drive()
    {
        echo $this->name;
        echo " drive car\n";
    }
}

class Plane
{
    public function fly()
    {
        return function(){
            echo "plane can fly\n";
        };
    }
}

可在 Car 类中,通过 macro 方法给 Car 类动态添加方法,然后可将其作为方法、静态方法进行调用:

1
2
3
4
5
6
Car::macro('door', function(){
    echo "open car door\n";
});

Car::door();
$car->door();

假如,我想使用某个类中的所有方法,可先用 minix 方法进行批量添加:

1
2
3
4
Car::mixin(new Plane());

$car->fly();
$car::fly();

注意 Plane 类中方法,返回的是闭包,否则用 minix 导入后会无法调用该方法。

Macroable 源码解析

本文分析的代码为 Lavavel 5.7 版本,Macroable 源码地址: Macroable 源码

Macroable.php 文件代码 100 行出头,其中还包括不少注释,因此是比较容易理解的,其结构如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<?php

namespace Illuminate\Support\Traits;

trait Macroable
{
    protected static $macros = [];

    public static function macro($name, $macro){}

    public static function mixin($mixin){}

    public static function hasMacro($name){}

    public static function __callStatic($method, $parameters){}

    public function __call($method, $parameters){}
}

共 6 个方法,还有一个类静态变量,用来存储用户添加的自定义方法。

hasMacro

判断某个自定义方法是否存在

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/**
 * Checks if macro is registered.
 *
 * @param  string  $name
 * @return bool
 */
public static function hasMacro($name)
{
    return isset(static::$macros[$name]);
}

macro

添加一个自定义方法。其中 $macro 为对象或闭包。 如果传入的是对象,则该对象需实现 __invoke 魔术方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/**
 * Register a custom macro.
 *
 * @param  string $name
 * @param  object|callable  $macro
 *
 * @return void
 */
public static function macro($name, $macro)
{
    static::$macros[$name] = $macro;
}

minix

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
/**
 * Mix another object into the class.
 *
 * @param  object  $mixin
 * @return void
 *
 * @throws \ReflectionException
 */
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 的反射,首先获取 minix 对象中所有的 publicprotected 的方法。遍历各个方法,invoke 该方法,并把 invoke 的结果存入 macro 静态变量中。

上文提到过,被 mixin 的类中的方法,返回的必须是闭包,才能使用 mixin 方法被动态添加。 所以可能有人有这样的想法,把 $method->invoke($mixin) 改成 $method->getClosure($mixin),这样一来,被 mixin 的类中的方法就不用返回闭包,而是正常地写就行了。 实际上是不行的。因为通过 ReflectionMethod::getClosure() 获取的闭包,均不能进行重新绑定。

__callStatic

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
 * 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
        ));
    }

    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);
}

首先判断被调用的方法,是否已被添加到 macro 静态变量中。 如果被调用的方法是个闭包,则绑定 static 的类作用域,并调用该闭包。 否则,被调用的是对象(该对象需实现 __invoke 方法)。

__call

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
 * 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) {
        return call_user_func_array($macro->bindTo($this, static::class), $parameters);
    }

    return call_user_func_array($macro, $parameters);
}

代码逻辑同 __callStatic

参考资料

https://blog.csdn.net/pharaoh_shi/article/details/80984437

https://www.php.net/manual/zh/class.reflectionclass.php

https://learnku.com/laravel/t/2915/how-to-use-the-macro-method-to-extend-the-function-of-the-base-class-of-laravel

https://stackoverflow.com/questions/47165930/php-binding-method-to-another-class