ddn/jsonobject

JsonObject辅助类,简化PHP中处理JSON对象的过程

0.2.0 2023-07-25 08:51 UTC

This package is auto-updated.

Last update: 2024-08-25 11:23:30 UTC


README

JsonObject 是一个PHP类,用于简化从JSON定义中使用对象的过程。这个想法来自于使用Python中的 pydantic,以及它将解析和验证JSON数据到对象的能力。

为什么使用 JsonObject

我需要使用PHP的API,并且该API返回JSONObjects。因此,我需要将它们解析成PHP对象,以便在应用程序中使用。

工作流程如下

  1. 检索JSON对象定义
  2. 使用 JsonObject 解析JSON定义
  3. 在应用程序中使用生成的对象

用例

让我们看一下以下JSON示例

{
    "id": 0,
    "name": "John Doe",
    "age": 42,
    "emails": [
        "my@email.com",
        "other@email.com"
    ],
    "address": {
        "street": "My street",
        "number": 42,
        "city": "My city",
        "country": "My country"
    }
}

使用 JsonObject,我能够使用以下类定义我的数据模型

class User extends JsonObject {
    const ATTRIBUTES = [
        'id' => 'int',
        'name' => 'str',
        'age' => 'int',
        'emails' => 'list[str]',
        'address?' => 'Address',
    ];
}

class Address extends JsonObject {
    const ATTRIBUTES = [
        'street' => 'str',
        'number' => 'int',
        'city' => 'str',
        'country' => 'str',
    ];
}

然后添加以下命令

$user = User::fromObject(json_decode($json_text_definition));

JsonObject 类将执行将内容解析为对象的操作,我们将能够使用其定义的属性

echo($user->name);

定义的类也可以有方法,这将使得实现应用程序的数据模型更加容易。例如,可以像这样定义类 User

class User extends JsonObject {
    const ATTRIBUTES = [
        'id' => 'int',
        'name' => 'str',
        'age' => 'int',
        'emails' => 'list[str]',
        'address?' => 'Address',
    ];
    public function isAdult() {
        return $this->age >= 18;
    }
}

使用 JsonObject

JsonObject 类的想法是使用它将JSON数据解析为对象,以便这些对象可以包含其他方法,有助于实现应用程序的数据模型。

当解析JSON对象(或数组)时,其内容将根据在 ATTRIBUTES 常量中定义的类型进行递归解析。如果数据无效,因为它不包含预期的值,将抛出异常。

要使用JsonObject,必须从 JsonObject 继承,并为该类定义 ATTRIBUTES 常量,以定义该类期望的对象的属性以及每个属性的类型。

定义属性的类型

ATTRIBUTES 常量是一个关联数组,键是每个属性的 名称,值是每个属性的 类型

可能类型包括:

  • int:整型数字
  • float:浮点数
  • str:字符串
  • bool:布尔值
  • list[type]:类型为 type 的对象列表。
  • dict[type]:类型为 type 的对象字典。字典条目的键被转换为字符串。
  • object:是一个类名,它必须是 JsonObject 的子类。

可选属性和必选属性

在定义属性名称时,可以在名称末尾添加一个 ? 以指示该属性是可选的。例如,用例部分中的属性名称 address? 是可选的。

每个字段都认为是必选的,这意味着它必须在解析的对象(或数组)中存在。此外,对象必须属于定义的类型(即它必须被特定类型正确解析)。

必选属性

任何非可选属性都认为是必选的。这有两个特别需要注意的点

  1. 从外部结构创建对象时(无论是使用 fromArrayfromObject 函数)。
  2. 生成 对象数组 表示形式的 JsonObject

当从外部结构创建对象时,JsonObject将处理所有必填字段。如果其中任何一个字段缺失,将引发异常。

在下一个示例中,将引发异常,因为必填字段年龄未提供。

class User extends JsonObject {
    const ATTRIBUTES = [
        "name" => "str",
        "age" => "int",
    ];
}
(...)
$user = User::fromArray([ "name" => "John" ]);

当将对象转换为数组或对象(或获取其json表示形式)时,即使未设置,必填字段也会获得默认值。

因此,在下一个示例中

class User extends JsonObject {
    const ATTRIBUTES = [
        "name" => "str",
        "age" => "int",
        "birthDate?" => "str"
    ];
}
$user = new User();
echo((string)$user);

输出将如下

{
    "name": "",
    "age": 0
}

因为虽然属性姓名年龄是必填的,并且它们获得了它们的默认值(即数字为0,字符串为空),属性出生日期不是必填的,并且尚未设置。所以它不会在输出中生成。

将必填属性设置为null

将值设置为null的问题在考虑一个属性是否可选时具有特殊的相关性。

有人可能认为,如果我们将值设置为null,这意味着要取消设置该值,因此它应该只适用于可选值,而不适用于必填值。

JsonObject中,我们有一个不同的概念,因为将属性设置为null将意味着“将值设置为null”,而不是取消属性。为了取消属性,我们应该使用unset函数或类似函数。

取消必填属性

JsonObject也允许取消值。对于可选属性,这意味着删除值,因此它在一个数组表示形式或对象中(如果检索值,它将被设置为null)将不会有任何值。

但对于必填属性,取消它意味着将其值重置为默认值。这意味着它将被初始化为该类型的默认值(即数字为0,列表、字符串或字典为空等),或者其默认值在ATTRIBUTES常量中。

继承

JsonObject也能从其父类继承属性。以下是一个示例

class Vehicle extends JsonObject {
    const ATTRIBUTES = [
        "brand" => "str",
        "color" => "str"
    ]
}
class Car extends Vehicle {
    const ATTRIBUTES = [
        "wheels" => "int"
    ]
}
class Boat extends Vehicle {
    const ATTRIBUTES = [
        "length" => "float"
    ]
}

在这个示例中,类Vehicle将只有属性品牌颜色,但类Car将会有品牌颜色轮子属性,而类Boat将会有品牌颜色长度属性。

对象的创建

可以从JsonObject的子类创建对象,使用静态方法::fromArray::fromObject,从一个解析的json对象开始。

在先前的示例中,如果我们有一个名为car.json的文件,其内容如下

{
    "brand": "BMW",
    "color": "black"
}

我们可以使用以下代码获取Vehicle类的实例

$json = file_get_contents("car.json");
$vehicle = Vehicle::fromArray((array)json_decode($json, true));

另一种方法是像下面示例那样实例化对象

* PHP 8及以上

$car = new Car(brand: "BMW", color: "black", wheels: 4);

*之前的PHP版本

$car = new Car([ "brand" => "BMW", "color" => "black", "wheels" => 4]);

对象的方法

JsonObject

JsonObject是此库的核心类。它有以下方法:

  • __construct($data) - 从给定的数据创建一个新对象
  • __get($name) - 返回具有给定名称的属性的值
  • __set($name, $value) - 设置具有给定名称的属性的值
  • __isset($name) - 如果具有给定名称的属性已设置,则返回true
  • __unset($name) - 取消可选属性的值(或重置必填属性的值)。
  • toArray() - 返回包含对象数据的关联数组。数组递归创建,访问每个属性的每个子属性。
  • toObject() - 返回包含对象数据的对象,作为属性。数组递归创建,访问每个属性的每个子属性。
  • toJson() - 返回一个表示对象的标准对象的JSON字符串。
  • ::fromArray($data) - 通过解析给定的关联数组到类中定义的属性来创建一个对象。每个属性都按其定义的类型递归解析。
  • ::fromObject($data) - 通过解析给定的对象到类中定义的属性来创建一个对象。每个属性都按其定义的类型递归解析。

JsonDict

该对象用于处理来自JSON定义的字典。JsonDict类要求每个元素都必须来自给定的类型。

JsonDict对象可以用作类似数组的对象(例如 $jsonDict["key1"]),但在撰写本文时,字典中插入的元素类型未进行检查。类型用于在创建字典时(例如使用 fromArray 静态函数)或将内容导出到数组或对象(例如使用 toArray 函数)时解析内容。

方法包括

  • toArray()
  • toObject()
  • ::fromArray($data)
  • ::fromObject($data)

这些方法与JsonObject的情况解释相同。字典中元素的类型可能引用复杂类型,在解析内容时会递归考虑。

例如,类型 list[list[int]] 将用于解析 [ [ 1, 2, 3], [ 4, 5, 6 ]]

JsonArray

这个对象与JsonDict非常相似,区别在于索引必须是整数。在这种情况下,$value["key1"]将引发异常。

在这种情况下,实现向数组中添加元素(即[])的函数。

初始化值

在定义类时,可以为新创建的对象以及那些可选属性初始化值。

有两种方式

### 使用类属性

可以通过使用类属性来初始化对象值,因此如果属性值在类中设置,它将被复制到实例中作为属性,如果它已定义。

例如:

class User extends JsonObject {
    const ATTRIBUTES = [
        'id' => 'int',
        'name' => 'str',
        'age' => 'int',
        'emails' => 'list[str]',
        'address?' => 'Address',
        'sex?' => 'str'
    ];

    public $sex = "not revealed";
}

现在,属性 sex 被初始化为 未公开 而不是 null

使用属性的说明

要这样做,请为对象的类型定义一个 [ <type>, <default value> ] 元组。以下一个示例为例

class User extends JsonObject {
    const ATTRIBUTES = [
        'id' => 'int',
        'name' => 'str',
        'age' => 'int',
        'emails' => 'list[str]',
        'address?' => 'Address',
        'sex?' => [ 'str', 'not revealed' ]
    ];
}

当检索用户数据时,属性 sex 是可选的。使用这个新的类定义,如果 sex 未设置,则值将设置为 "未公开" 而不是 null

一个重要特性是,如果设置为 <default value> 的字符串是对象的某个方法,则在获取值时将调用该方法(如果尚未设置),并且为该属性设置的值将是该调用的结果。

例如:

class User extends JsonObject {
    const ATTRIBUTE = [
        ...
        'birthDay?' => [ 'str', 'computeBirthDate' ]
    ]
    function computeBirthDate() {
        $now = new DateTime();
        $now->sub(DateInterval::createFromDateString("{$this->age} years"));
        return $now->format("Y-m-d");
    }
}

在这个例子中,如果我们没有设置 birthDate 属性,但是检索它,它将通过从当前日期减去年龄来计算。

其他工具和技术事实

解析值

如果要将任意对象解析为JsonObject,可以使用函数JsonObject::parse_typed_value。这很重要,可以将任何类型转换为JsonObject类型。

例如:

$myobject = JsonObject::parse_typed_value("list[str]", [ "my", "name", "is", "John" ]);

将获得一个类型为 JsonList<str> 的对象。

类型检查

该库的默认行为是确保设置的属性值与其定义的类型匹配。但这意味着,由于浮点数(float)不是整数(int),将浮点数设置为0将会失败,因为0是一个整数。在这种情况下,用户必须在赋值之前对值进行类型转换。要控制是否严格检查类型,可以使用常量STRICT_TYPE_CHECKING

如果将STRICT_TYPE_CHECKING设置为True,类型将被严格检查,例如将9.3赋值给int将引发异常。如果设置为False,数值类型将相互转换。例如,如果我们将9.3赋值给int,它将被自动截断为9

其他重要的类型检查是在将空值(即""null)赋值给数值类型时。在这种情况下,我们有一个常量STRICT_TYPE_CHECKING_EMPTY_ZERO

如果将STRICT_TYPE_CHECKING_EMPTY_ZERO设置为True(默认行为),当将空值赋给数值类型时,它将被视为0。即,将空字符串或null值赋给int属性,意味着赋值为0。如果设置为False,库将检查类型,并最终引发异常。

增强的JsonLists

现在JsonList也支持使用负索引,因此-1将是最后一个元素,-2是倒数第二个,等等。

JsonList对象包含排序或过滤的函数。

  • public function sort(callable $callback = null) : JsonList:使用给定的回调函数对列表进行排序。如果没有提供回调函数,将使用默认的比较函数对列表进行排序。
  • public function filter(callable $callback) : JsonList:使用给定的回调函数过滤列表。回调函数必须返回一个布尔值。如果回调返回true,则元素将被包含在结果列表中。如果返回false,则元素将被丢弃。