7

其实这篇文章更应该针对python, 因为python默认情况下json序列化一个类对象时,是要报错的。

但是我觉得php的码农更多吧,而且主要是想传达一种思想,思想无国界哈。

那就拿PHP举粟,我们先来看看

1. json_encode对类的处理

先来看段测试代码:

class A {
    public $a;
    public $b;
    private $c;
    static public $d;
    static protected $e;
    
    public function __construct(){
        $this->a = 1;
        $this->b = 2;
        $this->c = 3;
        self::$d = 4;
        self::$e = 5;
    }
}
$obj = new A;
echo json_encode($obj);

输出结果为:

>> {"a":1,"b":2}

get_object_vars() 装饰下输出代码:

echo json_encode( get_object_vars($obj) );

输出结果还是:

>> {"a":1,"b":2}

综上我们差不多可以作个结论:
json_encode()序列化一个对象时,会先提取(get_object_vars)对象的公有(public)属性合并为一个数组,再进行序列化。private、protected、static属性以及类方法都将被丢弃。

1.1 不改变属性声明如何自定义JSON序列化的字段?

好,实操中真正碰到的问题来了。

class A {
    public $a;
    public $b;
    public function __construct($a, $b){
        $this->a = $a;
        $this->b = $b;
    }
    public function sum(){
        return $this->a + $this->b;
    }
}
$obj = new A(1,2);

我想json_encode得到一个sum字段,如:
{"a":1, "b":2, "sum":3}

怎么实现呢?
童鞋们还得记PHP的类有个魔法方法 __toString(), 该方法是当尝试将对象转化为字符串时,就会调用该函数,从而得到期望的字符串。

class B{
    public function __toString(){
        return "我只是个大B";
    }
}

echo(new B); // such as print(), strval(), ...
>> 我只是个大B

如果你是个Python coder, 这就不足为奇了,因为python的魔法方法真的不要花样太多。

好吧,那么有没有一个神奇的方法如 __json() 让我们来实现自定义JSON序列化呢?

暂时没有,不过PHP作者们早接到了这种需求,并做出了解决方案,请看:

1.2 接口JsonSerializable::jsonSerialize(),了解一下!

系统支持:(PHP 5 >= 5.4.0, PHP 7)
怎么用呢?看代码:

// 1. 类先要实现JsonSerializable接口
class A implements JsonSerializable {
    public $a;
    public $b;
    public function __construct($a, $b){
        $this->a = $a;
        $this->b = $b;
    }
    public function sum(){
        return $this->a + $this->b;
    }
    
    // 2. 实现jsonSerialize() 方法
    public function jsonSerialize(){
        // 定义我们需要的字段
        return array(
            "a"=>$this->a,
            "b"=>$this->b,
            "sum"=>$this->sum()
        );
    }
}

$obj = new A(1,2);
echo json_encode($obj);

这下满意了吧,看:

>> {"a":1,"b":2,"sum":3}

当类实现了jsonSerialize()时,json_encode(object)将使用jsonSerialize()取代get_object_vars()得到需要序列化的字段信息。

1.3 DateTime等系统对象如何处理

由于这些类是系统定义的,显示是无法添加上边说的接口,再定义一个子类做兼容也是个工作量不小的问题。

echo json_encode(date_create());
>> {"date":"2019-12-17 07:49:24.086271", "timezone_type":3, "timezone":"UTC"}

然而,因业务需求,所有接口里返回的时间对象,我只想要统一为:
YYYY-MM-DD HH:MM:SS 这种格式字符串

因此,建议大家在框架应用时

2. 自定义自己的json_encode方法

json_encode()显然是无法重写的了,那我就自己定义一个方法

2.1 my_json_encode()

然后整个项目查找json_encode替换为my_json_encode即可快速解决。

定义一个完全兼容json_encode()参数的方法

function my_json_encode($value, $options=0, $depth=512){
    $value = my_json_handle($value);
    return json_encode($value, $options, $depth);
}

代码里定义一个个性化的序列化处理方法:

2.2 my_json_handle($value)

在这里,你可以定义任何数据类型如何JSON序列化。

function my_json_handle($mixed, $depth=512, $recursion=1){
    // recursion limited.
    if($recursion >= $depth){ 
        return print_r($mixed, true);
    }

    // 这是一个类对象数据
    if(is_object($mixed)){
    
        // 假设遇到DateTime数据, 输出为我们想要的
        if($mixed instanceof DateTime || $mixed instanceof DateTimeImmutable){
            return date_format($mixed, 'Y-m-d H:i:s');
        }
        // 你可以在这里自定义更多系统预设类的处理...
        
        // 优先兼容实现了jsonSerialize()方法的类.
        if($mixed instanceof JsonSerializable){
            $mixed = $mixed->jsonSerialize();
        }
        elseif(method_exists($mixed, "__json")){
            // 添加 __json() 魔法方法支持.
            // 个人感觉 __json()比jsonSerialize()更科学且好记
            // 类不需要再 implements JsonSerializable
            $mixed = $mixed->__json();
        }
        // 默认get_object_vars()提取属性字段
        if(is_object($mixed)){
            $mixed = get_object_vars($mixed);
        }
        // 上边的if判断里没有像DateTime那样直接return $mixed
        // 是因为可能得到是个可迭代的数据, 我们需要再进行迭代处理
    }
    // 可迭代的嵌套数据
    if(is_array($mixed)){
        foreach($mixed as $k=>$v){
            // 当前迭代层级+1
            $mixed[$k] = my_json_handle($v, $depth, $recursion+1);
        }
    }
    return $mixed;
}

2.3 测试下我们的成果

// 不需要再 implements JsonSerializable
class A {
    public $a;
    public $b;
    public $datetime;
    public function __construct($a, $b){
        $this->a = $a;
        $this->b = $b;
        $this->datetime = date_create("now",  new DateTimeZone('+0800'));
    }
    public function sum(){
        return $this->a + $this->b;
    }
    
    // 只需实现 __json() 方法
    public function __json(){
        // 定义我们需要的字段
        return array(
            "datetime"=>$this->datetime,
            "timestamp"=>$this->datetime->getTimestamp(),
            "sum"=>$this->sum()
        );
    }
}

echo my_json_encode(new A(1,2));
>> {"datetime":"2019-12-17 16:34:15", "timestamp":1576571655, "sum":3}

当你在项目中=使用ORM框架,并定义了大量的Model,在json序列化这些Model时,你会发现这篇文章还是有很实用的。
能规范你的接口输出字段,ajax接口不会经常多字段或少字段,同一个字段一会是字符串一会又是整型的各种尴尬。

后记

PHP中的标量(Scalar Data), 目前只发现float浮点数据在json_encode时可能出现溢出,PHP7可通过配置
serialize_precision = -1
解决。

另外浏览器端可能不支持长整型,有些订单ID位数比较长的时候(大于32bit整数),浏览器端JSON.parse()的时候就容易被截短数据,后台处理时可以把它转化为字符串。


idollo
1.4k 声望15 粉丝