crell/serde

一款通用的序列化和反序列化库

资助包维护!
Crell

1.3.0 2024-09-20 04:48 UTC

README

Latest Version on Packagist Software License Total Downloads

Serde(发音为“seer-dee”)是一款快速、灵活、强大且易于使用的PHP序列化和反序列化库,支持多种标准格式。它从Rust的Serde包和Symfony Serializer中汲取灵感,尽管它不是直接基于任何一个。

目前,Serde支持将PHP对象序列化和反序列化为PHP数组、JSON、YAML、TOML和CSV文件。它还支持通过流将数据序列化为JSON或CSV。计划进一步支持,但按设计也可以由任何人扩展。

安装

通过Composer

$ composer require crell/serde

用法

Serde旨在快速上手并适应更复杂的场景。在其最基本的形式中,您可以这样做

use Crell\Serde\SerdeCommon;

$serde = new SerdeCommon();

$object = new SomeClass();
// Populate $object somehow;

$jsonString = $serde->serialize($object, format: 'json');

$deserializedObject = $serde->deserialize($jsonString, from: 'json', to: SomeClass::class);

(命名参数是可选的,但推荐使用。)

Serde非常易于配置,但常见情况只需使用提供的SerdeCommon类即可。对于大多数基本场景,这就足够了。

主要功能

支持格式

Serde可以序列化为

  • PHP数组(array
  • JSON(json
  • 流式JSON(json-stream
  • YAML(yaml
  • TOML(toml
  • CSV(csv
  • 流式CSV(csv-stream

Serde可以从以下内容反序列化

  • PHP数组(array
  • JSON(json
  • YAML(yaml
  • TOML(toml
  • CSV(csv

YAML支持需要Symfony/Yaml库。

TOML支持需要Vanodevium/Toml库。

XML支持正在开发中。

强大的对象支持

Serde自动支持嵌套对象在其它对象属性中的情况,只要没有循环引用,将会递归处理。

Serde处理publicprivateprotectedreadonly属性,包括读写操作,并提供可选默认值。

如果您尝试序列化或反序列化实现了PHP的__serialize()__unserialize()钩子的对象,那些钩子将会被尊重。(如果您想直接从PHP的内部序列化格式中读取/写入,只需直接调用serialize()/unserialize()即可。)

Serde还支持后加载回调,允许您在必要时重新初始化派生信息,而无需将其存储在序列化格式中。

PHP对象可以转换为序列化格式,也可以从序列化格式转换回来。嵌套对象可以被扁平化或收集,具有公共接口的类可以被映射到相应的对象,数组值可以被压缩成字符串进行序列化,并在读取时再次展开成数组。

配置

Serde的行为几乎完全由属性驱动。任何类都可以不进行额外配置即可直接序列化或反序列化,但有许多配置选项可以选择。

属性处理由Crell/AttributeUtils提供。这也值得一看。

主要属性是 Crell\Serde\Attributes\Field 属性,它可以放置在任何对象属性上。(静态属性将被忽略。)它的所有参数都是可选的,包括 Field 本身。(也就是说,添加没有参数的 #[Field] 与不指定它是一样的。)下面列出了可用参数的含义。

虽然不是必需的,但强烈建议您始终使用命名参数与属性一起使用。参数的精确顺序 无法保证

在下面的示例中,通常直接引用 Field。然而,您也可以导入命名空间,然后使用属性的命名空间版本,如下所示

use Crell\Serde\Attributes as Serde;

#[Serde\ClassSettings(includeFieldsByDefault: false)]
class Person
{
    #[Serde\Field(serializedName: 'callme')]
    protected string $name = 'Larry';
}

您选择哪种方式主要取决于个人喜好,但如果您正在混合 Serde 属性与其他库的属性,则建议使用命名空间方法。

还有一个可以放置在要序列化的类上的 ClassSettings 属性。目前它有四个参数

  • includeFieldsByDefault,默认值为 true。如果设置为 false,则没有 #[Field] 属性的属性将被忽略。这相当于在所有属性上隐式设置 exclude: true
  • requireValues,默认值为 false。如果设置为 true,则在反序列化时,如果传入数据中未提供任何字段,将抛出异常。这也可以在字段级别打开或关闭。(见下面的 requireValue。)类级别的设置适用于未指定其行为的任何字段。
  • renameWith。如果设置,将使用指定的重命名策略来处理类的所有属性,除非属性指定了自己的。 (见下面的 renameWith。)类级别的设置适用于未指定其行为的任何字段。
  • omitNullFields,默认值为 false。如果设置为 true,则在序列化时将省略类上任何为 null 的属性。它对反序列化没有影响。这也可以在字段级别打开或关闭。(见下面的 omitIfNull。)
  • scopes,设置给定类定义属性的范畴。见下面的范畴部分。

exclude(布尔值,默认 false)

如果设置为 true,Serde 将在序列化和反序列化时完全忽略该属性。

serializedName(字符串,默认 null)

如果提供,此字符串将用作将属性序列化到格式时以及读取它时使用的名称。例如

use Crell\Serde\Attributes\Field;

class Person
{
    #[Field(serializedName: 'callme')]
    protected string $name = 'Larry';
}

往返于

{
    "callme": "Larry"
}

renameWith(重命名策略,默认 null)

renameWith 键指定了一种将属性名称打乱以产生序列化名称的方法。最常见的例子是大小写折叠,例如将序列化到使用与 PHP 不同的约定格式的格式。

renameWith 的值可以是实现 RenamingStrategy 接口的对象。最常见的版本已经通过 Cases 枚举和 Prefix 类提供,但您也可以提供自己的。

Cases 枚举实现了 RenamingStrategy 并提供了一系列实例(案例)以进行常见的重命名。例如

use Crell\Serde\Attributes\Field;
use Crell\Serde\Renaming\Cases;

class Person
{
    #[Field(renameWith: Cases::snake_case)]
    public string $firstName = 'Larry';

    #[Field(renameWith: Cases::CamelCase)]
    public string $lastName = 'Garfield';
}

序列化/反序列化

{
    "first_name": "Larry",
    "LastName": "Garfield"
}

可用案例有

  • Cases::UPPERCASE
  • Cases::lowercase
  • Cases::snake_case
  • Cases::kebab_case

    (使用连字符,而不是下划线)
  • Cases::CamelCase
  • Cases::lowerCamelCase

Prefix 类在序列化时将前缀附加到值上,但保留属性名称不变。

use Crell\Serde\Attributes\Field;
use Crell\Serde\Renaming\Prefix;

class MailConfig
{
    #[Field(renameWith: new Prefix('mail_')]
    protected string $host = 'smtp.example.com';

    #[Field(renameWith: new Prefix('mail_')]
    protected int $port = 25;

    #[Field(renameWith: new Prefix('mail_')]
    protected string $user = 'me';

    #[Field(renameWith: new Prefix('mail_')]
    protected string $password = 'sssh';
}

序列化/反序列化

{
    "mail_host": "smtp.example.com",
    "mail_port": 25,
    "mail_user": "me",
    "mail_password": "sssh"
}

如果同时指定了 serializedNamerenameWith,则使用 serializedName,忽略 renameWith

alias(数组,默认 []

在反序列化时(仅限反序列化),如果预期的序列化名称在传入的数据中没有找到,则会检查这些附加属性名称,以查看是否可以找到值。如果可以,则将从传入数据中的该键读取值。如果不可以,则其行为与值最初就没有找到时相同。

use Crell\Serde\Attributes\Field;

class Person
{
    #[Field(alias: ['layout', 'design'])]
    protected string $format = '';
}

以下三个JSON字符串都会被读取到相同的对象中

{
    "format": "3-column-layout"
}
{
    "layout": "3-column-layout"
}
{
    "design": "3-column-layout"
}

这主要在API密钥更改时很有用,而旧的数据可能仍然包含旧的关键字名称。

omitIfNull(布尔值,默认为false)

此键仅适用于序列化。如果设置为true,并且当对象序列化时此属性的值为null,则将从输出中完全省略它。如果为false,则将输出写入null,无论这看起来像什么特定格式。

useDefault(布尔值,默认为true)

此键仅适用于反序列化。如果一个类的属性在传入数据中没有找到,并且此属性为true,则将分配默认值。如果为false,则将完全跳过值。反序列化的对象是否现在处于无效状态取决于对象本身。

要使用的默认值来源于多个不同位置。默认值的优先级顺序是

  1. Field属性的default参数提供的值。
  2. 代码提供的默认值,如反射所报告。
  3. 如果有,则具有相同名称的构造函数参数的默认值。

例如,以下类

use Crell\Serde\Attributes\Field;

class Person
{
    #[Field(default: 'Hidden')]
    public string $location;

    #[Field(useDefault: false)]
    public int $age;

    public function __construct(
        public string $name = 'Anonymous',
    ) {}
}

如果从一个空源(如JSON中的{})反序列化,将导致一个设置locationHiddennameAnonymous,而age仍然未初始化的对象。

default(混合类型,默认为null)

此键仅适用于反序列化。如果指定,则如果正在反序列化的传入数据中缺少值,则将使用此值,而不管源代码中的默认值是什么。

strict(布尔值,默认为true)

此键仅适用于反序列化。如果设置为true,则将拒绝传入数据中的类型不匹配,并抛出异常。如果为false,则格式化程序将尝试根据PHP的正常转换规则转换传入的值。这意味着,例如,如果strict设置为false,则整数属性的值可以是字符串"1",但如果设置为true,则将抛出异常。

对于序列字段,如果strict设置为true,则将拒绝非序列值。(它必须通过array_is_list()检查。)如果strict设置为false,则将接受任何类似数组的值,但通过array_values()传递以丢弃任何键并重新索引。

此外,在非strict模式下,传入数组中的数值字符串将被转换为整数或浮点数,具体取决于序列字段和字典字段。在strict模式下,数值字符串仍然会被拒绝。

此设置的精确处理可能因传入的格式略有不同,因为某些格式以不同的方式处理它们自己的类型。(例如,XML中一切都是字符串。)

requireValue(布尔值,默认为false)

此键仅适用于反序列化。如果设置为true,如果传入数据未包含此字段的值且未指定默认值,则将抛出MissingRequiredValueWhenDeserializing异常。如果没有设置,且没有默认值,则将使属性保持未初始化状态。

如果字段具有默认值,则将始终使用默认值用于缺失数据,并且此设置没有效果。

flatten(布尔值,默认为false)

flatten 关键字只能应用于数组或对象的属性。经过“扁平化”的属性将在序列化时将其所有属性直接注入到父对象中,并在反序列化时将父对象的值“收集”到其中。

多个对象和数组可以进行扁平化(序列化),但在反序列化时,只有最后标记为 flatten 的数组属性会收集剩余的键。不过,任何数量的对象都可以“收集”它们的属性。

例如,考虑分页。将分页信息表示为结果集的对象属性在 PHP 中可能非常有用,但在序列化的 JSON 或 XML 中,您可能希望删除额外的对象。

给定以下类集

use Crell\Serde\Attributes as Serde;

class Results
{
    public function __construct(
        #[Serde\Field(flatten: true)]
        public Pagination $pagination,
        #[Serde\SequenceField(arrayType: Product::class)]
        public array $products,
    ) {}
}

class Pagination
{
    public function __construct(
        public int $total,
        public int $offset,
        public int $limit,
    ) {}
}

class Product
{
    public function __construct(
        public string $name,
        public float $price,
    ) {}
}

在序列化时,$pagination 对象将被“扁平化”,这意味着它的三个属性将直接包含在 Results 的属性中。因此,此对象的 JSON 序列化副本可能如下所示

{
    "total": 100,
    "offset": 20,
    "limit": 10,
    "products": [
        {
            "name": "Widget",
            "price": 9.99
        },
        {
            "name": "Gadget",
            "price": 4.99
        }
    ]
}

已删除 Pagination 对象的额外“层”。在反序列化时,这些额外属性将“收集”回 Pagination 对象。

现在考虑这个更复杂的例子

use Crell\Serde\Attributes as Serde;

class DetailedResults
{
    public function __construct(
        #[Serde\Field(flatten: true)]
        public NestedPagination $pagination,
        #[Serde\Field(flatten: true)]
        public ProductType $type,
        #[Serde\SequenceField(arrayType: Product::class)]
        public array $products,
        #[Serde\Field(flatten: true)]
        public array $other = [],
    ) {}
}

class NestedPagination
{
    public function __construct(
        public int $total,
        public int $limit,
        #[Serde\Field(flatten: true)]
        public PaginationState $state,
    ) {}
}

class PaginationState
{
    public function __construct(
        public int $offset,
    ) {
    }
}

class ProductType
{
    public function __construct(
        public string $name = '',
        public string $category = '',
    ) {}
}

在这个例子中,NestedPaginationPaginationState 都将在序列化时进行扁平化。NestedPagination 本身也有一个应该进行扁平化的字段。只要它们之间没有共享属性名,两者都可以干净地扁平化和收集。

此外,还有一个额外的数组属性,$other$other 可以包含任何所需的关联数组,其值也将扁平化到输出中。

在收集时,只有最后扁平化的数组会获取任何数据,并且会获取所有其他属性未记录的属性。例如,DetailedResults 的一个实例可能序列化为 JSON,如下所示

{
    "total": 100,
    "offset": 20,
    "limit": 10,
    "products": [
        {
            "name": "Widget",
            "price": 9.99
        },
        {
            "name": "Gadget",
            "price": 4.99
        }
    ],
    "foo": "beep",
    "bar": "boop"
}

在这种情况下,$other 属性有两个键,分别是 foobar,其值分别是 beepboop。相同的 JSON 将反序列化为之前相同的对象。

值对象

扁平化还可以与重命名结合使用,以静默地转换值对象。考虑以下示例

class Person
{
    public function __construct(
        public string $name,
        #[Field(flatten: true)]
        public Age $age,
        #[Field(flatten: true)]
        public Email $email,
    ) {}
}

readonly class Email
{
    public function __construct(
        #[Field(serializedName: 'email')] public string $value,
    ) {}
}

readonly class Age
{
    public function __construct(
        #[Field(serializedName: 'age')] public int $value
    ) {
        $this->validate();
    }

    #[PostLoad]
    private function validate(): void
    {
        if ($this->value < 0) {
            throw new \InvalidArgumentException('Age cannot be negative.');
        }
    }
}

在这个例子中,EmailAge 都是值对象,在后者中带有额外的验证。然而,两者都标记为 flatten: true,因此它们的属性将在序列化时提升到 Person 的级别。但是,它们都使用了相同的属性名,因此都指定了自定义序列化名称。上面的对象将序列化为(并反序列化为)如下所示

{
    "name": "Larry",
    "age": 21,
    "email": "me@example.com"
}

请注意,由于反序列化绕过了构造函数,Age 中的额外验证必须放置在单独的方法中,该方法由构造函数调用并标记为在反序列化后自动运行。

还可以指定扁平化值的前缀,这也会递归地应用。例如,假设上面的 Age 类相同

readonly class JobDescription
{
    public function __construct(
        #[Field(flatten: true, flattenPrefix: 'min_')]
        public Age $minAge,
        #[Field(flatten: true, flattenPrefix: 'max_')]
        public Age $maxAge,
    ) {}
}

class JobEntry
{
    public function __construct(
        #[Field(flatten: true, flattenPrefix: 'desc_')]
        public JobDescription $description,
    ) {}
}

在这种情况下,序列化 JobEntry 时,首先将 $description 属性扁平化,前缀为 desc_。然后,JobDescription 将扁平化其两个年龄字段,每个字段都有一个单独的前缀。这将产生一个类似以下的序列化输出

{
    "desc_min_age": 18,
    "desc_max_age": 65,
}

并且它可以反序列化为相同的三层对象结构。

flattenPrefix(字符串,默认为空字符串)

当一个对象或数组属性被扁平化时,默认情况下其属性将使用它们的现有名称(如果指定了,则使用 serializedName)进行扁平化。如果同一个类在父类中两次包含,或者存在其他名称冲突,这可能会导致问题。相反,扁平化字段可以给定一个 flattenPrefix 值。该字符串将在序列化时添加到属性名称的前面。

如果设置在非扁平化字段上,此值没有意义且没有效果。

序列和字典

在大多数语言和许多序列化格式中,值序列列表(被称为数组、序列或列表)与任意大小的任意值到其他任意值的映射(被称为字典或映射)之间存在区别。PHP不区分这两种数据类型,而是将它们都放入一个关联数组变量类型中。

有时这可以行得通,但有时这两种数据类型之间的区别非常重要。为了支持这些情况,Serde 允许您将数组属性标记为 #[SequenceField]#[DictionaryField](并建议您始终这样做)。这样做可以确保使用正确的序列化路径来处理属性,同时也打开了许多其他功能。

arrayType

#[SequenceField]#[DictionaryField] 上,arrayType 参数允许您指定该结构中所有值的类型。例如,整数序列可以轻松地序列化到和反序列化从大多数格式,无需任何额外帮助。然而,有序的 Product 对象列表可以序列化,但没有方法可以告诉如何将其反序列化为 Product 对象而不是嵌套的关联数组(这同样是合法的)。arrayType 参数解决了这个问题。

如果指定了 arrayType,则假定该数组的所有值都是该类型。它可以是指定所有值都是类的 class-string,或是一个表示四种支持的标量的 ValueType 枚举值。

在反序列化时,Serde 将验证所有传入的值是否都是正确的标量类型,或者根据特定的格式寻找嵌套的对象结构,并将这些结构转换为指定的对象类型。

例如

use Crell\Serde\Attributes\SequenceField;

class Order
{
    public string $orderId;

    public int $userId;

    #[SequenceField(arrayType: Product::class)]
    public array $products;
}

在这种情况下,该属性告诉 Serde,$products 是一个索引的、顺序的 Product 对象列表。在序列化时,这可能表示为字典数组(在 JSON 或 YAML 中)或在其他格式中使用一些额外的元数据。

在反序列化时,那些否则会被忽略的对象数据将被提升回 Product 对象。

arrayTypeDictionaryField 上也以完全相同的方式工作。

keyType

仅在 DictionaryField 上,可以将数组限制为只允许整数或字符串键。它有两个合法值,KeyType::IntKeyType::String(一个枚举)。如果设置为 KeyType::Int,则反序列化将拒绝任何具有字符串键的数组,但将接受数字字符串。如果设置为 KeyType::String,则反序列化将拒绝任何具有整数键的数组,包括数字字符串。

(PHP 自动将整数字符串数组键转换为实际的整数,因此在基于字符串的字典中无法允许它们。)

如果没有设置值,则接受任何键类型。

implodeOn

如果存在,SequenceFieldimplodeOn 参数指示应将值连接成一个字符串序列化,使用提供的值作为粘合剂。例如

use Crell\Serde\Attributes\SequenceField;

class Order
{
    #[SequenceField(implodeOn: ',')]
    protected array $productIds = [5, 6, 7];
}

将序列化为 JSON

{
    "productIds": "5,6,7"
}

在反序列化时,该字符串将自动在放入对象时拆分成数组。

默认情况下,在反序列化时,将使用 trim() 删除单个值中的多余空白。可以通过将 trim 属性参数设置为 false 来禁用此操作。

joinOn

DictionaryField 也支持序列化时的压缩/解压缩,但需要两个键。implodeOn 指定在区分值之间使用的字符串。joinOn 指定在键和值之间使用的字符串。

例如

use Crell\Serde\Attributes\DictionaryField;

class Settings
{
    #[DictionaryField(implodeOn: ',', joinOn: '=')]
    protected array $dimensions = [
        'height' => 40,
        'width' => 20,
    ];
}

将序列化/反序列化为此 JSON

{
    "dimensions": "height=40,width=20"
}

SequenceField 一样,除非在属性参数列表中指定了 trim: false,否则将自动使用 trim() 删除值。

日期和时间字段

DateTimeDateTimeImmutable 字段也可以序列化,您可以使用 DateFieldUnixTimeField 属性来控制它们的序列化方式。 DateField 有两个参数,可以单独使用或一起使用。不指定任何一个参数与完全不指定 DateField 属性相同。

use Crell\Serde\Attributes\DateField;

class Settings
{
    #[DateField(format: 'Y-m-d')]
    protected DateTimeImmutable $date = new DateTimeImmutable('4 July 2022-07-04 14:22);
}

将序列化为以下 JSON

{
    "date": "2022-07-04"
}

时区

timezone 参数可以是 PHP 中合法的任何时区字符串,例如 America/ChicagoUTC。如果指定,值将首先转换为该时区,然后再进行序列化。如果没有指定,值将保持序列化前的时区。这会不会影响输出取决于 format

在反序列化时,timezone 没有作用。如果传入的值指定了时区,则生成的 DateTime[Immutable] 对象将使用该时区。如果没有指定,则使用系统默认时区。

格式

此参数允许您指定序列化时使用的格式。它可以接受 PHP 的 date_format 语法 中的任何字符串,包括在 DateTimeInterface 上定义的各种常量之一。如果未指定,则默认格式为 RFC3339_EXTENDED,或 Y-m-d\TH:i:s.vP。虽然不是最人性化的,但它是 Javascript/JSON 使用的默认格式,因此具有合理的兼容性。

在反序列化时,format 没有作用。Serde 将将字符串值传递给 DateTimeDateTimeImmutable 构造函数,因此任何 PHP 可以识别的格式都将根据 PHP 的标准日期解析规则进行解析。

Unix 时间

在需要将日期序列化为/反序列化为 Unix 时间的情况下,可以使用 UnixTimeField,它支持可以处理高达微秒分辨率的分辨率参数。

use Crell\Serde\Attributes\UnixTimeField;
use Crell\Serde\Attributes\Enums\UnixTimeResolution;

class Jwt
{
    #[UnixTimeField]
    protected DateTimeImmutable $exp;
    
    #[UnixTimeField(resolution: UnixTimeResolution::Milliseconds)]
    protected DateTimeImmutable $iss;
}

将序列化为以下 JSON

{
    "exp": 1707764358,
    "iss": 1707764358000
}

序列化的整数应读作“自纪元以来的这些秒”或“自纪元以来的这些毫秒”等。(“纪元”是指 1970 年 1 月 1 日,人类首次登上月球的第二年。)

请注意,由于我们能够表示的整型大小有限制,因此允许的毫秒和微秒的范围远小于秒。对于 21 世纪初的纪元,不应有问题,但尝试记录自纪元以来沙丘(位于 10,000 年左右)的微秒数将无法实现。

生成器、可迭代对象和可遍历对象

PHP 有许多“懒列表”选项。通常,它们都是实现了 \Traversable 接口的对象。然而,它们有几种语法选项可用,各有其细微差别。Serde 以不同的方式支持它们。

如果将属性定义为 iterable,那么无论它是一个 Traversable 对象还是一个生成器,序列化过程都会将可迭代对象“耗尽”并将其转换为数组。请注意,如果可迭代对象是一个无限迭代器,则此过程将永远继续,并且您的程序将冻结。不要这样做。

此外,当使用 iterable 属性时,属性必须根据需要标记为 #[SequenceField]#[DictionaryField]。Serde 不能像通常那样自行推断它是哪种类型,它(通常)可以与数组一起使用。

在反序列化时,传入的值将始终分配给一个数组。由于数组是一个 iterable,因此这仍然是类型安全的。虽然在理论上可以动态构建一个生成器以延迟生成值,但这实际上不会节省任何内存。

请注意,这意味着序列化和反序列化对象不会完全对称。原始对象可能具有生成器属性,但反序列化的对象将具有数组。

如果一个属性被定义为其他 Traversable 对象类型(通常是因为它实现了 \Iterator\IteratorAggregate),则它将被序列化和反序列化为一个普通对象。它的 iterable 特性将被忽略。在这种情况下,禁止使用 #[SequenceField]#[DictionaryField] 属性。

CSV 格式化器

Serde 包含了支持序列化和反序列化 CSV 文件的功能。然而,由于 CSV 是一种更有限的格式,仅支持某些对象结构。

具体来说,所涉及的对象必须有一个标记为 #[SequenceField] 的单个属性,并且它必须有一个显式的 arrayType,它是一个类。这个类,反过来,只能包含 intfloatstring 属性。其他任何内容都会抛出错误。

例如

namespace Crell\Serde\Records;

use Crell\Serde\Attributes\SequenceField;

class CsvTable
{
    public function __construct(
        #[SequenceField(arrayType: CsvRow::class)]
        public array $people,
    ) {}
}

class CsvRow
{
    public function __construct(
        public string $name,
        public int $age,
        public float $balance,
    ) {}
}

这种组合将生成一个三列的 CSV 文件,也可以从三列的 CSV 文件中反序列化。

CSV 格式化器使用 PHP 的原生 CSV 解析和写入工具。如果您想控制使用的分隔符,请将这些作为构造函数参数传递给一个 CsvFormatter 实例,并将其注入到 Serde 类中,而不是默认值。

请注意,这个单独的属性可能是一个生成器。这允许 CSV 在任意数据上动态生成。在反序列化时,它仍然会反序列化为一个数组。

Serde 包含两个基于流的格式化器(但目前不包括反格式化器),一个用于 JSON,一个用于 CSV。它们几乎以与其他格式化器相同的方式工作,但在调用 $serde->serialize() 时,您可以(并且应该)传递一个额外的 init 参数。$init 应该是 Serde\Formatter\FormatterStream 的一个实例,它包装了一个可写的 PHP 流句柄。

返回的值将是相同的流句柄,在将要序列化的对象写入它之后。

例如

// The JsonStreamFormatter and CsvStreamFormatter are not included by default.
$s = new SerdeCommon(formatters: [new JsonStreamFormatter()]);

// You may use any PHP supported stream here, including files, network sockets,
// stdout, an in-memory temp stream, etc.
$init = FormatterStream::new(fopen('/tmp/output.json', 'wb'));

$result = $serde->serialize($data, format: 'json-stream', init: $init);

// $result is a FormatterStream object that wraps the same handle as before.
// What you can now do with the stream depends on what kind of stream it is.

在这个例子中,$data 对象(无论是什么)将被部分序列化为 JSON 并流式传输到指定的文件句柄。

CsvStreamFormatter 以完全相同的方式工作,但输出 CSV 数据,并且在接受的对象方面具有与 CsvFormatter 相同的限制。

在许多情况下,这实际上不会带来很多好处,因为整个对象必须全部在内存中。但是,它可以与对延迟迭代器的支持结合使用,以有一个产生对象的属性,例如从数据库查询或从其他来源读取。

考虑以下示例

use Crell\Serde\Attributes\SequenceField;

class ProductList
{
    public function __construct(
        #[SequenceField(arrayType: Product::class)]
        private iterable $products,
    ) {}
}

class Product
{
    public function __construct(
        public readonly string $name,
        public readonly string $color,
        public readonly float $price,
    ) {}
}

$databaseConn = ...;

$callback = function() use ($databaseConn) {
    $result = $databaseConn->query("SELECT name, color, price FROM products ORDER BY name");

    // Assuming $record is an associative array.
    foreach ($result as $record) {
        yield new Product(...$record);
    }
};

// This is a lazy list of products, which will be pulled from the database.
$products = new ProductList($callback());

// Use the CSV formatter this time, but JsonStream works just as well.
$s = new SerdeCommon(formatters: [new CsvStreamFormatter()]);

// Write to stdout, aka, back to the browser.
$init = FormatterStream::new(fopen('php://output', 'wb'));

$result = $serde->serialize($products, format: 'csv-stream', init: $init);

这种设置将懒加载数据库中的记录并从中实例化一个对象,然后将这些数据懒加载地输出到 stdout。无论数据库中有多少产品记录,内存使用量大致保持不变。(注意数据库驱动程序可能对其整个结果集进行自己的缓冲,这可能会导致内存问题。但这是一个单独的问题。)

虽然对于 CSV 可能过于冗余,但它对于序列化为 JSON 的更复杂对象可以工作得非常好。

类型映射

类型映射是 Serde 的一个强大功能,它允许精确控制具有继承的对象的序列化和反序列化方式。类型映射在对象的类和序列化数据中包含的唯一标识符之间进行转换。

在抽象上,类型映射是实现 TypeMap 接口的对象。类型映射可以作为属性、类或接口上的属性提供,也可以在设置 Serde 时提供,以允许任意映射。

以下示例将用于对类型映射的其余解释

use Crell\Serde\Attributes\SequenceField;

interface Product {}

interface Book extends Product {}

class PaperBook implements Book
{
    protected string $title;
    protected int $pages;
}

class DigitalBook implements Book
{
    protected string $title;
    protected int $bytes;
}

class Sale
{
    protected Book $book;

    protected float $discountRate;
}

class Order
{
    protected string $orderId;

    #[SequenceField(arrayType: Book::class)]
    protected array $products;
}

SaleOrder 都引用 Book,但那个值可以是 PaperBookDigitalBook 或任何实现了 Book 的其他类。类型映射为 Serde 提供了一种方法,使其能够知道它是哪个具体类型。

类名映射

类映射的最简单情况是在对象属性上包含 #[ClassNameTypeMap] 属性。例如,

use Crell\Serde\ClassNameTypeMap;

class Sale
{
    #[ClassNameTypeMap(key: 'type')]
    protected Book $book;

    protected float $discountRate;
}

现在当一个 Sale 对象被序列化时,将包含一个名为 type 的额外属性,该属性包含类名。因此,对数字书的销售序列化如下:

{
    "book": {
        "type": "Your\\App\\DigitalBook",
        "title": "Thinking Functionally in PHP",
        "bytes": 45000
    },
    "discountRate": 0.2
}

在反序列化时,将读取“type”属性,并使用它来确定剩余的值应该用来构建一个特定的 DigitalBook 实例。

类名映射的优势在于它们非常简单,并且可以与实现该接口的任何类一起工作,即使是你尚未考虑到的类。缺点是它们将 PHP 实现细节(类名)放入输出中,这可能不是你所希望的。

静态映射

静态映射允许你从类到有意义的键提供固定映射。

use Crell\Serde\Attributes\StaticTypeMap;

class Sale
{
    #[StaticTypeMap(key: 'type', map: [
        'paper' => Book::class,
        'ebook' => DigitalBook::class,
    ])]
    protected Book $book;

    protected float $discountRate;
}

现在,如果一个 Sale 对象被序列化,它将看起来像这样:

{
    "book": {
        "type": "ebook",
        "title": "Thinking Functionally in PHP",
        "bytes": 45000
    },
    "discountRate": 0.2
}

静态映射的优势在于简单性,并且不会将 PHP 特定的实现细节污染输出。缺点是它们是静态的:它们只能处理你代码时知道的类,如果遇到任何其他类,将抛出异常。

集合上的类型映射

类型映射也可以应用于数组属性,无论是序列还是字典。在这种情况下,它们将应用于该集合中的所有值。例如:

use Crell\Serde\Attributes as Serde;

class Order
{
    protected string $orderId;

    #[Serde\SequenceField(arrayType: Book::class)]
    #[Serde\StaticTypeMap(key: 'type', map: [
        'paper' => Book::class,
        'ebook' => DigitalBook::class,
    ])]
    protected array $books;
}

$products 是一个实现 Book 的对象数组,可以是 PaperBookDigitalBook。此对象的序列化副本可能如下所示:

{
    "orderId": "abc123",
    "products": [
        {
            "type": "ebook",
            "title": "Thinking Functionally in PHP",
            "bytes": 45000
        },
        {
            "type": "paper",
            "title": "Category Theory for Programmers",
            "pages": 335
        }
    ]
}

在反序列化时,将再次使用 type 属性来确定剩余属性应该被填充到的类。

类型映射类

除了在属性上放置类型映射外,你还可以将其放置在属性引用的类或接口上。

use Crell\Serde\Attributes\StaticTypeMap;

#[StaticTypeMap(key: 'type', map: [
    'paper' => Book::class,
    'ebook' => DigitalBook::class,
])]
interface Book {}

现在,该类型映射将应用于 Sale::$bookOrder::$books,而无需我们进行更多操作。

类型映射也是可继承的。这意味着如果我们想的话,可以在 Product 上放置类型映射

use Crell\Serde\Attributes\StaticTypeMap;

#[StaticTypeMap(key: 'type', map: [
    'paper' => Book::class,
    'ebook' => DigitalBook::class,
    'toy' => Gadget::class,
])]
interface Product {}

并且 SaleOrder 仍然会使用适当的键进行序列化。

动态类型映射

类型映射也可以在创建 Serde 对象时直接提供。任何实现 TypeMap 的对象都可以使用。这最有用的情况是基于用户配置、数据库值、应用中安装的插件等动态确定可能的类列表。

use Crell\Serde\TypeMap;

class ProductTypeMap implements TypeMap
{
    public function __construct(protected readonly Connection $db) {}

    public function keyField(): string
    {
        return 'type';
    }

    public function findClass(string $id): ?string
    {
        return $this->db->someLookup($id);
    }

    public function findIdentifier(string $class): ?string
    {
        return $this->db->someMappingLogic($class);
    }
}

$typeMap = new ProductTypeMap($dbConnection);

$serde = new SerdeCommon(typeMaps: [
    Your\App\Product::class => $typeMap,
]);

$json = $serde->serialize($aBook, to: 'json');

在实际操作中,你可能会通过依赖注入系统来设置它。

请注意,ClassNameTypeMapStaticTypeMap 也可以注入,以及其他实现 TypeMap 的任何类。

自定义类型映射

你也可以编写自己的类型映射作为属性。唯一的要求是

  1. 类实现了 TypeMap 接口。
  2. 类被标记为 #[\Attribute]
  3. 类在 两者 类和属性上都是合法的。也就是说,#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_PROPERTY)]

作用域

Serde 支持使用“作用域”来在不同的上下文中识别属性的版本。

任何属性(FieldTypeMapSequenceFieldDictionaryFieldPostLoad 等)都可以接受一个 scopes 参数,该参数接受一个字符串数组。如果指定,则该属性仅在序列化或反序列化在指定的作用域内时有效。如果没有指定作用域属性,则行为将回退到无作用域属性或省略属性。

例如,给定此类

class User
{
    private string $username;

    #[Field(exclude: true)]
    private string $password;

    #[Field(exclude: true)]
    #[Field(scope: 'admin')]
    private string $role;
}

如果你这样序列化它

$json = $serde->serialize($user, 'json');

它将产生这个 JSON 响应

{
    "username": "Larry"
}

这是因为,在无作用域请求中,$role 上的第一个 Field 被使用,它将其排除在输出之外。然而,如果你指定一个作用域

$json = $serde->serialize($user, 'json', scopes: ['admin']);

那么 $roleadmin 版本的 Field 将被使用,它不会被排除,并且得到这个结果

{
    "username": "Larry",
    "role": "Developer"
}

在使用作用域时,可能需要禁用自动属性包含并明确指定每个属性。例如

#[ClassSettings(includeFieldsByDefault: false)]
class Product
{
    #[Field]
    private int $id = 5;

    #[Field]
    #[Field(scopes: ['legacy'], serializedName: 'label')]
    private string $name = 'Fancy widget';

    #[Field(scopes: ['newsystem'])]
    private float $price = '9.99';

    #[Field(scopes: ['legacy'], serializedName: 'cost')]
    private float $legacyPrice = 9.99;

    #[Field(serializedName: 'desc')]
    private string $description = 'A fancy widget';

    private int $stock = 50;
}

如果没有指定范围进行序列化,将会得到这样的结果:

{
    "id": 5,
    "name": "Fancy widget",
    "desc": "A fancy widget"
}

因为这些是在没有指定范围时唯一“在范围”内的字段。

如果使用legacy范围进行序列化

{
    "id": 5,
    "label": "Fancy widget",
    "cost": 9.99,
    "desc": "A fancy widget"
}

在这种情况下,将使用特定范围的Field,它会改变序列化的名称。现在还包括了$legacyPrice属性,但已重命名为"cost"。

如果使用newsystem范围进行序列化

{
    "id": 5,
    "name": "Fancy widget",
    "price": "9.99",
    "desc": "A fancy widget"
}

在这种情况下,$name属性使用无范围版本的Field,因此不会被重命名。基于字符串的$price现在在范围内,但基于浮点的$legacyPrice不在范围内。注意,在这些情况下,都没有包括当前的$stock,因为它没有任何属性。

最后,还可以同时序列化多个范围。这是一个或操作,因此任何标记为任何指定范围的字段都将被包括在内。

$json = $serde->serialize($product, 'json', scopes: ['legacy', 'newsystem']);
{
    "id": 5,
    "name": "Fancy widget",
    "price": "9.99",
    "cost": 9.99,
    "desc": "A fancy widget"
}

注意,由于$name上的Field存在无范围和范围版本,范围版本将获胜,并将属性重命名。

如果对于指定范围可能适用多个属性变体,那么在范围中字典顺序第一个将优先于后面的,范围属性将优先于无范围属性。

注意,在反序列化时,指定范围将排除范围外的属性及其默认值。也就是说,它们不会被设置,即使是默认值,因此可能是“未初始化”的。这很少是期望的,因此可能更倾向于不指定范围进行反序列化,即使值是用范围序列化的。这取决于您的用例。

有关范围的更多信息,请参阅AttributeUtils文档。

使用#[PostLoad]进行验证

需要注意的是,在反序列化时,__construct()根本不会被调用。这意味着构造函数中存在的任何验证在反序列化时都不会运行。

相反,Serde将查找任何带有#[\Crell\Serde\Attributes\PostLoad]属性的任何方法或方法。该属性除了范围外不接受任何参数。在对象被填充后,任何PostLoad方法将按字典顺序调用,不传递任何参数。此功能的主要用例是验证,在这种情况下,如果填充的数据以某种方式无效,则方法应该抛出异常。(例如,某些整数必须是正数。)

方法的可见性无关紧要。Serde将以相同的方式调用publicprivateprotected方法。请注意,然而,在反序列化的类的父类中的private方法将不会调用,因为它从该作用域中不可访问。

扩展Serde

内部,Serde有六种类型的扩展共同工作,以生成序列化或反序列化的产品。

  • 如上所述,类型映射是可选的,它将类名转换为查找标识符及其反向。
  • Exporter负责从对象中提取值,如有必要,对其进行处理,然后将它们传递给格式化器。这是序列化管道的一部分。
  • Importer负责使用格式化器从传入的数据中提取数据,然后将它们翻译成必要的格式,以便写入对象。这是反序列化管道的一部分。
  • Formatter 负责将数据写入特定的输出格式,例如 JSON 或 YAML。这是序列化管道的一部分。
  • Deformatter 负责从传入的格式读取数据并将其传递给 Importer。这是反序列化管道的一部分。
  • TypeField 是可以添加到属性中的自定义类型字段,可以为相应的导出器或导入器提供更多类型特定的逻辑。在某种程度上,它们是导出器或导入器的“额外参数”,如果您实现了一个自定义的 TypeField,您几乎肯定需要实现自己的导出器或导入器。 DateTimeFieldDictionaryFieldSequenceField 是类型字段的示例。类型字段也是可传递的,因此可以将它们放在自定义类中,以便在任何使用该类的属性上应用(除非在本地覆盖)。

总而言之,ImporterExporter 实例被称为“处理器”。

通常,ImporterExporterPHP 类型特定的,而 FormatterDeformatter序列化格式特定的。自定义导入器和导出器也可以声明自己为格式特定,如果它们包含对格式敏感的优化。

ImporterExporter 可以在同一个对象上实现,也可以不实现。同样,FormatterDeformatter 可以一起实现,也可以不实现。这取决于特定实现的便利性,提供的扩展根据使用情况做了一些工作。

上述链接的接口提供了如何使用它们的更精确说明。在大多数情况下,您只需要实现一个格式化器或反格式化器来支持新的格式。您只需要在处理需要额外特殊处理的特定类时实现导入器或导出器,例如,其序列化表示与其实例表示几乎没有关系。

以下是一些处理常见情况的自定义处理器的示例。

  • DateTimeExporter:此对象将 DateTimeDateTimeImmutable 对象转换成字符串形式的序列化表示,反之亦然。具体来说,在序列化时,它将使用 \DateTimeInterface::RFC3339_EXTENDED 格式作为字符串。时间戳将作为普通字符串出现在序列化输出中。在反序列化时,它将接受 DateTime 构造函数支持的任何日期时间格式。
  • DateTimeZoneExporter:此对象将 DateTimeZone 对象转换成时区字符串形式的序列化表示,反之亦然。即 DateTimeZone('America/Chicago') 将表示为字符串 America/Chicago`。
  • NativeSerializeExporter:此对象适用于任何具有 __serialize() 方法(序列化时)或 __unserialize() 方法(反序列化时)的类。这些 PHP 魔术方法提供对象的替代表示形式,用于与 PHP 的原生 serialize()unserialize() 方法一起使用,但也可以用于任何其他格式。如果定义了 __serialize(),则将调用它,并将其返回的关联数组写入所选格式作为字典。如果定义了 __unserialize(),则此对象将从传入数据中读取字典,然后将它传递给新创建的对象上的该方法,然后由该方法负责以适当的方式填充对象。在任一方向上都不会进行进一步处理。
  • EnumOnArrayImporter:Serde 原生支持 PHP 枚举,可以根据需要将它们序列化为整数或字符串。然而,在从 PHP 数组格式读取的特殊情况下,此对象将接管并支持读取传入数据中的枚举字面量。这允许,例如,配置数组包含手动插入的枚举值,并且仍然可以干净地导入到已定义的对象中。

架构图

序列化大致如下

反序列化看起来非常相似

在这两种情况下,请注意,几乎所有行为都是由一个独特的序列化/反序列化对象控制的,而不是由 Serde 本身控制。Serde 本身只是一个配置运行时对象上下文的包装器。

依赖注入配置

Serde 设计为可以“即插即用”,无需任何额外设置。然而,当将其包含在更大的系统中时,最好通过依赖注入正确配置它。

您可以通过三种方式设置 Serde。

  1. SerdeCommon 类包含大多数可用的处理程序和格式化程序,开箱即用,尽管您也可以通过构造函数添加额外的处理程序。
  2. SerdeBasic 类没有任何预配置;您需要自己提供所有所需的处理程序、格式化程序或类型映射,按照您希望它们应用的顺序。
  3. 您还可以扩展 Serde 的基类本身,并创建自己的自定义预配置,只包含您想要的处理程序或格式化程序(提供的或自定义)。

SerdeCommonSerdeBasic 都接受四个参数:要使用的 ClassAnalyzer、处理程序数组、格式化程序数组和类型映射数组。如果没有提供分析器,Serde 将默认创建一个内存缓存的分析器,以确保它始终可以工作。然而,在 DI 配置中,强烈建议您自己配置分析器,并带有适当的缓存,然后将它注入到 Serde 中作为依赖项,以避免重复的分析器(和重复的缓存)。如果您在不同服务中有多个不同的 Serde 配置,也可能有益于将所有处理程序和格式化程序也作为服务,并显式地将它们注入到 SerdeBasic 中,而不是依赖于 SerdeCommon

变更日志

请参阅 CHANGELOG 以获取有关最近更改的更多信息。

测试

$ composer test

贡献

请参阅 CONTRIBUTINGCODE_OF_CONDUCT 以获取详细信息。

安全性

如果您发现任何安全相关的问题,请使用 GitHub 安全报告表单 而不是问题队列。

鸣谢

本库的初始开发得到了TYPO3 GmbH的赞助。

许可证

较轻的GPL版本3或更高版本。请参阅许可证文件获取更多信息。