Laravel中通过Macroable宏来扩展类的能力

思考并回答以下问题:

  • $new_closure = Closure::bind($closure, new ClassName(), ClassName::class);和$new_closure = $closure->bindTo(new ClassName(), ClassName::class);

PHP闭包中的绑定bindTo和bind

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
final class Closure {
// 用于禁止实例化
private function __construct() { }

// 复制一个闭包,绑定指定的$this对象和类作用域。这个方法是 Closure::bindTo() 的静态版本
static function bind ( Closure $closure , object $newthis [, mixed $newscope = 'static' ] ) : Closure

// 复制当前闭包对象,绑定指定的$this对象和类作用域。
public function bindTo ( object $newthis [, mixed $newscope = 'static' ] ) : Closure

// 当尝试以调用函数的方式调用一个对象时,__invoke() 方法会被自动调用。
public function __invoke ([ $... ] ) : mixed

// Temporarily binds the closure to newthis, and calls it with any given parameters.
// 一次性的,绑定$this,并可以传入参数,直接发起调用
// @since 7.0
function call ($newThis, ...$parameters) {}

// 将给定的函数转化为匿名函数,可传入函数名
public static function fromCallable (callable $callable) {}
}

fromCallable等价于

1
2
$reflexion = new ReflectionFunction('addDiscount');
$closure = $reflexion->getClosure();

bindTo和bind函数的功能相同,一个是静态调用,一个实例调用,请看下面的示例:

创建一个匿名函数

1
2
3
4
5
$clo = function ($name) {
echo $name;
};

var_dump($clo);

结果为:

1
2
3
4
5
6
7
object(Closure)#1 (1) {
["parameter"]=>
array(1) {
["$name"]=>
string(10) "<required>"
}
}

由此可见$clo为Closure类的实例对象。所以Closure类是不能使用通过构造函数来实例化,也没这个必要,所以构造函数被定义为私有的。

所以上面说的实例调用是指$clo->bindTo($newthis, $newscope)
所以上面说的静态调用是指Closure::bind($clo, $newthis, $newscope)都能够得到一个新的闭包。

那么,它的使用场景是什么?

有时候我们的在闭包内部想要访问某个类或者对象的属性和方法,但是它们是由访问修饰符控制的,也就存在作用于的问题。

并且想要使用$this来访问的话,必须是在对象的内部,鉴于这些问题,我们有必要来操作一下闭包,使其具有这些能力。

  • 1、具有“本类内部”同等效果的作用域
  • 2、将 $this 传递到闭包里面,绑定到具体的实例,可使用$this访问。

看例子:

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
class Test {
public static $name = "rao";
protected static $color = "red";
private static $height = "188";

public $age = 12;
protected $sex = 1;
private $weight = 100;

function a(){
$fun = function (){
var_dump(Test::$name);
var_dump(Test::$color);
var_dump(Test::$height);
var_dump($this->age);
var_dump($this->sex);
var_dump($this->weight);
};

var_dump($fun);
$fun();
}
}

(new Test())->a();

打印:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
object(Closure)#2 (1) {
["this"]=>
object(Test)#1 (3) {
["age"]=>
int(12)
["sex":protected]=>
int(1)
["weight":"Test":private]=>
int(100)
}
}
string(3) "rao"
string(3) "red"
string(3) "188"
int(12)
int(1)
int(100)

由于$fun是定义在类的内部,于是它已经具备了这两个能力,这叫自动绑定。如果不想被自动绑定$this,可以使用静态闭包

1
2
3
4
5
$fun = static function (){
var_dump(Test::$name);
var_dump(Test::$color);
var_dump(Test::$height);
};

但是”本类内部“的功能还在。

但是对于一个外来的闭包,是不具备这些能力的。

$fun2不在类里面定义。

1
2
3
4
5
6
7
8
$fun2 = function (){
var_dump(Test::$name); // 可以访问
var_dump(Test::$color); // 无法访问
var_dump(Test::$height); // 无法访问
var_dump($this->age); // 无法访问
var_dump($this->sex); // 无法访问
var_dump($this->weight); // 无法访问
};

好,我们来使用bind()方法。

1
2
3
4
5
$fun22 = Closure::bind($fun2, new Test(), Test::class);
// 或者 $fun22 = $fun2->bindTo(new Test(), Test::class);

var_dump($fun22);
$fun22();

打印:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
object(Closure)#3 (1) {
["this"]=>
object(Test)#2 (3) {
["age"]=>
int(12)
["sex":protected]=>
int(1)
["weight":"Test":private]=>
int(100)
}
}
string(3) "rao"
string(3) "red"
string(3) "188"
int(12)
int(1)
int(100)

如果只想绑定$this的话,PHP7.0以后可以直接使用call,绑定并调用。

1
$fun2->call(new Test());

和上面的结果一模一样,这就是bind和bindTo的作用。当然根据需要也可以只赋予某一种能力。
newthis
需要绑定到匿名函数的对象,或者NULL创建未绑定的闭包。

newscope
想要绑定给闭包的类作用域,或者‘static’表示不改变。如果传入一个对象,则使用这个对象的类型名。 类作用域用来决定在闭包中$this对象的 私有、保护方法的可见性。

这种操作一般用在框架里面,来扩展类或者实例的功能。

比如Laravel框架中的Illuminate\Support\Traits\Macroable宏。
Laravel提供的Macroable可以在不改变类结构的情况为其扩展功能

Macroable

我们以Illuminate\Support\Traits\Macroable为例分析。

通过trait可以很方便的在任何类中使用。

Laravel提供的Macroable可以在不改变类结构的情况下为其扩展功能。也就是为一个类动态注入一些方法,并且和该类本身的方法拥有同样的作用域和调用方式。

Macroable的核心是基于匿名函数的绑定功能

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
trait Macroable
{
/**
* The registered string macros.
*
* @var array
*/
protected static $macros = [];

/**
* Register a custom macro.
*
* @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
* @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));
}
}

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

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

/**
* 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和__call里面,如果一个类想要借助Macroable来提升能力,基本操作是:

1、use Macroable;

2、调用macro添加一个方法到$macros

macro可以添加匿名函数和对象,之所能通过调用匿名函数的方式调用对象,前提是该对象要实现__invoke()方法,也就是说,调用这个对象的入口是__invoke() 方法。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

class Foo
{
use Macroable;
}

final class Join
{
public function __invoke(...$string)
{
return implode('-', $string);
}
}

Foo::macro('join', new Join());

或者

1
2
3
Foo::macro('join', function(...$string){
return implode('-', $string);
});

3、或者调用mixin将一个对象包含的全部方法都注册到当前类中。
使用了反射类获取到public和protected的方法,并且将protected方法设置为可访问,最后调用了这些方法,将返回值注入到 $macros;我们知道注册进去的都是可以被当作闭包调用的,因此我们反射的这个对象里面的方法的返回值应该是闭包,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
final class Str
{
public function join()
{
// 返回匿名函数
return function(...$string){
return implode('-', $string);
};
}

public function split()
{
// 返回匿名函数
return function(string $string){
return explode('-', $string);
};
}
}

// 执行注入
mixin(new Str())

为了更便捷的扩展一个类的功能,macro,mixin,hasMacro三个方法都被设计为静态调用,可以通过类直接调用来扩展方法。

0%