在 Laravel 中,Collection 类本身实现了不少方法,比如 sum、groupBy 等,若想给这个类加上一些自定义的方法,有如下方案:
方法一:通过修改 Collection 类的源码,在其中加入自定义方法的代码。但是这样就要修改框架源码,并且升级框架源码后,要把自定义方法重新在框架源码中加上,比较麻烦。
方法二:通过 Macroable,在不修改框架源码的情况下,动态地把自定义方法加到 Collection 中。
本文主要介绍 Macroable 相关的知识。
Macroable 用法
Macroable 共有 5 个方法:macro、minix、hasMacro、__callStatic、__call ,其中后两个为魔术方法。接下来通过代码示例来了解这几个方法的用法
首先定义两个类:Car、Plane,后面的示例都是以这两个类为基础的。
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 对象中所有的 public、protected 的方法。遍历各个方法,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