square/pjson

JSON 与 PHP 模型序列化/反序列化库。直接将 JSON 反序列化为 PHP 对象模型类。

v0.5.0 2024-03-15 18:19 UTC

README

一个简单的 JSON 到 PHP 对象转换库

我们经常需要与返回 JSON 的 API 或数据源进行交互。PHP 只提供将 JSON 反序列化为数组或 stdClass 类型对象的可能性。

此库帮助将 JSON 反序列化为自定义定义的类的实际对象。它通过使用 PHP8 类属性的属性来实现。

示例

类的简单序列化

use Square\Pjson\Json;
use Square\Pjson\JsonSerialize;

class Schedule
{
    use JsonSerialize;

    #[Json]
    protected int $start;

    #[Json]
    protected int $end;

    public function __construct(int $start, int $end)
    {
        $this->start = $start;
        $this->end = $end;
    }
}

(new Schedule(1, 2))->toJson();

将产生以下结果

{
    "start": 1,
    "end": 2
}

然后可以通过以下方式实现反向操作

Schedule::fromJsonString('{"start":1,"end":2}');

这将返回一个 Schedule 类的实例,其属性根据 JSON 设置。

自定义名称

上一个示例可以修改为在 JSON 中使用自定义名称而不是仅使用属性名称

use Square\Pjson\Json;
use Square\Pjson\JsonSerialize;

class Schedule
{
    use JsonSerialize;

    #[Json('begin')]
    protected int $start;

    #[Json('stop')]
    protected int $end;

    public function __construct(int $start, int $end)
    {
        $this->start = $start;
        $this->end = $end;
    }
}

(new Schedule(1, 2))->toJson();

将产生以下结果

{
    "begin": 1,
    "stop": 2
}

使用这些新的属性名称进行反序列化与之前一样工作

dump(Schedule::fromJsonString('{"begin":1,"stop":2}'));

// ^ Schedule^ {#345
//   #start: 1
//   #end: 2
// }

私有/受保护

属性的可见性无关紧要。私有或受保护的属性也可以进行序列化和反序列化(请参阅前面的示例)。

属性路径

有时 JSON 格式与我们想要的 PHP 版本不完全一致。例如,假设我们收到的用于前面调度示例的 JSON 格式如下

{
    "data": {
        "start": 1,
        "end": 2
    }
}

通过如下声明我们的类 json 属性,我们仍然可以直接将这些属性读取到我们的类中

class Schedule
{
    use JsonSerialize;

    #[Json(['data', 'start'])]
    protected int $start;

    #[Json(['data', 'end'])]
    protected int $end;

    public function __construct(int $start, int $end)
    {
        $this->start = $start;
        $this->end = $end;
    }
}

递归序列化/反序列化

如果我们正在处理一个稍微复杂一点的 JSON 结构,我们希望属性是类,可以正确地反序列化。

{
    "saturday": {
        "start": 0,
        "end": 2
    },
    "sunday": {
        "start": 0,
        "end": 7
    }
}

以下 2 个 PHP 类可以很好地与此一起使用

class Schedule
{
    use JsonSerialize;

    #[Json]
    protected int $start;

    #[Json]
    protected int $end;
}

class Weekend
{
    use JsonSerialize;

    #[Json('saturday')]
    protected Schedule $sat;
    #[Json('sunday')]
    protected Schedule $sun;
}

数组

当我们处理一个数组的项目时,其中每个项目都应该是一个给定的类,我们需要告诉 pjson 目标类型

{
    "days": [
        {
            "start": 0,
            "end": 2
        },
        {
            "start": 0,
            "end": 2
        },
        {
            "start": 0,
            "end": 2
        },
        {
            "start": 0,
            "end": 7
        }
    ]
}

假设 Schedule 仍然定义如前,我们可以定义一个星期如下

class Week
{
    use JsonSerialize;

    #[Json(type: Schedule::class)]
    protected array $days;
}

如果 JSON 如下,这也会工作

{
    "days": {
        "monday": {
            "start": 0,
            "end": 2
        },
        "wednesday": {
            "start": 0,
            "end": 2
        }
    }
}

生成的 PHP 对象将是

Week^ {#353
  #days: array:2 [
    "monday" => Schedule^ {#344
      #start: 0
      #end: 2
    }
    "wednesday" => Schedule^ {#343
      #start: 0
      #end: 2
    }
  ]
}

集合类

类似于数组,您可能希望使用集合类。只要您的类实现 Traversable 接口,您就可以这样做。在这种情况下,pjson 将默认尝试通过构造函数传递数据数组来构造您的类。如果这不适合您,您可以为您的集合指定一个自定义工厂方法

class Collector
{
    use JsonSerialize;

    #[Json(type: Schedule::class)]
    public Collection $schedules;

    #[Json(type: Schedule::class, collection_factory_method: 'make')]
    public Collection $static_factoried_schedules;

    #[Json(type: Schedule::class, collection_factory_method: 'makeme')]
    public Collection $factoried_schedules;
}

在我们的示例中,集合有一个静态工厂方法 make 和一个实例方法 makeme,它们都可以使用。构造函数选项也有效。您可以在 tests/Definitions 目录中查看集合类。

这允许您使用类似以下内容的 JSON 进行操作

{
    "schedules": [
        {
            "schedule_start": 1,
            "schedule_end": 2
        }
    ],
    "factoried_schedules": [
        {
            "schedule_start": 10,
            "schedule_end": 20
        }
    ],
    "static_factoried_schedules": [
        {
            "schedule_start": 100,
            "schedule_end": 200
        }
    ]
}

多态反序列化

假设您有两个继承自基类的类。您可能将它们作为集合接收,并且事先不知道您将处理其中一个还是另一个。例如

abstract class CatalogObject
{
    use JsonSerialize;

    #[Json]
    protected $id;

    #[Json]
    protected string $type;
}

class CatalogCategory extends CatalogObject
{
    use JsonSerialize;

    #[Json('parent_category_id')]
    protected string $parentCategoryId;
}

class CatalogItem extends CatalogObject
{
    use JsonSerialize;

    #[Json]
    protected string $name;
}

您可以在 CatalogObject 上实现 fromJsonData(array $array) : static 以根据接收到的数据进行区分并返回正确的序列化

abstract class CatalogObject
{
    use JsonSerialize;

    #[Json]
    protected $id;

    #[Json]
    protected string $type;

    public static function fromJsonData($jd): static
    {
        $t = $jd['type'];

        return match ($t) {
            'category' => CatalogCategory::fromJsonData($jd),
            'item' => CatalogItem::fromJsonData($jd),
        };
    }
}

警告:请确保每个子类直接 use JsonSerialize。否则,当它们调用 ::fromJsonData 时,它们将调用 CatalogObject 的父类,导致无限递归。

有了这个,我们可以做

$jsonCat = '{"type": "category", "id": "123", "parent_category_id": "456"}';
$c = CatalogObject::fromJsonString($jsonCat);
$this->assertEquals(CatalogCategory::class, get_class($c));

$jsonItem = '{"type": "item", "id": "123", "name": "Sandals"}';
$c = CatalogObject::fromJsonString($jsonItem);
$this->assertEquals(CatalogItem::class, get_class($c));

列表

如果您正在处理要反序列化的东西的列表,您可以使用 MyClass::listFromJsonString($json)MyClass::listfromJsonData($array)。例如

Schedule::listFromJsonString('[
    {
        "schedule_start": 1,
        "schedule_end": 2
    },
    {
        "schedule_start": 11,
        "schedule_end": 22
    },
    {
        "schedule_start": 111,
        "schedule_end": 222
    }
]');

产生与以下相同的结果

[
    new Schedule(1, 2),
    new Schedule(11, 22),
    new Schedule(111, 222),
];

初始路径

有时你关心的JSON会嵌套在一个属性下,但你不需要/不需要对最外层进行建模。为此,你可以将一个$path传递给反序列化方法。

Schedule::fromJsonString('{
    "data": {
        "schedule_start": 1,
        "schedule_end": 2
    }
}', path: 'data');

Schedule::fromJsonString('{
    "data": {
        "main": {
            "schedule_start": 1,
            "schedule_end": 2
        }
    }
}', path: ['data', 'main']);

枚举

支持在PHP 8.1中直接使用支持的后备枚举。

class Widget
{
    use JsonSerialize;

    #[Json]
    public Status $status;
}

enum Status : string
{
    case ON = 'ON';
    case OFF = 'OFF';
}
$w = new Widget;
$w->status = Status::ON;

$w->toJson(); // {"status": "ON"}

并且可以通过JsonSerialize特质或JsonDataSerializable接口来支持常规枚举。

class Widget
{
    use JsonSerialize;

    #[Json]
    public Size $size;
}

enum Size
{
    use JsonSerialize;

    case BIG;
    case SMALL;

    public static function fromJsonData($d, array|string $path = []): static
    {
        return match ($d) {
            'BIG' => self::BIG,
            'SMALL' => self::SMALL,
            'big' => self::BIG,
            'small' => self::SMALL,
        };
    }

    public function toJsonData()
    {
        return strtolower($this->name);
    }
}

$w = new Widget;
$w->size = Size::BIG;

$w->toJson(); // {"status": "big"}

必需属性

你可以标记一个属性在反序列化时为必需。

readonly class Token
{
    use JsonSerialize;

    #[Json(required: true)]
    public string $key;
}

$token = Token::fromJsonString('{"key":"data"}'); // successful

Token::fromJsonString('{"other":"has no key"}'); // throws Exception

标量 <=> 类

在某些情况下,你可能希望在反序列化后标量值变成PHP对象,反之亦然。例如,一个BigInt类可以持有字符串形式的int,并在序列化为JSON时将其表示为字符串。

class Stats
{
    use JsonSerialize;

    #[Json]
    public BigInt $count;
}

class BigInt implements JsonDataSerializable
{
    public function __construct(
        protected string $value,
    ) {
    }

    public static function fromJsonData($jd, array|string $path = []) : static
    {
        return new BigInt($jd);
    }

    public function toJsonData()
    {
        return $this->value;
    }
}

$stats = new Stats;
$stats->count = new BigInt("123456789876543234567898765432345678976543234567876543212345678765432");
$stats->toJson(); // {"count":"123456789876543234567898765432345678976543234567876543212345678765432"}

集合类

如果你希望使用pjson与集合类一起使用

与PHPStan一起使用

使用此库时,你可能会有一些属性看起来在代码的任何地方都没有被读取或写入,但它们仅用于JSON序列化。PHPStan会对此类问题提出警告,但你可以在你的phpstan.neon中添加此库的扩展,以帮助PHPStan理解这是预期行为。

includes:
  - vendor/square/pjson/extension.neon

Laravel集成

通过可转换类型

如果你希望通过Pjson将Eloquent模型属性转换为类,你可以使用提供的转换实用工具来实现。

use Illuminate\Contracts\Database\Eloquent\Castable;
use Square\Pjson\Json;
use Square\Pjson\JsonSerialize;
use Square\Pjson\Integrations\Laravel\JsonCastable;

class Schedule implements Castable // implement the laravel interface
{
    use JsonSerialize;
    use JsonCastable; // use the provided Pjson trait

    #[Json]
    protected int $start;

    #[Json]
    protected int $end;

    public function __construct(int $start, int $end)
    {
        $this->start = $start;
        $this->end = $end;
    }
}

然后在你的Eloquent模型中

$casts = [
    'schedule' => Schedule::class,
];

通过转换参数

或者,你可以简单地使用Laravel的转换参数。在这种情况下,Schedule类保持不变。

use Square\Pjson\Json;
use Square\Pjson\JsonSerialize;

class Schedule
{
    use JsonSerialize;

    #[Json]
    protected int $start;

    #[Json]
    protected int $end;

    public function __construct(int $start, int $end)
    {
        $this->start = $start;
        $this->end = $end;
    }
}

并且你提供转换的目标类

$casts = [
    'schedule' => JsonCaster::class.':'.Schedule::class,
];