Laravel 中的 MicroableTrait

分享 lifesign ⋅ 于 3年前 ⋅ 最后回复由 秋风 3个月前 ⋅ 2821 阅读

“macro”缘起

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

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

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

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

microable 在 laravel 中的含义

首先我们举个栗子, 为了方便测试, 添加如下代码到 app/routes.php

#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 类简单的改造一下

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

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

重新测试,依然报错

Call to undefined method Hello::sayHi()

现在试试加入如下代码

#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 方法是如何定义

#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 方法时

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

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

__callStatic 方法

#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 方法时

#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 方法(通过静态和实例方法两种方式)时,

#File: app/routes.php
Route::get('test', function(){
    //...
    $hello->sayHi();
    Hello::sayHi();
    //...
});

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

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

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

#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 执行。

#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--

本文章首发在 Laravel China 社区
本帖已被设为精华帖!
回复数量: 4
  • duke-lee
    3年前

    很棒的文章. :+1:

  • monkey 最重要的事,永远只有一件
    3年前

    很专业的介绍,WHAT WHEN HOW 都描述出来了

  • gold3bear
    3年前

    niubility

  • 秋风
    10个月前

    file
    请问这个问题可能的原因是什么,做api开发路由,路由部分可以访问,部分访问的时候就会报这错!

暂无评论~~
您需要登陆以后才能留下评论!

Composer 中国全量镜像

Top 100 扩展包

Lumen 中文文档

Laravel 速查表

Laravel 中文文档

Laravel 项目开发规范

Laravel 开发环境部署

Elasticsearch-PHP 中文文档

Lumen 中文文档

GraphQL PHP 中文文档

社区文档撰写指南

TDD 构建 Laravel 论坛笔记

PHP PSR 标准规范

PHP 设计模式全集

Dingo API 中文文档