xiaohuilam/laravel

13. Macroable 解析

Opened this issue · 0 comments

还记得那个《高速修车》的陈年老梗吗:

公司的业务呢,就像跑在高速路上的车,车不能停,但是新需求和修bug也不能停。

这里要跟大家扒代码的 Macroable 呢,有点像上面这个悖论的出路;但其实也不是,因为他没有实现 且不停,只实现了 且不停。请原谅我这尴尬的幽默。


从陈年教程入手

网上很多教程都是提及了,Laravel 绝大部分类,都可以扩展方法。比如这个例子:

use Illuminate\Support\Collection;

Collection::macro('bcsum', function () {
    $sum = 0;
    foreach ($this as $item) {
        $sum = bcadd($sum, $item, 2);
    }
    return $sum;
});

dump((new Collection([1,2,3,4]))->bcsum()); // it says "10.00"

我们可以看到,在不改变代码的前提下,我们在使用的过程中给 Illuminate\Support\Collection 扩充了 bcsum 的功能。就如同在其中加入了如下代码:

<?php

class Collection ...
{
    public function bcsum()
    {
        $sum = 0;
        foreach ($this as $item) {
            $sum = bcadd($sum, $item, 2);
        }
        return $sum;
    }
}

代码解析

class Collection implements ArrayAccess, Arrayable, Countable, IteratorAggregate, Jsonable, JsonSerializable
{
use Macroable;

我们看到,Illuminate\Support\Collection 使用了 Illuminate\Support\Traits\Macroable 这个 trait

macro 的代码

作用是将闭包存进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;
}

魔术方法

在调用我们的方法时,按名字匹配出来,参数传入触发。

/**
* 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
));
}
if (static::$macros[$method] instanceof Closure) {
return call_user_func_array(Closure::bind(static::$macros[$method], null, static::class), $parameters);
}
return call_user_func_array(static::$macros[$method], $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 call_user_func_array($macro, $parameters);
}

这里之所以定义了两个方法,__call__callStatic,是因为 laravel 很多类,部分可以静态调用,部分可以动态调用。macroable 如果要能被这两种类可用,那么就需要定义两个魔术方法。


经验分享

失灵的情况

一. 魔术一个魔术方法,不可行

我们现在知道了 Macroable 的神奇,但是其实Macroable 也有失灵的时候。举个例子:

<?php
class JoeDoe
{
    use \Illuminate\Support\Traits\Macroable;
}

JoeDoe::macro('__toString', function () {
    return '123';
});

echo new JoeDoe();

而运行时,其还是报了这个错:

PHP Recoverable fatal error:  Object of class JoeDoe could not be converted to string

但是如果我们在 JoeDoe 这个类中手工硬编码 __toString 这个方法却是可以运行的的;
为什么?

其实道理很简单,因为 echo JoeDoe 对象时候,php 内核检查其中有没有 __toString 这个方法时,就已经报错了,根本还来不及走到 __call 这个魔术方法。