xiaohuilam / laravel

Laravel 深入详解 —— 源代码解析,新手进阶指南
433 stars 80 forks source link

13. Macroable 解析 #13

Open xiaohuilam opened 5 years ago

xiaohuilam commented 5 years ago

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

公司的业务呢,就像跑在高速路上的车,车不能停,但是新需求和修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;
    }
}

代码解析

https://github.com/xiaohuilam/laravel/blob/d081c918b7e582ec5b3f94316f44834466cec37d/vendor/laravel/framework/src/Illuminate/Support/Collection.php#L42-L44

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

macro 的代码

作用是将闭包存进static::$macros https://github.com/xiaohuilam/laravel/blob/d081c918b7e582ec5b3f94316f44834466cec37d/vendor/laravel/framework/src/Illuminate/Support/Traits/Macroable.php#L19-L30

魔术方法

在调用我们的方法时,按名字匹配出来,参数传入触发。 https://github.com/xiaohuilam/laravel/blob/d081c918b7e582ec5b3f94316f44834466cec37d/vendor/laravel/framework/src/Illuminate/Support/Traits/Macroable.php#L64-L112

这里之所以定义了两个方法,__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 这个魔术方法。