前言
之所以写这篇博客,是因为写 http demo 的时候的时候,用到了 laravel
里边的 validation
组件,按照老版本的方式调用 $this->validate()
的时候,找不到这个方法了,所以就看了一下文档,发现 laravel >= 5.5
之后,调用方法转移到 Request
实例去了,即 framework/Request.php at 7.x · laravel/framework · GitHub。有一个细节是这个方法是用 phpdoc
的方式标注出来的, 也就是说这个方法不是直接放置在类中的,找了一下确实没有找到。 经过 grep
之后找到了 (framework/FoundationServiceProvider.php at 7.x · laravel/framework · GitHub), 发现这个是通过 macro
方式动态添加进去的, 那如何动态添加一个方法到类里边呢?
开工探索
First Try, 直接动态赋值给对象
作为一个动态语言,第一想法就是实例化的时候动态赋值进去。其实这样是不可以的,因为我们使用 $obj->echo = function () {}
的时候其实是给一个变量赋值了一个方法。
- 声明一个类
1
2
3
4
|
<?php
class FirstClass {
}
|
- 一个单元测试
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
|
<?php
class FirstClassTest extends TestCase
{
/**
* @var FirstClass
*/
private $obj;
protected function setUp(): void
{
$this->obj = new FirstClass();
$this->obj->hello = function () {
return '我是动态添加的';
};
}
protected function tearDown(): void
{
$this->obj = null;
}
public function testMethodNotFound()
{
$this->expectExceptionMessage('Call to undefined method');
$this->obj->hello();
}
public function testCallMethod()
{
$func = $this->obj->hello;
$this->assertSame(true, is_callable($func));
$this->assertSame('我是动态添加的', ($this->obj->hello)());
}
}
|
从单元测试中可以看出来,$this->hello
其实就是一个 closure
, 并不是一个 method
。
Second Try 使用 __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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
<?php
class SecondClass
{
protected static $macros = [];
protected $current;
public function __construct($current = null)
{
$this->current = $current ?? time();
}
/**
* @param string $name
* @param callable $callable
* @throws Exception
*/
public static function macro(string $name, $callable)
{
static::$macros[$name] = $callable;
}
public function __call($name, $arguments)
{
if (!isset(static::$macros[$name])) {
throw new Exception("method $name not found");
}
/** @var Closure $macro */
$macro = static::$macros[$name];
if ($macro instanceof Closure) {
return call_user_func($macro, ...$arguments);
}
return $macro(...$arguments);
}
}
|
- 来一个单元测试
测试一下是否可行
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
|
<?php
class SendClassTest extends TestCase
{
public function testCallEcho()
{
SecondClass::macro('echo', function () {
return 'hello world!';
});
$obj = new SecondClass();
$this->assertSame('hello world!', $obj->echo());
}
public function testCallEchoWithArgs()
{
SecondClass::macro('echo', function (...$args) {
$arg = implode("|", $args);
return "hello world!\nargs: {$arg}";
});
$args = [];
foreach (range(0, 100) as $y) {
$args[] = rand(0, 100);
}
$obj = new SecondClass();
$this->assertSame(
"hello world!\nargs: " . implode("|", $args),
$obj->echo(...$args)
);
}
}
|
似乎成功了是不是?如果不是所有 $this
的话,确实成功了, 我们来看下边这个测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
<?php
class SendClassTest extends TestCase
{
/**
* @throws \Exception
*/
public function testCallEchoAndUseThisInFunc()
{
SecondClass::macro('echo', function () {
$current = date("Y-m-d H:i:s", $this->current);
return "hello world! now: $current";
});
$obj = new SecondClass();
$this->expectException(Notice::class);
// 注意,这里报错的类是本测试类, SendClassTest, 也是就 this 绑定异常 了
$this->expectExceptionMessage('Undefined property: Minbaby\ExtraDemo\Blog\Test\BindMethodToClass\SendClassTest::$current');
$obj->echo();
}
}
}
|
当我们在动态添加的函数中,试图获取成员属性的时候提示未定义,且 $this
是指向单元测试这个类的,而不是实例化的类。
一脸问号???
Third Try binding of $this
为啥和我们预期都不一样的,明明直接在类中写匿名函数的时候 $this
回绑定到实例上,动态附加上去的跟我们预期的不一样呢?
Automatic binding of $this
因为 php 偷偷帮你做了一个 $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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
<?php
class ThirdClass
{
protected static $macros = [];
protected $current;
public function __construct($current = null)
{
$this->current = $current ?? time();
}
/**
* @param string $name
* @param callable $callable
* @throws Exception
*/
public static function macro(string $name, $callable)
{
static::$macros[$name] = $callable;
}
public function __call($name, $arguments)
{
if (!isset(static::$macros[$name])) {
throw new Exception("method $name not found");
}
/** @var Closure $macro */
$macro = static::$macros[$name];
if ($macro instanceof Closure) {
$macro = Closure::bind($macro, $this, static::class);
return call_user_func($macro, ...$arguments);
}
return $macro(...$arguments);
}
}
|
测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
<?php
class ThirdClassTest extends TestCase
{
/**
* @throws \Exception
*/
public function testCallEchoAndUseThisInFunc()
{
ThirdClass::macro('echo', function () {
$current = date("Y-m-d H:i:s", $this->current);
return "hello world!now: {$current}";
});
$current = time();
$obj = new ThirdClass($current);
$current = date("Y-m-d H:i:s", $current);
$this->assertSame("hello world!now: $current", $obj->echo());
}
}
|
后记
其实我不是太喜欢这种"黑魔法”, 核心问题就是会把类搞得了乱七八杂,导致找一个方法的时候很麻烦。
参考