Laravel-Macroable

思考并回答以下问题:

  • Macro的中文是什么意思?
  • static延迟静态绑定是什么意思?Traits中使用static是什么意思?
  • Macroable Traits维护一个string=>obj/callable的数组并提供静态写入macro和查询hasMacro函数。怎么理解?
  • __call()函数可以直接调用吗?(new Test(‘construct_param’))->__call(‘method’, ‘parameter’);可以这样写吗?

简介

计算机科学里的宏(Macro),是一种批量处理的称谓。一般说来,宏是一种规则或模式,或称语法替换,用于说明某一特定输入(通常是字符串)如何根据预定义的规则转换成对应的输出(通常也是字符串)。这种替换在预编译时进行,称作宏展开。它能使日常工作变得更容易。

今天我们讲讲Laravel中的宏操作。

完整的源码

PHP的static延迟静态绑定功能

Illuminate\Support\Traits\Macroable.php

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
105
106
107
108
109
110
111
112
113
114
115
116
117
<?php

namespace Illuminate\Support\Traits;

use Closure;
use ReflectionClass;
use ReflectionMethod;
use BadMethodCallException;

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
* @param bool $replace
* @return void
*
* @throws \ReflectionException
*/
public static function mixin($mixin, $replace = true)
{
$methods = (new ReflectionClass($mixin))->getMethods(
ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_PROTECTED
);

foreach ($methods as $method) {
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) {
return call_user_func_array(Closure::bind($macro, null, static::class), $parameters);
}

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) {
return call_user_func_array($macro->bindTo($this, static::class), $parameters);
}

return $macro(...$parameters);
}
}

Macroable::macro方法

1
2
3
4
public static function macro($name, $macro)
{
static::$macros[$name] = $macro;
}

很简单的代码,根据参数的注释,$macro可以传一个闭包或者对象,之所以可以传对象,多亏了PHP中的魔术方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Father
{
// 通过增加魔术方法__invoke我们就可以把对象当做闭包来使用了。
public function __invoke()
{
echo __CLASS__;
}
}

class Child
{
use \Illuminate\Support\Traits\Macroable;
}

// 增加了宏指令之后,我们就能调用Child对象中不存在的方法了
Child::macro('show', new Father);
// 输出:Father
(new Child)->show();

Macroable::mixin方法

这个方法是把一个对象的方法的返回结果注入到原对象中。

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
public static function mixin($mixin, $replace = true)
{
$methods = (new ReflectionClass($mixin))->getMethods(
ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_PROTECTED
);

foreach ($methods as $method) {
if ($replace || ! static::hasMacro($method->name)) {
$method->setAccessible(true);
static::macro($method->name, $method->invoke($mixin));
}
}
}

// 实际使用
class Father
{
public function say()
{
return function () {
echo 'say';
};
}

public function show()
{
return function () {
echo 'show';
};
}

protected function eat()
{
return function () {
echo 'eat';
};
}
}

class Child
{
use \Illuminate\Support\Traits\Macroable;
}

// 批量绑定宏指令
Child::mixin(new Father);

$child = new Child;
// 输出:say
$child->say();
// 输出:show
$child->show();
// 输出:eat
$child->eat();

在上面的代码可以看出mixin可以将一个类的方法绑定到宏类中。需要注意的就是,方法必须是返回一个闭包类型。

Macroable::hasMacro方法

1
2
3
4
public static function hasMacro($name)
{
return isset(static::$macros[$name]);
}

这个方法就比较简单没什么复杂可言,就判断是否存在宏指令。通常是使用宏指令之前判断一下。

Macroable::__call和Macroable::__callStatic方法

正是由于这两个方法,我们才能进行宏操作,两个方法除了执行方式不同,代码大同小异。这里讲一下__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
public function __call($method, $parameters)
{
// 如果不存在这个宏指令,直接抛出异常
if (! static::hasMacro($method)) {
throw new BadMethodCallException("Method {$method} does not exist.");
}

// 得到存储的宏指令
$macro = static::$macros[$method];

// 闭包做一点点特殊的处理
if ($macro instanceof Closure) {
return call_user_func_array($macro->bindTo($this, static::class), $parameters);
}

// 不是闭包,比如对象的时候,直接通过这种方法运行,但是要确保对象有`__invoke`方法
return call_user_func_array($macro, $parameters);
}

class Child
{
use \Illuminate\Support\Traits\Macroable;

protected $name = 'father';
}

// 闭包的特殊处理,需要做的就是绑定$this, 如
Child::macro('show', function () {
echo $this->name;
});

// 输出:father
(new Child)->show();

在上面的操作中我们绑定宏时,在闭包中可以通过$this来调用Child的属性,是因为在__call方法中我们使用Closure::bindTo方法。

官网对Closure::bindTo的解释:复制当前闭包对象,绑定指定的$this对象和类作用域。

Laravel中对类增加宏指令

Laravel中很多类都使用了宏这个trait。

比如Illuminate\Filesystem\Filesystem::class,我们想为这个类增加一个方法,但不会动到里面的代码。

Illuminate\Filesystem\Filesystem.php

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

namespace Illuminate\Filesystem;

use ErrorException;
use FilesystemIterator;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Support\Traits\Macroable;
use Symfony\Component\Finder\Finder;

class Filesystem
{
use Macroable;

/**
* Determine if a file or directory exists.
*
* @param string $path
* @return bool
*/
public function exists($path)
{
return file_exists($path);
}

/**
* Determine if a file or directory is missing.
*
* @param string $path
* @return bool
*/
public function missing($path)
{
return ! $this->exists($path);
}
}

我们只需要到App\Providers\AppServiceProvider::register方法增加宏指令(你也可以专门新建一个服务提供者专门处理)。

App\Providers\AppServiceProvider.php

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
<?php

namespace App\Providers;

use Illuminate\Support\Facades\Schema;
use Illuminate\Support\ServiceProvider;
use Illuminate\Filesystem\Filesystem;

class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
//
}

/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
Filesystem::macro('readFile', function ($fileName, $useIncludePath = null, $context = null){
readfile($fileName, $useIncludePath, $context);
});
}
}

然后增加一条测试路由,测试我们新增加的方法。

1
2
3
4
5
<?php

Route::get('/', function (\Illuminate\Filesystem\Filesystem $filesystem){
$filesystem->readFile(__DIR__.'/../.env.example');
});

然后打开浏览器运行,你就会发现,我们的代码可以正常的运行了并输出结果了。

Macroable

Laravel提供的Macroable可以在不改变类结构的情况为其扩展功能,本文将教你从零开始构建一个Macroable。

Macroable的核心是基于匿名函数的绑定功能,先来回顾下匿名函数的绑定功能。

预备知识

PHP可通过匿名函数的绑定功能来扩展类或者实例的功能。

定义类

1
2
3
class Foo
{
}

定义匿名函数

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

使用bindTo为类的实例添加join功能

1
2
3
$foo = new Foo();
$bindFoo = $join->bindTo($foo, Foo::class);
$bindFoo('a', 'b', 'c'); // "a-b-c"

PHP7之后引入了call方法更高效的实现了该功能

1
2
$foo = new Foo();
$join->call($foo, 'a', 'b', 'c'); // "a-b-c"

对于本例而言,使用bind方法进行静态绑定更贴合实际场景

1
2
$bindClass = \Closure::bind($join, null, Foo::class);
$bindClass('a', 'b', 'c'); // "a-b-c"

如果还没看懂的话,可以参考我之前写的PHP核心特性-匿名函数。

通过匿名函数扩展类的功能

了解了匿名函数的绑定功能后,就可以对其进行简单的封装了。首先,定义一个数组用来保存要添加的功能列表

1
2
3
4
5
6
7
8
9
10
11
12
<?php

trait Macroable {
// 保存要扩展的功能
protected static $macros = [];

// 添加要扩展功能
public static function macro($name, $macro)
{
static::$macros[$name] = $macro;
}
}

macros属性保存了要添加的功能名及实现,在类中使用该Trait

1
2
3
4
class Foo 
{
use Macroable;
}

添加join功能

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

join功能及对应的实现已经保存到了macros数组中。接下来是调用join方法

1
Foo::join('a', 'b', 'c')

由于Foo中的join静态方法不存在,会自动将方法名和参数转发到__callStatic魔术方法中。因此,在魔术方法中手动调用绑定的匿名函数即可

1
2
3
4
5
6
7
8
9
10
11
public static function __callStatic($name, $parameters)
{
// 获取匿名函数
$macro = static::$macros[$name];

// 绑定到类
$bindClass = \Closure::bind($macro, null, static::class);

// 调用并返回调用结果
return $bindClass(...$parameters);
}

测试

1
echo Foo::join('a', 'b', 'c'); // a-b-c

动态扩展与静态扩展的实现原理完全一样

1
2
3
4
5
6
7
8
public function __call($name, $parameters) 
{
// 获取匿名函数
$macro = static::$macros[$name];

// 调用并返回调用结果
return $macro->call($this, ...$parameters);
}

测试

1
2
$foo = new Foo();
echo $foo->join('a', 'b', 'c'); // 'a-b-c'

通过对象实例来扩展类的功能

之前,我们通过匿名函数的方式扩展类的功能

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

现在,我们考虑如何通过对象的方式来实现同样的功能。首先,将匿名函数改造成类

1
2
3
4
5
6
7
final class Join
{
public function __invoke(...$string)
{
return implode('-', $string);
}
}

当以函数的方式调用该类时,就会激活__invoke 方法

1
2
$join = new Join();
$join('a', 'b', 'c'); // a-b-c

现在,将Join的实例添加到类中,实现同样的效果

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

只需要对原有的__callStatic 方法增加一层判断即可。如果是匿名函数则绑定该匿名函数并调用,如果是对象则以函数的方式调用对象,激活对象的__invoke 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public function __call($name, $parameters) 
{
$macro = static::$macros[$name];

if($macro instanceof Closure){
return $macro->call($this, ...$parameters);
}

return $macro(...$parameters);
}

public static function __callStatic($name, $parameters)
{
$macro = static::$macros[$name];

// 闭包
if($macro instanceof Closure){
$bindClass = \Closure::bind($macro, null, static::class);
return $bindClass(...$parameters);
}

// 对象实例,则激活该对象
return $macro(...$parameters);
}

测试

1
Foo::join('a', 'b', 'c');  // a-b-c

同时扩展多个方法

最后,Laravel的Macroable还实现了同时扩展多个方法。

原理其实很简单,将功能类似的方法定义在一个类中

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

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

每个方法都返回了匿名函数,我们只需要将每个匿名函数添加到$macros列表中即可,只需要用到PHP的反射功能即可实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static function mixin($mixin) 
{
// 通过反射获取对象的 ReflectionMethod 列表
$methods = (new \ReflectionClass($mixin))->getMethods(
\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED
);

// 遍历 ReflectionMethod 列表,依次保存到 $macros 中
foreach ($methods as $method) {
$method->setAccessible(true);
// 依次激活该对象的每个方法,每个方法返回的匿名函数刚好保存在 $macros 中
static::macro($method->name, $method->invoke($mixin));
}
}

测试

1
2
3
Foo::mixin(new Str());
Foo::join('a', 'b', 'c');
Foo::split('a-b-c');

当然,这个功能没多大作用,还不如直接用Trait来的直观方便。

利用Laravel Macroable特性优化多态参数传递的技巧分享

准备

如果我们现在需要设计这样一个接口:获取指定文章的所有评论Or获取指定视频的所有评论。

我们有三张表:
视频表:videos
文章表:posts
评论表:comments
评论表中有这两个字段:commentable_type、commentable_id 分别存储评论主体信息。
他们之间的模型关系为多态关联,就不再多解释了,哈哈。

当接收到这个需求的时候,你可能会困惑,主体不确定该怎么去设计呢。通过 commentable_type 判断是什么模型,然后再根据确定的类型和 commentable_id 获取到具体的对象吗?此时你脑中的代码是什么样子的呢,反正当时我的脑子里面是一堆乱糟糟的代码,哈哈。现在就来给大家介绍这种优雅的实现方式。

获取可评论对象

首先我们利用macro给Request定义一个commentable:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Request::macro(('commentable'), function(bool $required = false){
if (!request()->has('commentable_type')) {
return $required ? abort(422, '目标对象不存在') : null;
}

$model = request()->get('commentable_type');

$model = Relation::getMorphedModel($model) ?? $mode;

$commentable = call_user_func([$model, 'find'], request()->get('commentable_id'));

if (!$commentable){
return $required ? abort(422, '目标对象不存在') : null;
}

return $commentable;
});

可以看到,目标对象的转换就是通过commentable_type,commentable_id这两个参数来的,这段代码不是很难,大家研究一下就看懂哒。可以在服务提供者中定义。

控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class CommentController extends Controller
{
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request
*
* @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection
*/
public function index(Request $request)
{
return CommentResource::collection($request->commentable(true)->comments()->get());
}

}

大家会发现现在已经可以通过 $request->commentable(true)来获取可评论对象了,但是少了验证,但是这个验证该怎么写呢,现在我们来看一下。

验证规则

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
class Polymorphic extends Rule
{
/**
* @var string
*/
protected $name;

/**
* @var string
*/
protected $message;

/**
* Create a new rule instance.
*
* @param string $name
* @param string|null $message
*/
public function __construct(string name, string $message = null)
{
$this->name = $name;
$this->message = $message;
}

/**
* Determine if the validation rule passes.
*
* @param string $attribute
* @param mixed $value
*
* @return bool
*/
public function passes($attribute, $value)
{
$model = request()->get(\sprintf('%s_type'), $this->name);

$model = Relation::getMorphedModel($model) ?? $mode;

return \class_exists($model) && (bool) \call_user_func([$model, 'find'], \request()->get(\sprintf('%s_id'), $this->name)));
}

/**
* Get the validation error message.
*
* @return string
*/
public function message()
{
return $this->message ?: '未指定目标对象或目标不存在';
}

}

现在规则定义好了,我们来使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request
*
* @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection
*
* @throws \Illuminate\Validation\ValidationException
*/
public function index(Request $request)
{
$this->validate($request, [
'commentable_id' => ['required', new Polymorphic('commentable', '未指定评论对象或对象不存在')],
]);

return CommentResource::collection($request->commentable(true)->comments()->get());
}

宏 (Macros)

宏是一种挂接到框架并扩展某些附加功能而无需显式继承该类的方法。许多Illuminate组件都使用了 Macroable 特性,该特性允许您为类扩展更多功能。 如果我们深入研究代码并了解其特征,则可以看到静态属性 $macros。 该静态属性的作用是 全局访问。 如果将宏分配给 Collection 类,则所有 Collection 实例将能够访问同一宏。

使用方法
注册宏
通过调用静态方法 macro($name,$macro) 将宏绑定到类中。 该方法从字面上通过键 $name 将 $macro(关闭执行功能)绑定到 $macros 数组中。 用它可以扩展更多用法,例如,像文档中所述的 Collection 类。 请注意,这通常是在服务提供者中完成的。

macro 详解
我们还可以看到 __call 和 __callStatic 魔术方法。 只要在类上调用了不可访问的方法,PHP 就会自动调用这两个魔术方法。 我们想重写它以查看宏是否存在(static :: hasMacro($ macro),如果存在,则从数组中解析 macro 并调用它。 macro 通常是个可以被任何类调用的闭包。 请注意,如果愿意,可以通过调用 mixin 方法在另一个对象中建立宏。 您可能没有注意到,在 Closure 中宏调用 $this 将访问该类本身的当前实例。 这怎么可能? 我如何使用 $this 访问 Closure 中的对象的属性和方法? 好吧,PHP 的闭包中有一种称为 bindTo 的方法,而 Laravel 则利用了这种方法。 它将当前宏对象绑定到 Closure ,因此 $this 引用类本身,而不是 Closure。

冲突
如果使用 Macroable 特性的类具有 __call 方法,则将发生冲突。 两种方法都不能被调用,因此当我们使用 Macroable 时,我们应该将 Macroable 的 __call 方法重命名为其他名称,但是请确保在对象的__call 方法中调用你重命名的方法。 可以在 Cache\Repository 类中看到示例。 我们导入 trait,同时将 __call 重命名为 macrocrock。 然后在 Repository 的__call 中,如果类具有宏,请确保调用 macroCall 方法 ,否则默认调用 b 本地方法,这样既可解决冲突。

注意: 不要过度使用它。 如果您有很多宏,或者有一些非常复杂的逻辑,那么继承类并执行相应的逻辑要比用宏扩展服务提供者要好得多。

我比较喜欢使用一个宏来检查 Request 类,以确定是否选中了表单中的复选框:

1
2
3
Request::macro('checked', function ($attribute) {
return in_array($this->input($attribute, false), [true, 1, '1', 'on']);
});

0%