andreamk/jsonserialize

通过 JSON 改进对象序列化和反序列化的库


README

PHPStan PSR12 PHPUnit 5.4,5.6 PHPUnit 7, 8

此库结合了原生 PHP 序列化的功能和 JSON 的可移植性,特别是它允许使用 JSON 对象的 受保护和私有属性 进行编码。当在类中定义时,魔法方法 __sleep__serialize__wakeup__unserialize 与序列化时使用的方式相同。

使用此库序列化和反序列化的值将保留其类型,因此数组、关联数组和对象将保留其类型和类。

要求

PHP 5.4+

安装

通过 Composer

composer require andreamk/jsonserialize 

可以通过 composer 自动加载器或库自动加载器包含此库

require_once PATH . '/jsonserialize/src/Autoloader.php';
\Amk\JsonSerialize\Autoloader::register();

基本用法

use Amk\JsonSerialize\JsonSerialize;

$json = JsonSerialize::serialize($value);

$value = JsonSerialize::unserialize($json);
public static JsonSerialize::serialize(mixed $value, int $flags = 0, int $depth = 512): string|false

serialize 函数将任何值转换为 JSON,就像 json_encode 函数一样,不同之处在于 JSON 中将包含对象的私有和受保护属性以及对应类的引用。

public static JsonSerialize::unserialize(string $json, int $depth = 512, int $flags = 0): mixed

接受一个 JSON 编码的字符串并将其转换为 PHP 变量,就像 json_decode。如果引用对象的类未定义,则对象将作为 stdClass 实例化,所有属性变为公共。

注意:当对象反序列化时,它将以不调用构造函数的方式实例化,就像 PHP 的 unserialize 函数一样。如果正在反序列化的变量是一个对象,在成功重建对象后,PHP 将自动尝试调用 __wakeup() 方法(如果存在)。

高级用法

方法 __sleep

public __sleep(): array

Amk/JsonSerialize 序列化函数检查类是否有名为 __sleep() 的魔法函数。如果有,则在该序列化之前执行该函数。它可以清理对象,并应返回一个包含该对象所有应序列化的变量名称的数组。如果没有返回数组,则抛出异常。

__sleep() 的预期用途是提交挂起数据或执行类似的清理任务。此外,如果非常大的对象不需要完全保存,此函数也很有用。

方法 __wakeup

public __wakeup(): void

Amk/JsonSerialize 反序列化函数检查是否存在名为 __wakeup() 的魔法函数。如果存在,此函数可以重建对象可能拥有的任何资源。

__wakeup() 的预期用途是在序列化过程中可能丢失的数据库连接重新建立,并执行其他重新初始化任务。

方法 __serialize

public __serialize(): array

Amk/JsonSerialize 序列化函数检查类是否有名为 __serialize() 的魔法函数。如果有,则在该序列化之前执行该函数。它必须构建并返回一个表示对象序列化形式的关联数组。如果没有返回数组,则抛出异常。

注意:如果在同一对象中同时定义了 __serialize() 和 __sleep() 方法,则只有 __serialize() 方法会被调用。__sleep() 方法将被忽略。

方法 __unserialize

public __unserialize(array $data): void

Amk/JsonSerialize 的 __unserialize() 方法检查是否存在具有魔法名称 __unserialize() 的函数。如果存在,此函数将传递从 __serialize() 返回的恢复后的数组。然后,它可以从该数组恢复对象的属性,具体取决于适当的操作。

注意:如果在同一对象中同时定义了 __unserialize() 和 __wakeup() 方法,则只有 __unserialize() 方法会被调用。__wakeup() 方法将被忽略。

抽象 JsonSerializable 类

class MyClass extends \Amk\JsonSerialize\AbstractJsonSerializable {
}

$obj  = new MyClass();
$json = json_encode($obj);

通过扩展实现 JsonSerializable 接口的 AbstractJsonSerializable 类,可以使用 PHP 的正常 json_encode 函数,为扩展此类的对象获得使用 JsonSerialize::serialize 获得相同的结果。

标志

- JSON_SKIP_CLASS_NAME

在某些情况下,在 JSON 序列化对象时可以不暴露类。例如,如果我们想通过 AJAX 调用将对象的内 容发送到浏览器。在这些情况下,我们可以使用 JSON_SKIP_CLASS_NAME 标志,除了 json_encode 函数的正常标志外。

use Amk\JsonSerialize\JsonSerialize;

$json = JsonSerialize::serialize(
    $value,
    JSON_PRETTY_PRINT | JsonSerialize::JSON_SKIP_CLASS_NAME
);

- JSON_SKIP_MAGIC_METHODS

当此标志开启时,类的 __sleep 和 __serialize 方法将被忽略。

- JSON_SKIP_SANITIZE

默认情况下,如果 json_encode 由于无效字符而失败,则应用字符串清理。激活此标志将关闭清理。

方法 serializeToData

public static serializeToData(
    mixed $value, 
    $flags = 0
): mixed

在某些情况下,在将数据结构转换为 JSON 之前处理数据结构是有用的。使用 serializeToData 方法,您将获得传递给具有 serialize 方法的 json_encode 函数的值。

$data = JsonSerialize::serializeToData($obj, JsonSerialize::JSON_SKIP_CLASS_NAME);
$data['extraProp'] = true;
unset($data['prop']);
$json = json_encode($data);

方法 unserializeToObj

public static JsonSerialize::unserializeToObj(
    string $json, 
    object|string $obj, 
    int $depth = 512, 
    int $flags = 0
) : object

在某些情况下,在已实例化的对象中反序列化 JSON 数据可能很有用。例如,如果我们正在处理带有 JSON_SKIP_CLASS_NAME 标志的序列化 JSON。

在这种情况下,我们没有关于引用类的信息,因此使用正常的反序列化函数,结果将是一个关联数组。使用 unserializeToObj 方法,我们强制使用参数传递的对象。

$obj = new MyClass();
$json = JsonSerialize::serialize($obj , JsonSerialize::JSON_SKIP_CLASS_NAME);


$obj2 = new MyClass();
JsonSerialize::unserializeToObj($json, $obj2);

方法 unserializeWithMap

public static unserializeWithMap(
    string $json, 
    JsonUnserializeMap $map, 
    $depth = 512, 
    $flags = 0
): mixed

映射反序列化是一种非常强大的方法,可以将属性映射到更改反序列化中的类型。经典的用途是强制将通常为关联数组的对象进行对象反序列化。除了 nuti PHP 原生类型外,还有一些特殊类型,其中可以指定对象的类,以及为具有递归引用的对象引用另一个属性。

$map = new JsonUnserializeMap(
    [
        '' => 'object';
        'prop1/prop11' => 'bool',
        'prop2' => 'cl:MyClass'
    ]
);
$val = JsonSerialize::unserializeWithMap($json, $map);

有关更多信息,请参阅映射部分

反序列化映射

映射通过 JsonUnserializeMap 类定义。可以通过向构造函数传递项目数组来初始化它,并且可以使用 addPropremovePropresetMap 方法在以后操作项目列表。

映射对象由键(属性标识符)值(属性类型)对组成。请注意,映射不需要定义结构中所有属性的定义,但只需需要强制转换为特定类型的属性

$map = new JsonUnserializeMap(
    [
    'prop' => 'object',
    'prop/flag1' => 'int',
    'prop/flag2' => 'bool'
    ]
);

属性标识符

  • 空标识符对应于根元素
  • 属性级别由字符 / 分隔
  • 字符 * 是通配符,如果您想映射数组中的所有元素,则很有用

一些示例

[
    '' => type, // itendifies the root element
    'prop' => type, // identifies the level 1 property ($val->prop or $val['prop'])
    'v1/v2/v3' => type, // identifies the level 3 property ($val->v1->v2->v3 or $val['v1']['v2']['v3'])
    'v1/*' => type, // identifies all the properties that are children of v1,
    'v1/*/v3' => type, // identifies all v3 properties of the children of v1
]
  • 通配符属性但优先级低于特定属性,因此您可以定义所有属性的类型,但除少数属性外。在下一个示例中,元素的所有子属性都将为整数,而 flag 将为布尔值
[
    'element/*' => 'int',
    'element/flag' => 'bool',
]

属性类型

  • 类型是一个字符串,可以是 PHP 原生类型 boolbooleanfloatintintegerstringarrayobjectnull
  • 如果类型以字符 ? 开头,则它可以设置为可空的。这意味着如果 JSON 值为 null,则保持 null,否则取类型的值。
  • 使用特殊类型 cl: 后跟类标识符,定义类型是该类定义的对象。
  • 使用特殊类型 rf: 后跟属性标识符(不带通配符),定义了属性引用。这仅在属性已定义且是对象时才有意义。
$map = new JsonUnserializeMap(
    [
    '' => 'cl:' . MyClass::class, // root val is an istance of MyClass
    'items/*' => '?cl:' . MyItems::class, // all element of items are istances of MyItems or null
    'items/*/parent' => 'rf:', // all prop parent of all items ar a reference to root value
    'obj' => '?object' // obj can be null or an object
    ]
);

注意:在定义类类型 cl: 时,如果存在,也会执行 __wakeup 和 unserialize 方法。在定义引用 rf: 时,JSON 中此对象的子值将被忽略,并且由于类已经初始化,所以不会执行 __wakeup 或 __unserialie 方法。

工作原理

serialize 和 unserialize 方法在标准 JSON 上工作,可以被任何编写/读取 json 文件的函数读取。特别是如果值是标量值或数组,使用标准函数 json_encode 和 json_decode 不会有区别。

如果您使用这些函数与对象一起使用,除了公共和私有属性外,还会添加数据所属的类。

此代码

namespace Test;

class MyClass {
    public $publicProp = 'public';
    protected $protectedProp = 'protected';
    private $privateProp = 'private';
    private $arrayProp = [1,2,3];
}

$object = new MyClass();
$json = JsonSerialize::serialize($object, JSON_PRETTY_PRINT);

将生成此 JSON

{
    "CL_-=_-=": "Tests\\MyClass",
    "publicProp": "public",
    "protectedProp": "protected",
    "privateProp": "private",
    "arrayProp": [
        1,
        2,
        3
    ]
}

在反序列化 JSON 时,如果数据是数组类型,并且存在 AbstractJsonSerializeObjData::CLASS_KEY_FOR_JSON_SERIALIZE (CL_-=_-=) 键,则此数组将转换为对象。

如果类存在,则实例化的对象将属于定义的类;否则,它将是一个 stdClass 对象。

如果对象是 stdClass 类型,则数组的所有项都将成为公共属性;否则,将使用数组的值更新类中定义且存在于数组中的每个属性。

如果您正在处理随着时间的推移而更改的序列化类的项目,了解此功能的不同之处非常重要。

假设我们有一个 WordPress 插件,其中有一个 Settings 类来描述插件设置。随着时间的推移,该类的属性可能被添加或删除,这是很常见的。在这种情况下,我们可能有序列化属性不存在于类中,以及存在于类中但尚未序列化的属性。

反序列化通过丢弃 JSON 中未定义在类中的所有属性,并保留定义在类中但不存在于 JSON 中的属性的默认值来处理此问题,除非它是 stdClass 类,其中所有 JSON 值都会被分配。

因此,在这种情况下,我们拥有此 JSON

{
    "CL_-=_-=": "MyClass",
    "propA": "value A",
    "propB": "value B"
}

以及以下方式定义的类

class MyClass {
    public $propA = 'init A';
    public $propC = 'init C';
}

$obj = JsonSerialize::unserialize($json);
var_dump($obj);

结果将是

object(MyClass)#1 (2) {
  ["propA"]=>
  string(7) "value A"
  ["propC"]=>
  string(6) "init C"
}