0%

详解 Laravel 中的 macrotrait

##“macro” 缘起
“macro” 也就是宏的概念,在计算机科学和编程世界中有很长的历史了,也就意味着在不同的情境下有不同的含义。
可能大部分人第一次接触宏是在 basic/vb-script 中,那时候宏就是一个没有参数的函数。 C 语言也有宏,在程序编译前会进行宏名的字符替换。
甚至 MicroSoft 的 Office 也有宏,为了避免一再地重复相同的动作,就会把常用的动作写成宏,自动化的完成某项任务。

虽然每个宏的定义不同,但是他们具有一些共有的特性

都可以执行某个特定的任务,但是对整个编程环境只有有限的访问权。

接下来我们来看看 laravel 中的 marcro。

##microable 在 laravel 中的含义
首先我们举个栗子,为了方便测试,添加如下代码到 app/routes.php

1
2
3
4
5
6
7
#File: app/routes.php
class Hello{}

Route::get('test', functioni(){
$hello = new Hello;
$hello->sayHi();
});

如果我们在浏览器中访问 test 这个路由,显然会报错

call to undefined method Hello:sayHi();

原因是我们没有在 Hello 这个类中定义 sayHi 的方法。

这次我们把 hello 类简单的改造一下

1
2
3
4
5
6
#File: app/routes.php
class Hello
{
//引入 illuminate support 中的 MacroableTrait
use Illuminate\Support\Traits\MacroableTrait;
}

注意这里的 macroableTrait 用了完整的命名空间,使用 trait 也会触发 php 的 autoloader.

重新测试,依然报错

Call to undefined method Hello::sayHi()

现在试试加入如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
#File: app/routes.php
class Hello
{
use Illuminate\Support\Traits\MacroableTrait;
}
Route::get('test', function(){
$hello = new Hello;
Hello::macro('sayHi', function(){
echo "hello", "\n<br>\n";
});
$hello->sayHi();
Hello::sayHi();
});

注意到这里的静态方法 macro 是从 Illuminate\Support\Traits\MacroableTrait 中继承而来

刷新页面,我们可以看到如下输出

Hello

Hello

以上就是 MacroableTrait 的一个简单使用,当我们调用 macro 方法时,我们实际上给 Hello 类以及所有 Hello 类实例化的对象添加了一个 sayHi 的方法。

在 laravel 中的比较常见的使用方式是在一些带有 bootstrap 性质的引导脚本中,通过预先注册一些宏,使得开发者可以在其他地方调用已注册的宏。
比如 Formresponse

MacroableTrait 原理

MacroableTrait 的代码其实很简单,我们可以通过如下路径找到它
vendor/laravel/framework/src/Illuminate/Support/Traits/MacroableTrait.php

我们先来看看之前使用过的 macro 方法是如何定义

1
2
3
4
5
6
7
8
9
10
11
12
#File: vendor/laravel/framework/src/Illuminate/Support/Traits/MacroableTrait.php
/**
* Register a custom macro.
*
* @param string $name
* @param callable $macro
* @return void
*/
public static function macro($name, callable $macro)
{
static::$macros[$name] = $macro;
}

macro 方法接受两个参数,一个是 string 类型的 $name, 用于标识具体某个宏的名字,第二个参数是
一个 callable 的参数,意味着可以是匿名函数,或者静态方法,又或者实例对应的某个方法,总之一切
可以 callable 的东西,以 key/value 的形式存储在一个静态变量 $macros 中。

往下看我们可以看到熟悉的 _call 以及 __callStaic 方法

当触发 __call 方法时

1
2
3
4
5
#File: vendor/laravel/framework/src/Illuminate/Support/Traits/MacroableTrait.php
public function __call($method, $parameters)
{
return static::__callStatic($method, $parameters);
}

会把对应的 $method 以及 $parameters 参数继续传递给 __callStatic 方法, 由它去统一接管.

__callStatic 方法

1
2
3
4
5
6
7
8
9
10
11
#File: vendor/laravel/framework/src/Illuminate/Support/Traits/MacroableTrait.php

public static function __callStatic($method, $parameters)
{
if (static::hasMacro($method))
{
return call_user_func_array(static::$macros[$method], $parameters);
}

throw new \BadMethodCallException("Method {$method} does not exist.");
}

会取出 $method 对应在 $macros 中注册的 callable 参数,通过 call_user_func_array 来真正的触发。

拿开头的例子来实例说明,首先当我们的 Hello 类调用 macro 方法时

1
2
3
4
5
6
7
8
#File: app/routes.php
Route::get('test', function(){
//...
Hello::macro('sayHi', function(){
echo "Hello","\n<br>\n";
});
//...
});

我们告诉 Hello 类在 static::$macros 中注册一个名为 sayHi , 值为匿名函数的一个宏。
相当于 static::$macros['sayHi'] = function(){echo "Hello", "\n<br>\n";}

接着当我们在调用 sayHi 方法 (通过静态和实例方法两种方式) 时,

1
2
3
4
5
6
7
#File: app/routes.php
Route::get('test', function(){
//...
$hello->sayHi();
Hello::sayHi();
//...
});

由于 sayHi 方法没有在 Hello 类中定义,php 会尝试去调用 __call(-> 调用时) 和 __callStatic(:: 静态调用时),
__call 将方法以及参数又显式的传递给了 __callStatic, 那么意味着

__callStatic 才是两种方式调用时真正执行的方法。

如果我们把参数的传入过程再具体化,如下图所示

1
2
3
4
5
6
7
8
9
10
#File: vendor/laravel/framework/src/Illuminate/Support/Traits/MacroableTrait.php
public static function __callStatic('sayHi', $parameters)
{
if (static::hasMacro('sayHi'))
{
return call_user_func_array(static::$macros['sayHi'], $parameters);
}

throw new \BadMethodCallException("Method {'sayHi'} does not exist.");
}

取出 sayHi 对应的匿名函数,调用 call_user_func_array 执行。

1
2
3
4
5
6
7
8
9
10
11
12
#File: vendor/laravel/framework/src/Illuminate/Support/Traits/MacroableTrait.php
public static function __callStatic('sayHi', $parameters)
{
if (static::hasMacro('sayHi'))
{
return call_user_func_array(function(){
echo "Hello","\n<br>\n";
}, $parameters);
}

throw new \BadMethodCallException("Method {'sayHi'} does not exist.");
}

以上就是全部的执行过程。

When to Use

尽管 MacroableTrait 很强大, 但并不意味着所有的类都适合用它, macro 无法知道类或者对象中的其他属性和方法。
由于我们注册进 macros 中的是匿名函数 (或者 php 的 callback), 也就是说这些 function/callback 是无法访问 $this , self 或者 static 的。
因此,通过 MacroableTrait 注册的宏更像是一个个无状态的 helper 方法。如果你需要在对象或者类中获取相关的一些状态,那么不合适用
MacroableTrait

## 总结

通过利用 php 本身的魔术方法和静态的方式,MacroableTrait 提供了一种在运行时动态为对象创建方法的能力,这种方式也是模拟了在其他动态语言中的特色 (比如 python/ruby)

但是要注意 MacroableTrait 的使用场景,它并不会感知类的其他属性和方法。

Reference:

–EOF–

坚持原创分享,您的支持将鼓励我继续创作!