跳到内容

如何使用序列化器

编辑此页

Symfony 提供了一个序列化器,用于将数据结构从一种格式转换为 PHP 对象,反之亦然。

这在构建 API 或与第三方 API 通信时最常用。序列化器可以将传入的 JSON 请求负载转换为应用程序使用的 PHP 对象。然后,在生成响应时,你可以使用序列化器将 PHP 对象转换回 JSON 响应。

它也可以用于例如将 CSV 配置文件数据加载为 PHP 对象,甚至在格式之间进行转换(例如 YAML 到 XML)。

安装

在使用 Symfony Flex 的应用程序中,在使用序列化器之前,运行此命令来安装序列化器 Symfony 包

1
$ composer require symfony/serializer-pack

注意

序列化器包还安装了 Serializer 组件的一些常用可选依赖项。当在 Symfony 框架之外使用此组件时,你可能需要从 symfony/serializer 包开始,并在需要时安装可选依赖项。

参见

Symfony Serializer 组件的一个流行的替代方案是第三方库 JMS serializer

序列化对象

对于此示例,假设你的项目中存在以下类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// src/Model/Person.php
namespace App\Model;

class Person
{
    public function __construct(
        private int $age,
        private string $name,
        private bool $sportsperson
    ) {
    }

    public function getAge(): int
    {
        return $this->age;
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function isSportsperson(): bool
    {
        return $this->sportsperson;
    }
}

如果你想将此类型的对象转换为 JSON 结构(例如,通过 API 响应发送它们),请通过使用 SerializerInterface 参数类型来获取 serializer 服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// src/Controller/PersonController.php
namespace App\Controller;

use App\Model\Person;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Serializer\SerializerInterface;

class PersonController extends AbstractController
{
    public function index(SerializerInterface $serializer): Response
    {
        $person = new Person('Jane Doe', 39, false);

        $jsonContent = $serializer->serialize($person, 'json');
        // $jsonContent contains {"name":"Jane Doe","age":39,"sportsperson":false}

        return JsonResponse::fromJsonString($jsonContent);
    }
}

serialize() 的第一个参数是要序列化的对象,第二个参数用于选择合适的编码器(即格式),在本例中是 JsonEncoder

提示

当你的控制器类扩展了 AbstractController(如上面的示例所示),你可以通过使用 json() 方法,使用序列化器从对象创建一个 JSON 响应,从而简化你的控制器

1
2
3
4
5
6
7
8
9
10
class PersonController extends AbstractController
{
    public function index(): Response
    {
        $person = new Person('Jane Doe', 39, false);

        // when the Serializer is not available, this will use json_encode()
        return $this->json($person);
    }
}

在 Twig 模板中使用序列化器

你也可以在任何 Twig 模板中使用 serialize 过滤器来序列化对象

1
{{ person|serialize(format = 'json') }}

有关更多信息,请参阅 twig 参考

反序列化对象

API 通常还需要将格式化的请求体(例如 JSON)转换为 PHP 对象。此过程称为反序列化(也称为“hydration”)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src/Controller/PersonController.php
namespace App\Controller;

// ...
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
use Symfony\Component\HttpFoundation\Request;

class PersonController extends AbstractController
{
    // ...

    public function create(Request $request, SerializerInterface $serializer): Response
    {
        if ('json' !== $request->getContentTypeFormat()) {
            throw new BadRequestException('Unsupported content format');
        }

        $jsonData = $request->getContent();
        $person = $serializer->deserialize($jsonData, Person::class, 'json');

        // ... do something with $person and return a response
    }
}

在这种情况下,deserialize() 需要三个参数

  1. 要解码的数据
  2. 此信息将被解码成的类的名称
  3. 用于将数据转换为数组的编码器的名称(即输入格式)

当向此控制器发送请求(例如 {"first_name":"John Doe","age":54,"sportsperson":true})时,序列化器将创建一个新的 Person 实例,并将属性设置为给定 JSON 中的值。

注意

默认情况下,未映射到反标准化对象的其他属性将被 Serializer 组件忽略。例如,如果对上述控制器的请求包含 {..., "city": "Paris"},则 city 字段将被忽略。你也可以使用 序列化器上下文(稍后你将学习到)在这些情况下抛出异常。

参见

你还可以将数据反序列化到现有的对象实例中(例如,在更新数据时)。请参阅 在现有对象中反序列化

序列化过程:标准化器和编码器

序列化器在(反)序列化对象时使用两步过程

在两个方向上,数据总是首先转换为数组。这会将过程分为两个独立的职责:

标准化器
这些类将对象转换为数组,反之亦然。它们负责繁重的工作,即找出要序列化的类属性、它们持有的值以及它们应该具有的名称。
编码器
编码器将数组转换为特定的格式,反之亦然。每个编码器都确切地知道如何解析和生成特定格式,例如 JSON 或 XML。

在内部,Serializer 类在(反)序列化对象时使用排序的标准化器列表和一个用于特定格式的编码器。

默认 serializer 服务中配置了几个标准化器。最重要的标准化器是 ObjectNormalizer。此标准化器使用反射和 PropertyAccess 组件 在任何对象和数组之间进行转换。你将在稍后了解更多关于 此标准化器和其他标准化器 的信息。

默认序列化器还配置了一些编码器,涵盖了 HTTP 应用程序常用的格式:

序列化器编码器 中阅读更多关于这些编码器及其配置的信息。

提示

API Platform 项目为更多高级格式提供了编码器:

序列化器上下文

序列化器及其标准化器和编码器通过序列化器上下文进行配置。此上下文可以在多个位置配置:

你可以同时使用所有三个选项。当在多个位置配置相同的设置时,上面列表中的后者将覆盖前一个(例如,特定属性上的设置将覆盖全局配置的设置)。

配置默认上下文

你可以在框架配置中配置默认上下文,例如在反序列化时禁止额外的字段:

1
2
3
4
5
# config/packages/serializer.yaml
framework:
    serializer:
        default_context:
            allow_extra_attributes: false

在序列化/反序列化时传递上下文

你也可以为单个 serialize()/deserialize() 调用配置上下文。例如,你可以仅为一个序列化调用跳过具有 null 值的属性:

1
2
3
4
5
6
7
8
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;

// ...
$serializer->serialize($person, 'json', [
    AbstractObjectNormalizer::SKIP_NULL_VALUES => true
]);

// next calls to serialize() will NOT skip null values
使用上下文构建器

你可以使用“上下文构建器”来帮助定义(反)序列化上下文。上下文构建器是 PHP 对象,它们提供上下文选项的自动完成、验证和文档。

1
2
3
4
5
use Symfony\Component\Serializer\Context\Normalizer\DateTimeNormalizerContextBuilder;

$contextBuilder = (new DateTimeNormalizerContextBuilder())
    ->withFormat('Y-m-d H:i:s');
$serializer->serialize($something, 'json', $contextBuilder->toArray());

每个标准化器/编码器都有其相关的上下文构建器。要创建更复杂的(反)序列化上下文,你可以使用 withContext() 方法将它们链接起来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use Symfony\Component\Serializer\Context\Encoder\CsvEncoderContextBuilder;
use Symfony\Component\Serializer\Context\Normalizer\ObjectNormalizerContextBuilder;

$initialContext = [
    'custom_key' => 'custom_value',
];

$contextBuilder = (new ObjectNormalizerContextBuilder())
    ->withContext($initialContext)
    ->withGroups(['group1', 'group2']);

$contextBuilder = (new CsvEncoderContextBuilder())
    ->withContext($contextBuilder)
    ->withDelimiter(';');

$serializer->serialize($something, 'csv', $contextBuilder->toArray());

参见

你还可以 创建你的上下文构建器,以便为你的自定义上下文值提供自动完成、验证和文档。

在特定属性上配置上下文

最后,你还可以在特定对象属性上配置上下文值。例如,配置日期时间格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
// src/Model/Person.php

// ...
use Symfony\Component\Serializer\Attribute\Context;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;

class Person
{
    #[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
    public \DateTimeImmutable $createdAt;

    // ...
}

注意

当使用 YAML 或 XML 时,映射文件必须放置在以下位置之一:

  • config/serializer/ 目录中的所有 *.yaml*.xml 文件。
  • bundle 的 Resources/config/ 目录中的 serialization.yamlserialization.xml 文件;
  • bundle 的 Resources/config/serialization/ 目录中的所有 *.yaml*.xml 文件。

你还可以指定特定于标准化或反标准化的上下文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// src/Model/Person.php

// ...
use Symfony\Component\Serializer\Attribute\Context;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;

class Person
{
    #[Context(
        normalizationContext: [DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'],
        denormalizationContext: [DateTimeNormalizer::FORMAT_KEY => \DateTime::RFC3339],
    )]
    public \DateTimeImmutable $createdAt;

    // ...
}

你还可以将上下文的使用限制为某些

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/Model/Person.php

// ...
use Symfony\Component\Serializer\Attribute\Context;
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;

class Person
{
    #[Groups(['extended'])]
    #[Context([DateTimeNormalizer::FORMAT_KEY => \DateTime::RFC3339])]
    #[Context(
        context: [DateTimeNormalizer::FORMAT_KEY => \DateTime::RFC3339_EXTENDED],
        groups: ['extended'],
    )]
    public \DateTimeImmutable $createdAt;

    // ...
}

该属性可以根据需要在单个属性上重复多次。没有组的上下文始终首先应用。然后,匹配组的上下文按照提供的顺序合并。

如果你在多个属性中重复相同的上下文,请考虑在你的类上使用 #[Context] 属性,以将该上下文配置应用于该类的所有属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
namespace App\Model;

use Symfony\Component\Serializer\Attribute\Context;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;

#[Context([DateTimeNormalizer::FORMAT_KEY => \DateTime::RFC3339])]
#[Context(
    context: [DateTimeNormalizer::FORMAT_KEY => \DateTime::RFC3339_EXTENDED],
    groups: ['extended'],
)]
class Person
{
    // ...
}

序列化为或从 PHP 数组

默认的 Serializer 也可以仅执行 两步序列化过程 中的一步,方法是使用相应的接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
use Symfony\Component\Serializer\Encoder\DecoderInterface;
use Symfony\Component\Serializer\Encoder\EncoderInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
// ...

class PersonController extends AbstractController
{
    public function index(DenormalizerInterface&NormalizerInterface $serializer): Response
    {
        $person = new Person('Jane Doe', 39, false);

        // use normalize() to convert a PHP object to an array
        $personArray = $serializer->normalize($person, 'json');

        // ...and denormalize() to convert an array back to a PHP object
        $personCopy = $serializer->denormalize($personArray, Person::class);

        // ...
    }

    public function json(DecoderInterface&EncoderInterface $serializer): Response
    {
        $data = ['name' => 'Jane Doe'];

        // use encode() to transform PHP arrays into another format
        $json = $serializer->encode($data, 'json');

        // ...and decode() to transform any format to just PHP arrays (instead of objects)
        $data = $serializer->decode('{"name":"Charlie Doe"}', 'json');
        // $data contains ['name' => 'Charlie Doe']
    }
}

忽略属性

ObjectNormalizer 标准化器标准化对象的所有属性以及所有以 get*()has*()is*()can*() 开头的方法。有些属性或方法永远不应该被序列化。你可以使用 #[Ignore] 属性排除它们:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/Model/Person.php
namespace App\Model;

use Symfony\Component\Serializer\Attribute\Ignore;

class Person
{
    // ...

    #[Ignore]
    public function isPotentiallySpamUser(): bool
    {
        // ...
    }
}

potentiallySpamUser 属性现在永远不会被序列化。

1
2
3
4
5
6
7
8
9
10
11
12
13
use App\Model\Person;

// ...
$person = new Person('Jane Doe', 32, false);
$json = $serializer->serialize($person, 'json');
// $json contains {"name":"Jane Doe","age":32,"sportsperson":false}

$person1 = $serializer->deserialize(
    '{"name":"Jane Doe","age":32,"sportsperson":false","potentiallySpamUser":false}',
    Person::class,
    'json'
);
// the "potentiallySpamUser" value is ignored

使用上下文忽略属性

你还可以使用 ignored_attributes 上下文选项在运行时传递要忽略的属性名称数组:

1
2
3
4
5
6
7
8
9
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;

// ...
$person = new Person('Jane Doe', 32, false);
$json = $serializer->serialize($person, 'json',
[
    AbstractNormalizer::IGNORED_ATTRIBUTES => ['age'],
]);
// $json contains {"name":"Jane Doe","sportsperson":false}

但是,如果过度使用,这可能会很快变得难以维护。有关更好的解决方案,请参阅下一节关于序列化组的内容。

选择特定属性

你可能需要在一种情况下排除某些属性,但在另一种情况下序列化它们,而不是在所有情况下都排除属性或方法。组是实现此目的的便捷方法。

你可以将 #[Groups] 属性添加到你的类中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/Model/Person.php
namespace App\Model;

use Symfony\Component\Serializer\Attribute\Groups;

class Person
{
    #[Groups(["admin-view"])]
    private int $age;

    #[Groups(["public-view"])]
    private string $name;

    #[Groups(["public-view"])]
    private bool $sportsperson;

    // ...
}

你现在可以选择在序列化时使用哪些组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$json = $serializer->serialize(
    $person,
    'json',
    ['groups' => 'public-view']
);
// $json contains {"name":"Jane Doe","sportsperson":false}

// you can also pass an array of groups
$json = $serializer->serialize(
    $person,
    'json',
    ['groups' => ['public-view', 'admin-view']]
);
// $json contains {"name":"Jane Doe","age":32,"sportsperson":false}

// or use the special "*" value to select all groups
$json = $serializer->serialize(
    $person,
    'json',
    ['groups' => '*']
);
// $json contains {"name":"Jane Doe","age":32,"sportsperson":false}

使用序列化上下文

最后,你还可以使用 attributes 上下文选项在运行时选择属性:

1
2
3
4
5
6
7
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
// ...

$json = $serializer->serialize($person, 'json', [
    AbstractNormalizer::ATTRIBUTES => ['name', 'company' => ['name']]
]);
// $json contains {"name":"Dunglas","company":{"name":"Les-Tilleuls.coop"}}

只有 未被忽略 的属性才可用。如果设置了序列化组,则只能使用这些组允许的属性。

处理数组

序列化器能够处理对象数组。序列化数组就像序列化单个对象一样:

1
2
3
4
5
6
7
8
9
10
use App\Model\Person;

// ...
$person1 = new Person('Jane Doe', 39, false);
$person2 = new Person('John Smith', 52, true);

$persons = [$person1, $person2];
$JsonContent = $serializer->serialize($persons, 'json');

// $jsonContent contains [{"name":"Jane Doe","age":39,"sportsman":false},{"name":"John Smith","age":52,"sportsman":true}]

要反序列化对象列表,你必须将 [] 附加到类型参数:

1
2
3
4
// ...

$jsonData = ...; // the serialized JSON data from the previous example
$persons = $serializer->deserialize($JsonData, Person::class.'[]', 'json');

对于嵌套类,你必须向属性、构造函数或 setter 添加 PHPDoc 类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// src/Model/UserGroup.php
namespace App\Model;

class UserGroup
{
    /**
     * @param Person[] $members
     */
    public function __construct(
        private array $members,
    ) {
    }

    // or if you're using a setter

    /**
     * @param Person[] $members
     */
    public function setMembers(array $members): void
    {
        $this->members = $members;
    }

    // ...
}

提示

序列化器还支持静态分析中使用的数组类型,例如 list<Person>array<Person>。确保安装了 phpstan/phpdoc-parserphpdocumentor/reflection-docblock 包(这些包是 symfony/serializer-pack 的一部分)。

反序列化嵌套结构

某些 API 可能会提供冗长的嵌套结构,你希望在 PHP 对象中将其展平。例如,想象一下这样的 JSON 响应:

1
2
3
4
5
6
7
8
9
{
    "id": "123",
    "profile": {
        "username": "jdoe",
        "personal_information": {
            "full_name": "Jane Doe"
        }
    }
}

你可能希望将此信息序列化为单个 PHP 对象,例如:

1
2
3
4
5
6
class Person
{
    private int $id;
    private string $username;
    private string $fullName;
}

使用 #[SerializedPath] 使用 有效的 PropertyAccess 语法 指定嵌套属性的路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
namespace App\Model;

use Symfony\Component\Serializer\Attribute\SerializedPath;

class Person
{
    private int $id;

    #[SerializedPath('[profile][username]')]
    private string $username;

    #[SerializedPath('[profile][personal_information][full_name]')]
    private string $fullName;
}

警告

SerializedPath 不能与同一属性的 SerializedName 组合使用。

#[SerializedPath] 属性也适用于 PHP 对象的序列化:

1
2
3
4
5
6
use App\Model\Person;
// ...

$person = new Person(123, 'jdoe', 'Jane Doe');
$jsonContent = $serializer->serialize($person, 'json');
// $jsonContent contains {"id":123,"profile":{"username":"jdoe","personal_information":{"full_name":"Jane Doe"}}}

序列化和反序列化时转换属性名称

有时,序列化属性的名称必须与 PHP 类的属性或 getter/setter 方法不同。这可以使用名称转换器来实现。

序列化器服务使用 MetadataAwareNameConverter。使用此名称转换器,你可以使用 #[SerializedName] 属性更改属性的名称:

1
2
3
4
5
6
7
8
9
10
11
12
// src/Model/Person.php
namespace App\Model;

use Symfony\Component\Serializer\Attribute\SerializedName;

class Person
{
    #[SerializedName('customer_name')]
    private string $name;

    // ...
}

此自定义映射用于在序列化和反序列化对象时转换属性名称。

1
2
3
4
// ...

$json = $serializer->serialize($person, 'json');
// $json contains {"customer_name":"Jane Doe", ...}

参见

你还可以创建自定义名称转换器类。在 如何创建你的自定义名称转换器 中阅读更多相关信息。

CamelCase 到 snake_case

在许多格式中,通常使用下划线来分隔单词(也称为 snake_case)。但是,在 Symfony 应用程序中,通常使用 camelCase 来命名属性。

Symfony 提供了一个内置的名称转换器,旨在在序列化和反序列化过程中在 snake_case 和 CamelCased 样式之间进行转换。你可以通过将 name_converter 设置设置为 serializer.name_converter.camel_case_to_snake_case 来使用它,而不是元数据感知名称转换器:

1
2
3
4
# config/packages/serializer.yaml
framework:
    serializer:
        name_converter: 'serializer.name_converter.camel_case_to_snake_case'

序列化器标准化器

默认情况下,序列化器服务配置了以下标准化器(按优先级顺序排列):

UnwrappingDenormalizer
可以用于仅反标准化输入的一部分,请阅读本文稍后的 部分,了解更多信息。
ProblemNormalizer
根据 API Problem 规范 RFC 7807 标准化 FlattenException 错误。
UidNormalizer

标准化扩展 AbstractUid 的对象。

实现 Uuid 的对象的默认标准化格式是 RFC 4122 格式(示例:d9e7a184-5d5b-11ea-a62a-3499710062d0)。实现 Ulid 的对象的默认标准化格式是 Base 32 格式(示例:01E439TP9XJZ9RPFH3T1PYBCR8)。你可以通过将序列化器上下文选项 UidNormalizer::NORMALIZATION_FORMAT_KEY 设置为 UidNormalizer::NORMALIZATION_FORMAT_BASE_58UidNormalizer::NORMALIZATION_FORMAT_BASE_32UidNormalizer::NORMALIZATION_FORMAT_RFC_4122 来更改字符串格式。

它还可以将 uuidulid 字符串反标准化为 UuidUlid。格式无关紧要。

DateTimeNormalizer

这在 DateTimeInterface 对象(例如 DateTimeDateTimeImmutable)与字符串、整数或浮点数之间进行标准化。

DateTimeDateTimeImmutable) 转换为字符串、整数或浮点数。默认情况下,它使用 RFC 3339 格式将它们转换为字符串。使用 DateTimeNormalizer::FORMAT_KEYDateTimeNormalizer::TIMEZONE_KEY 来更改格式。

要将对象转换为整数或浮点数,请将序列化器上下文选项 DateTimeNormalizer::CAST_KEY 设置为 intfloat

7.1

DateTimeNormalizer::CAST_KEY 上下文选项在 Symfony 7.1 中引入。

ConstraintViolationListNormalizer
此规范化器根据 RFC 7807 标准,将实现 ConstraintViolationListInterface 的对象转换为错误列表。
DateTimeZoneNormalizer
此规范化器在 DateTimeZone 对象和表示时区名称的字符串之间进行转换,时区名称根据 PHP 时区列表
DateIntervalNormalizer
此规范化器在 DateInterval 对象和字符串之间进行规范化。默认情况下,使用 P%yY%mM%dDT%hH%iM%sS 格式。使用 DateIntervalNormalizer::FORMAT_KEY 选项可以更改此设置。
FormErrorNormalizer

此规范化器适用于实现 FormInterface 的类。

它将从表单中获取错误,并根据 API Problem 规范 RFC 7807 对其进行规范化。

TranslatableNormalizer

此规范化器使用 translator 将实现 TranslatableInterface 的对象转换为已翻译的字符串。

您可以通过设置 TranslatableNormalizer::NORMALIZATION_LOCALE_KEY 上下文选项来定义用于翻译对象的区域设置。

BackedEnumNormalizer

此规范化器在 BackedEnum 枚举和字符串或整数之间进行转换。

默认情况下,当数据不是有效的后备枚举时,会抛出异常。如果您想要 null 作为替代,您可以设置 BackedEnumNormalizer::ALLOW_INVALID_VALUES 选项。

DataUriNormalizer
此规范化器在 SplFileInfo 对象和 data URI 字符串 (data:...) 之间进行转换,以便可以将文件嵌入到序列化数据中。
JsonSerializableNormalizer

此规范化器适用于实现 JsonSerializable 的类。

它将调用 JsonSerializable::jsonSerialize() 方法,然后进一步规范化结果。这意味着嵌套的 JsonSerializable 类也将被规范化。

当您想要从使用简单 json_encode 的现有代码库逐步迁移到 Symfony Serializer 时,此规范化器特别有用,因为它允许您混合使用哪些规范化器用于哪些类。

json_encode 不同,可以处理循环引用。

ArrayDenormalizer
此反规范化器将数组的数组转换为对象数组(具有给定的类型)。请参阅 处理数组
ObjectNormalizer

这是功能最强大的默认规范化器,用于其他规范化器无法规范化的任何对象。

它利用 PropertyAccess 组件 来读取和写入对象。这使其可以直接或使用 getter、setter、hasser、isser、canner、adder 和 remover 访问属性。名称的生成方式是从方法名称中删除 getsethasisaddremove 前缀,并将首字母转换为小写(例如 getFirstName() -> firstName)。

在反规范化期间,它支持使用构造函数以及发现的方法。

危险

在序列化 DateTimeDateTimeImmutable 类时,请始终确保注册 DateTimeNormalizer,以避免过度使用内存和暴露内部细节。

内置标准化器

除了默认注册的规范化器(请参阅上一节),序列化器组件还提供了一些额外的规范化器。您可以通过定义服务并使用 serializer.normalizer 标记它来注册这些规范化器。例如,要使用 CustomNormalizer,您必须定义一个类似这样的服务

1
2
3
4
5
6
7
8
9
# config/services.yaml
services:
    # ...

    # if you're using autoconfigure, the tag will be automatically applied
    Symfony\Component\Serializer\Normalizer\CustomNormalizer:
        tags:
            # register the normalizer with a high priority (called earlier)
            - { name: 'serializer.normalizer', priority: 500 }
CustomNormalizer
此规范化器在规范化时调用 PHP 对象上的方法。PHP 对象必须实现 NormalizableInterface 和/或 DenormalizableInterface
GetSetMethodNormalizer

此规范化器是默认 ObjectNormalizer 的替代方案。它通过调用“getter”(以 gethasiscan 开头的公共方法)来读取类的内容。它将通过调用构造函数和“setter”(以 set 开头的公共方法)来反规范化数据。

对象被规范化为属性名称和值的映射(名称的生成方式是从方法名称中删除 get 前缀,并将首字母转换为小写;例如 getFirstName() -> firstName)。

PropertyNormalizer

这是 ObjectNormalizer 的又一个替代方案。此规范化器通过使用 PHP 反射 直接读取和写入公共属性以及私有和受保护的属性(来自类及其所有父类)。它支持在反规范化过程中调用构造函数。

对象被规范化为属性名称到属性值的映射。

您还可以使用 PropertyNormalizer::NORMALIZE_VISIBILITY 上下文选项将规范化器限制为仅使用具有特定可见性的属性(例如,仅公共属性)。您可以将其设置为 PropertyNormalizer::NORMALIZE_PUBLICPropertyNormalizer::NORMALIZE_PROTECTEDPropertyNormalizer::NORMALIZE_PRIVATE 常量的任意组合

1
2
3
4
5
6
7
8
9
10
use Symfony\Component\Serializer\Normalizer\PropertyNormalizer;
// ...

$json = $serializer->serialize($person, 'json', [
    // only serialize public properties
    PropertyNormalizer::NORMALIZE_VISIBILITY => PropertyNormalizer::NORMALIZE_PUBLIC,

    // serialize public and protected properties
    PropertyNormalizer::NORMALIZE_VISIBILITY => PropertyNormalizer::NORMALIZE_PUBLIC | PropertyNormalizer::NORMALIZE_PROTECTED,
]);

调试序列化器

使用 debug:serializer 命令转储给定类的序列化器元数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
$ php bin/console debug:serializer 'App\Entity\Book'

    App\Entity\Book
    ---------------

    +----------+------------------------------------------------------------+
    | Property | Options                                                    |
    +----------+------------------------------------------------------------+
    | name     | [                                                          |
    |          |   "groups" => [                                            |
    |          |       "book:read",                                         |
    |          |       "book:write",                                        |
    |          |   ],                                                       |
    |          |   "maxDepth" => 1,                                         |
    |          |   "serializedName" => "book_name",                         |
    |          |   "serializedPath" => null,                                |
    |          |   "ignore" => false,                                       |
    |          |   "normalizationContexts" => [],                           |
    |          |   "denormalizationContexts" => []                          |
    |          | ]                                                          |
    | isbn     | [                                                          |
    |          |   "groups" => [                                            |
    |          |       "book:read",                                         |
    |          |   ],                                                       |
    |          |   "maxDepth" => null,                                      |
    |          |   "serializedName" => null,                                |
    |          |   "serializedPath" => "[data][isbn]",                      |
    |          |   "ignore" => false,                                       |
    |          |   "normalizationContexts" => [],                           |
    |          |   "denormalizationContexts" => []                          |
    |          | ]                                                          |
    +----------+------------------------------------------------------------+

高级序列化

跳过 null

默认情况下,Serializer 将保留包含 null 值的属性。您可以通过将 AbstractObjectNormalizer::SKIP_NULL_VALUES 上下文选项设置为 true 来更改此行为

1
2
3
4
5
6
7
8
9
10
class Person
{
    public string $name = 'Jane Doe';
    public ?string $gender = null;
}

$jsonContent = $serializer->serialize(new Person(), 'json', [
    AbstractObjectNormalizer::SKIP_NULL_VALUES => true,
]);
// $jsonContent contains {"name":"Jane Doe"}

处理未初始化的属性

在 PHP 中,类型化属性具有 uninitialized 状态,这与未类型化属性的默认 null 不同。当您尝试在给定显式值之前访问类型化属性时,会收到错误。

为了避免序列化器在序列化或规范化具有未初始化属性的对象时抛出错误,默认情况下 ObjectNormalizer 会捕获这些错误并忽略此类属性。

您可以通过将 AbstractObjectNormalizer::SKIP_UNINITIALIZED_VALUES 上下文选项设置为 false 来禁用此行为

1
2
3
4
5
6
7
8
9
10
class Person {
    public string $name = 'Jane Doe';
    public string $phoneNumber; // uninitialized
}

$jsonContent = $normalizer->serialize(new Dummy(), 'json', [
    AbstractObjectNormalizer::SKIP_UNINITIALIZED_VALUES => false,
]);
// throws Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException
// as the ObjectNormalizer cannot read uninitialized properties

注意

如果给定对象具有未初始化的属性,则将 AbstractObjectNormalizer::SKIP_UNINITIALIZED_VALUES 上下文选项设置为 false 来使用 PropertyNormalizerGetSetMethodNormalizer 将抛出 \Error 实例,因为规范化器无法读取它们(直接或通过 getter/isser 方法)。

处理循环引用

循环引用在使用关联对象时很常见

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class Organization
{
    public function __construct(
        private string $name,
        private array $members = []
    ) {
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function addMember(Member $member): void
    {
        $this->members[] = $member;
    }

    public function getMembers(): array
    {
        return $this->members;
    }
}

class Member
{
    private Organization $organization;

    public function __construct(
        private string $name
    ) {
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function setOrganization(Organization $organization): void
    {
        $this->organization = $organization;
    }

    public function getOrganization(): Organization
    {
        return $this->organization;
    }
}

为了避免无限循环,当遇到这种情况时,规范化器会抛出 CircularReferenceException

1
2
3
4
5
6
7
8
$organization = new Organization('Les-Tilleuls.coop');
$member = new Member('Kévin');

$organization->addMember($member);
$member->setOrganization($organization);

$jsonContent = $serializer->serialize($organization, 'json');
// throws a CircularReferenceException

上下文中的键 circular_reference_limit 设置在将其视为循环引用之前序列化同一对象的次数。默认值为 1

除了抛出异常之外,循环引用也可以通过自定义可调用对象来处理。这在序列化具有唯一标识符的实体时尤其有用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use Symfony\Component\Serializer\Exception\CircularReferenceException;

$context = [
    AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER => function (object $object, ?string $format, array $context): string {
        if (!$object instanceof Organization) {
            throw new CircularReferenceException('A circular reference has been detected when serializing the object of class "'.get_debug_type($object).'".');
        }

        // serialize the nested Organization with only the name (and not the members)
        return $object->getName();
    },
];

$jsonContent = $serializer->serialize($organization, 'json', $context);
// $jsonContent contains {"name":"Les-Tilleuls.coop","members":[{"name":"K\u00e9vin", organization: "Les-Tilleuls.coop"}]}

处理序列化深度

序列化器还可以检测同一类的嵌套对象并限制序列化深度。这对于树结构很有用,在树结构中,同一对象被嵌套多次。

例如,假设一个家庭树的数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// ...
class Person
{
    // ...

    public function __construct(
        private string $name,
        private ?self $mother
    ) {
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function getMother(): ?self
    {
        return $this->mother;
    }

    // ...
}

// ...
$greatGrandmother = new Person('Elizabeth', null);
$grandmother = new Person('Jane', $greatGrandmother);
$mother = new Person('Sophie', $grandmother);
$child = new Person('Joe', $mother);

您可以为给定的属性指定最大深度。例如,您可以将最大深度设置为 1,以便始终仅序列化某人的母亲(而不是他们的祖母等)

1
2
3
4
5
6
7
8
9
10
11
12
// src/Model/Person.php
namespace App\Model;

use Symfony\Component\Serializer\Attribute\MaxDepth;

class Person
{
    #[MaxDepth(1)]
    private ?self $mother;

    // ...
}

要限制序列化深度,您必须在上下文(或 framework.yaml 中指定的默认上下文)中将 AbstractObjectNormalizer::ENABLE_MAX_DEPTH 键设置为 true

1
2
3
4
5
6
7
8
9
10
// ...
$greatGrandmother = new Person('Elizabeth', null);
$grandmother = new Person('Jane', $greatGrandmother);
$mother = new Person('Sophie', $grandmother);
$child = new Person('Joe', $mother);

$jsonContent = $serializer->serialize($child, null, [
    AbstractObjectNormalizer::ENABLE_MAX_DEPTH => true
]);
// $jsonContent contains {"name":"Joe","mother":{"name":"Sophie"}}

您还可以配置在达到最大深度时使用的自定义可调用对象。这可以用于例如返回下一个嵌套对象的唯一标识符,而不是省略属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
// ...

$greatGrandmother = new Person('Elizabeth', null);
$grandmother = new Person('Jane', $greatGrandmother);
$mother = new Person('Sophie', $grandmother);
$child = new Person('Joe', $mother);

// all callback parameters are optional (you can omit the ones you don't use)
$maxDepthHandler = function (object $innerObject, object $outerObject, string $attributeName, ?string $format = null, array $context = []): ?string {
    // return only the name of the next person in the tree
    return $innerObject instanceof Person ? $innerObject->getName() : null;
};

$jsonContent = $serializer->serialize($child, null, [
    AbstractObjectNormalizer::ENABLE_MAX_DEPTH => true,
    AbstractObjectNormalizer::MAX_DEPTH_HANDLER => $maxDepthHandler,
]);
// $jsonContent contains {"name":"Joe","mother":{"name":"Sophie","mother":"Jane"}}

使用回调函数序列化带有对象实例的属性

序列化时,您可以设置回调来格式化特定的对象属性。这可以代替 定义组的上下文 使用

1
2
3
4
5
6
7
8
9
10
11
12
13
$person = new Person('cordoval', 34);
$person->setCreatedAt(new \DateTime('now'));

$context = [
    AbstractNormalizer::CALLBACKS => [
        // all callback parameters are optional (you can omit the ones you don't use)
        'createdAt' => function (object $attributeValue, object $object, string $attributeName, ?string $format = null, array $context = []) {
            return $attributeValue instanceof \DateTime ? $attributeValue->format(\DateTime::ATOM) : '';
        },
    ],
];
$jsonContent = $serializer->serialize($person, 'json', $context);
// $jsonContent contains {"name":"cordoval","age":34,"createdAt":"2014-03-22T09:43:12-0500"}

高级反序列化

要求所有属性

默认情况下,当未提供这些参数时,Serializer 会将 null 添加到可为空的属性。您可以通过将 AbstractNormalizer::REQUIRE_ALL_PROPERTIES 上下文选项设置为 true 来更改此行为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person
{
    public function __construct(
        public string $firstName,
        public ?string $lastName,
    ) {
    }
}

// ...
$data = ['firstName' => 'John'];
$person = $serializer->deserialize($data, Person::class, 'json', [
    AbstractNormalizer::REQUIRE_ALL_PROPERTIES => true,
]);
// throws Symfony\Component\Serializer\Exception\MissingConstructorArgumentException

在反标准化时收集类型错误

当反规范化到具有类型化属性的对象的有效负载时,如果有效负载包含与对象类型不同的属性,您将收到异常。

使用 COLLECT_DENORMALIZATION_ERRORS 选项可以一次收集所有异常,并获取部分反规范化的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
try {
    $person = $serializer->deserialize($jsonString, Person::class, 'json', [
        DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS => true,
    ]);
} catch (PartialDenormalizationException $e) {
    $violations = new ConstraintViolationList();

    /** @var NotNormalizableValueException $exception */
    foreach ($e->getErrors() as $exception) {
        $message = sprintf('The type must be one of "%s" ("%s" given).', implode(', ', $exception->getExpectedTypes()), $exception->getCurrentType());
        $parameters = [];
        if ($exception->canUseMessageForUser()) {
            $parameters['hint'] = $exception->getMessage();
        }
        $violations->add(new ConstraintViolation($message, '', $parameters, null, $exception->getPath(), null));
    }

    // ... return violation list to the user
}

在现有对象中反序列化

序列化器还可以用于更新现有对象。您可以通过配置 object_to_populate 序列化器上下文选项来执行此操作

1
2
3
4
5
6
7
8
9
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;

// ...
$person = new Person('Jane Doe', 59);

$serializer->deserialize($jsonData, Person::class, 'json', [
    AbstractNormalizer::OBJECT_TO_POPULATE => $person,
]);
// instead of returning a new object, $person is updated instead

注意

AbstractNormalizer::OBJECT_TO_POPULATE 选项仅用于顶级对象。如果该对象是树结构的根,则 normalized 数据中存在的所有子元素都将使用新实例重新创建。

AbstractObjectNormalizer::DEEP_OBJECT_TO_POPULATE 上下文选项设置为 true 时,根 OBJECT_TO_POPULATE 的现有子对象将从 normalized 数据中更新,而不是反规范化器重新创建它们。这仅适用于单个子对象,不适用于对象数组。当它们存在于 normalized 数据中时,这些对象仍将被替换。

反序列化接口和抽象类

当使用关联对象时,属性有时会引用接口或抽象类。在反序列化这些属性时,Serializer 必须知道要初始化哪个具体类。这可以使用鉴别器类映射来完成。

假设存在一个由 ProductShipping 对象实现的 InvoiceItemInterface。当序列化对象时,序列化器将添加一个额外的“鉴别器属性”。这包含 productshipping。鉴别器类映射在反序列化时将这些类型名称映射到真实的 PHP 类名称

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
namespace App\Model;

use Symfony\Component\Serializer\Attribute\DiscriminatorMap;

#[DiscriminatorMap(
    typeProperty: 'type',
    mapping: [
        'product' => Product::class,
        'shipping' => Shipping::class,
    ]
)]
interface InvoiceItemInterface
{
    // ...
}

配置鉴别器映射后,序列化器现在可以为类型为 InvoiceItemInterface 的属性选择正确的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class InvoiceLine
{
    public function __construct(
        private InvoiceItemInterface $invoiceItem
    ) {
        $this->invoiceItem = $invoiceItem;
    }

    public function getInvoiceItem(): InvoiceItemInterface
    {
        return $this->invoiceItem;
    }

    // ...
}

// ...
$invoiceLine = new InvoiceLine(new Product());

$jsonString = $serializer->serialize($invoiceLine, 'json');
// $jsonString contains {"type":"product",...}

$invoiceLine = $serializer->deserialize($jsonString, InvoiceLine::class, 'json');
// $invoiceLine contains new InvoiceLine(new Product(...))

部分反序列化输入(解包)

序列化器将始终将完整的输入字符串反序列化为 PHP 值。当与第三方 API 连接时,您通常只需要返回的响应的特定部分。

为了避免反序列化整个响应,您可以使用 UnwrappingDenormalizer 并“解包”输入数据

1
2
3
4
5
$jsonData = '{"result":"success","data":{"person":{"name": "Jane Doe","age":57}}}';
$data = $serialiser->deserialize($jsonData, Object::class, [
    UnwrappingDenormalizer::UNWRAP_PATH => '[data][person]',
]);
// $data is Person(name: 'Jane Doe', age: 57)

unwrap_path 是 PropertyAccess 组件的 属性路径,应用于反规范化的数组。

处理构造函数参数

如果类构造函数定义了参数,就像 值对象 通常发生的情况一样,序列化器会将参数名称与反序列化的属性进行匹配。如果缺少某些参数,则会抛出 MissingConstructorArgumentsException

在这些情况下,使用 default_constructor_arguments 上下文选项为缺少的参数定义默认值

1
2
3
4
5
6
7
8
9
10
11
use App\Model\Person;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
// ...

$jsonData = '{"age":39,"name":"Jane Doe"}';
$person = $serializer->deserialize($jsonData, Person::class, 'json', [
    AbstractNormalizer::DEFAULT_CONSTRUCTOR_ARGUMENTS => [
        Person::class => ['sportsperson' => true],
    ],
]);
// $person is Person(name: 'Jane Doe', age: 39, sportsperson: true);

递归反标准化和类型安全

PropertyTypeExtractor 可用时,规范化器还将检查要反规范化的数据是否与属性的类型匹配(即使对于原始类型也是如此)。例如,如果提供了 string,但属性的类型为 int,则将抛出 UnexpectedValueException 。可以通过将序列化器上下文选项 ObjectNormalizer::DISABLE_TYPE_ENFORCEMENT 设置为 true 来禁用属性的类型强制。

处理布尔值

7.1

AbstractNormalizer::FILTER_BOOL 上下文选项在 Symfony 7.1 中引入。

PHP 将许多不同的值视为 true 或 false。例如,字符串 true1yes 被视为 true,而 false0no 被视为 false。

在反序列化时,Serializer 组件可以自动处理此问题。这可以通过使用 AbstractNormalizer::FILTER_BOOL 上下文选项来完成

1
2
3
4
5
6
7
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
// ...

$person = $serializer->denormalize(['sportsperson' => 'yes'], Person::class, context: [
    AbstractNormalizer::FILTER_BOOL => true
]);
// $person contains a Person instance with sportsperson set to true

此上下文使反序列化过程的行为类似于使用 FILTER_VALIDATE_BOOL 标志的 filter_var 函数。

配置元数据缓存

序列化器的元数据会自动缓存以提高应用程序性能。默认情况下,序列化器使用 cache.system 缓存池,该缓存池使用 cache.system 选项进行配置。

本作品,包括代码示例,根据 Creative Commons BY-SA 3.0 许可获得许可。
TOC
    版本