跳到內容

如何使用序列化器

編輯此頁面

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 物件。此過程稱為反序列化(也稱為「水合作用」)

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 類別在(反)序列化物件時使用已排序的正規化器列表和一個用於特定格式的編碼器。

預設序列化器服務中配置了幾個正規化器。最重要的正規化器是 ObjectNormalizer。此正規化器使用反射和 PropertyAccess 組件在任何物件和陣列之間進行轉換。您稍後將了解更多關於此正規化器和其他正規化器的資訊。

預設序列化器也配置了一些編碼器,涵蓋 HTTP 應用程式常用的格式

在序列化器編碼器中閱讀更多關於這些編碼器及其設定的資訊。

提示

API Platform 專案為更進階的格式提供編碼器

  • JSON-LD 以及 Hydra 核心詞彙
  • OpenAPI v2(前身為 Swagger)和 v3
  • GraphQL
  • JSON:API
  • HAL

序列化器上下文

序列化器及其正規化器和編碼器透過序列化器上下文進行配置。此上下文可以在多個位置配置

您可以同時使用所有三個選項。當在多個位置配置相同的設定時,上面列表中的後者將覆蓋前者(例如,特定屬性上的設定會覆蓋全域配置的設定)。

設定預設上下文

您可以在框架設定中配置預設上下文,例如在反序列化時不允許額外欄位

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

在序列化/反序列化時傳遞上下文

您也可以為單次呼叫 serialize()/deserialize() 設定上下文。例如,您可以僅為一次序列化呼叫略過具有空值的屬性

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.yaml 或 serialization.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 陣列或從 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');

對於巢狀類別,您必須將 PHPDoc 類型新增到屬性、建構子或 setter

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;
    }

    // ...
}

提示

Serializer 也支援靜態分析中使用的陣列類型,例如 list<Person> 和 array<Person>。請確保已安裝 phpstan/phpdoc-parser 和 phpdocumentor/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", ...}

另請參閱

您也可以建立自訂名稱轉換器類別。在如何建立您的自訂名稱轉換器中閱讀更多相關資訊。

駝峰式命名轉蛇底式命名

在許多格式中,通常使用底線分隔單字(也稱為蛇底式命名)。但是,在 Symfony 應用程式中,通常使用駝峰式命名來命名屬性。

Symfony 提供了一個內建的名稱轉換器,旨在在序列化和反序列化過程中在蛇底式命名和駝峰式命名樣式之間進行轉換。您可以將 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_58、UidNormalizer::NORMALIZATION_FORMAT_BASE_32 或 UidNormalizer::NORMALIZATION_FORMAT_RFC_4122 來變更字串格式。

它也可以將 uuid 或 ulid 字串反正規化為 Uuid 或 Ulid。格式無關緊要。

DateTimeNormalizer

這在 DateTimeInterface 物件(例如 DateTime 和 DateTimeImmutable)與字串、整數或浮點數之間進行正規化。

DateTime 和 DateTimeImmutable)轉換為字串、整數或浮點數。預設情況下,它使用 RFC 3339 格式將它們轉換為字串。使用 DateTimeNormalizer::FORMAT_KEY 和 DateTimeNormalizer::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

此正規化器會使用 翻譯器,將實作 TranslatableInterface 的物件轉換為已翻譯的字串。

您可以設定 TranslatableNormalizer::NORMALIZATION_LOCALE_KEY 內容選項,以定義用於翻譯物件的語系。

BackedEnumNormalizer

此正規化器會在 BackedEnum 列舉與字串或整數之間進行轉換。

預設情況下,當資料不是有效的 backed enumeration 時,會擲出例外。如果您想要改為 null,您可以設定 BackedEnumNormalizer::ALLOW_INVALID_VALUES 選項。

DataUriNormalizer
此正規化器會在 SplFileInfo 物件與 data URI 字串(data:...)之間進行轉換,以便將檔案嵌入序列化資料中。
JsonSerializableNormalizer

此正規化器適用於實作 JsonSerializable 的類別。

它將呼叫 JsonSerializable::jsonSerialize() 方法,然後進一步正規化結果。這表示巢狀 JsonSerializable 類別也將被正規化。

當您想要從使用簡單 json_encode 的現有程式碼庫逐步移轉到 Symfony Serializer 時,此正規化器特別有用,因為它可讓您混合使用哪些正規化器用於哪些類別。

json_encode 不同的是,可以處理循環參照。

ArrayDenormalizer
此反正規化器會將陣列的陣列轉換為物件的陣列(具有給定的類型)。請參閱 處理陣列
ObjectNormalizer

這是功能最強大的預設正規化器,適用於其他正規化器無法正規化的任何物件。

它利用 PropertyAccess Component 來讀取和寫入物件。這使其可以直接或使用 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 reflection。它支援在反正規化過程中呼叫建構子。

物件會正規化為屬性名稱到屬性值的對應。

您也可以使用 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 新增至可為 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 選項僅用於最上層物件。如果該物件是樹狀結構的根目錄,則正規化資料中存在的所有子元素都將使用新執行個體重新建立。

AbstractObjectNormalizer::DEEP_OBJECT_TO_POPULATE 內容選項設定為 true 時,會從正規化資料更新根 OBJECT_TO_POPULATE 的現有子系,而不是由反正規化器重新建立它們。這僅適用於單一子物件,不適用於物件陣列。當物件陣列出現在正規化資料中時,仍會被取代。

反序列化介面和抽象類別

使用關聯物件時,屬性有時會參照介面或抽象類別。當反序列化這些屬性時,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 元件的 屬性路徑,適用於反正規化的陣列。

處理建構子參數

如果類別建構子定義了引數,就像 Value Objects 通常發生的情況一樣,序列化器會將參數名稱與反序列化的屬性進行比對。如果缺少某些參數,則會擲出 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
    版本