深入浅出 Laravel Macroable

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

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

预备知识

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

定义类

class Foo
{
}

定义匿名函数

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

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

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

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

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

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

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

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

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

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

<?php

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

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

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

class Foo 
{
    use Macroable;
}

添加 join 功能

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

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

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

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

public static function __callStatic($name, $parameters)
{   
    // 获取匿名函数
    $macro = static::$macros[$name];

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

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

测试

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

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

public function __call($name, $parameters) 
{   
    // 获取匿名函数
    $macro = static::$macros[$name];

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

测试

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

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

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

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

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

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

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

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

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

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

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

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

测试

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

同时扩展多个方法

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

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

final class Str
{   
    public function join()
    {   
        // 返回匿名函数
        return function(...$string){
            return implode('-', $string);
        };
    }

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

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

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

测试

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

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

本作品采用《CC 协议》,转载必须注明作者和本文链接
本帖由系统于 2年前 自动加精
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
讨论数量: 1

这个功能更多的应该是应用在框架底层吧,比如我一个类都定义好了,但是很多用这个类的地方会需要一些自定义的方法,这时候,只需要在底层类中 引入 Macroable trait ,使用者就可以通过这个 Macroable 扩展类的功能

1年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!