Serializer组件

3.4 版本
维护中的版本

Serializer组件意味着用于把对象转换成一种特殊格式(XML, JSON, YAML, ...)或者反其道而行之。

要实现这个,Serializer组件遵循下列简易框图。

如你在图中所见,有一个数组充当着中间人。这样一来,Encoder(编码器)只负责将特定的format(格式)转换成array(数组),或者反过来。相同方式下,Normalizers将负责把特定的Object转换成array,或者反过来。

序列化是一个复杂的话题,虽然本组件未必能满足全部使用场合,但在开发用于“序列化和反序列化你的对象”之工具的过程中,仍然有其作用。

安装 

你可以通过下述两种方式安装:

然后,包容vendor/autoload.php文件,以开启Composer提供的自动加载机制。否则,你的程序将无法找到这个Symfony组件的类。

要使用 ObjectNormalizer,必须安装 PropertyAccess组件

用法 

使用Serializer十分简单。你只需设置 Serializer 来指定要使用哪个encoders和normalizer:

1
2
3
4
5
6
7
8
9
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
 
$encoders = array(new XmlEncoder(), new JsonEncoder());
$normalizers = array(new ObjectNormalizer());
 
$serializer = new Serializer($normalizers, $encoders);

首选的normalizer是 ObjectNormalizer,但也可以使用其他的normalizers。以下所有例程用的都是ObjectNormalizer

序列化一个对象 

本例假定在你的项目中已经存在下面这个类:

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
namespace Acme;
 
class Person
{
    private $age;
    private $name;
    private $sportsman;
 
    // Getters
    public function getName()
    {
        return $this->name;
    }
 
    public function getAge()
    {
        return $this->age;
    }
 
    // Issers
    public function isSportsman()
    {
        return $this->sportsman;
    }
 
    // Setters
    public function setName($name)
    {
        $this->name = $name;
    }
 
    public function setAge($age)
    {
        $this->age = $age;
    }
 
    public function setSportsman($sportsman)
    {
        $this->sportsman = $sportsman;
    }
}

现在,如果你希望序列化这个对象为JSON,只需使用之前创建的Serializer服务:

1
2
3
4
5
6
7
8
9
10
$person = new Acme\Person();
$person->setName('foo');
$person->setAge(99);
$person->setSportsman(false);
 
$jsonContent = $serializer->serialize($person, 'json');
 
// $jsonContent contains {"name":"foo","age":99,"sportsman":false}
 
echo $jsonContent; // or return it in a Response

serialize() 方法的第一个参数是“将要被序列化的对象”,第二个参数用来选择合适的encoder,在本例中是 JsonEncoder

反序列化一个对象 

现在来了解如何反向提取。这回,Person类的信息,将从XML格式中反解出来:

1
2
3
4
5
6
7
8
9
$data = <<<EOF
<person>
    <name>foo</name>
    <age>99</age>
    <sportsman>false</sportsman>
</person>
EOF;
 
$person = $serializer->deserialize($data, 'Acme\Person', 'xml');

在本例,deserialize() 方法需要三个参数:

  1. 将要被反解的信息

  2. 将要容纳反解出来的信息的类的名字

  3. 用于把信息转换成数组的encoder

反序列化到一个已存在的对象中 

serializer也可以用于更新一个既存对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$person = new Acme\Person();
$person->setName('bar');
$person->setAge(99);
$person->setSportsman(true);
 
$data = <<<EOF
<person>
    <name>foo</name>
    <age>69</age>
</person>
EOF;
 
$serializer->deserialize($data, 'Acme\Person', 'xml', array('object_to_populate' => $person));
// $person = Acme\Person(name: 'foo', age: '69', sportsman: true)

当使用ORM时这是一个常规需求。

属性群组 

有时,你需要对你的多个entity中不同的属性组进行序列化。Groups是一个绝佳的方式来实现此种需求。

假设你有以下原生PHP对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
namespace Acme;
 
class MyObj
{
    public $foo;
 
    private $bar;
 
    public function getBar()
    {
        return $this->bar;
    }
 
    public function setBar($bar)
    {
        return $this->bar = $bar;
    }
}

序列化在定义时可以指定使用annotations, XML 或 YAML。被normalizer所用到的 ClassMetadataFactory 对“将要使用哪一种格式”必须很清楚。

按如下代码对 ClassMetadataFactory 进行初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
// For annotations / 对于annotation
use Doctrine\Common\Annotations\AnnotationReader;
use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
// For XML / 对于XML格式
// use Symfony\Component\Serializer\Mapping\Loader\XmlFileLoader;
// For YAML / 对于YAML格式
// use Symfony\Component\Serializer\Mapping\Loader\YamlFileLoader;
 
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
// For XML / 对于XML格式
// $classMetadataFactory = new ClassMetadataFactory(new XmlFileLoader('/path/to/your/definition.xml'));
// For YAML / 对于YAML格式
// $classMetadataFactory = new ClassMetadataFactory(new YamlFileLoader('/path/to/your/definition.yml'));
 

然后,创建你的群组定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
namespace Acme;
 
use Symfony\Component\Serializer\Annotation\Groups;
 
class MyObj
{
    /**
     * @Groups({"group1", "group2"})
     */
    public $foo;
 
    /**
     * @Groups({"group3"})
     */
    public function getBar() // is* methods are also supported
                             // is* 方法也受到支持
    {
        return $this->bar;
    }
 
    // ...
}
1
2
3
4
5
6
Acme\MyObj:
    attributes:
        foo:
            groups: ['group1', 'group2']
        bar:
            groups: ['group3']
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" ?>
<serializer xmlns="http://symfony.com/schema/dic/serializer-mapping"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/serializer-mapping
        http://symfony.com/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd"
>
    <class name="Acme\MyObj">
        <attribute name="foo">
            <group>group1</group>
            <group>group2</group>
        </attribute>
 
        <attribute name="bar">
            <group>group3</group>
        </attribute>
    </class>
</serializer>

现在你就可以“仅对你需要的群组”之属性进行序列化了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
 
$obj = new MyObj();
$obj->foo = 'foo';
$obj->setBar('bar');
 
$normalizer = new ObjectNormalizer($classMetadataFactory);
$serializer = new Serializer(array($normalizer));
 
$data = $serializer->normalize($obj, null, array('groups' => array('group1')));
// $data = array('foo' => 'foo');
 
$obj2 = $serializer->denormalize(
    array('foo' => 'foo', 'bar' => 'bar'),
    'MyObj',
    null,
    array('groups' => array('group1', 'group3'))
);
// $obj2 = MyObj(foo: 'foo', bar: 'bar')

忽略属性 

使用属性群组(attribute groups)而不是setIgnoredAttributes()方法,是经过深思熟虑的最佳实践。

作为一个选择,有一种方式可以从原始对象中忽略属性。要移除那些属性,使用normalizer定义中的setIgnoredAttributes()方法:

1
2
3
4
5
6
7
8
9
10
11
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
 
$normalizer = new ObjectNormalizer();
$normalizer->setIgnoredAttributes(array('age'));
$encoder = new JsonEncoder();
 
$serializer = new Serializer(array($normalizer), array($encoder));
$serializer->serialize($person, 'json'); // Output: {"name":"foo","sportsman":false}
 

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

有时,经过序列化的属性,必须与PHP类中的属性或/getter/setter方法在命名上不一样。

Serializer组件提供了一个很好的方式,来翻译(translate)或映射(map)PHP属性名称,令其成为“序列化命名”:即,命名转换系统(The Name Converter System)。

给你一个对象:

1
2
3
4
5
class Company
{
    public $name;
    public $address;
}

在序列化之后的(内容)形式中,对所有属性必须被施以下面这种org_前缀:

1
{"org_name": "Acme Inc.", "org_address": "123 Main Street, Big City"}

一个自定义的命名转换器可以解决这种问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
 
class OrgPrefixNameConverter implements NameConverterInterface
{
    public function normalize($propertyName)
    {
        return 'org_'.$propertyName;
    }
 
    public function denormalize($propertyName)
    {
        // remove org_ prefix
        return 'org_' === substr($propertyName, 0, 4) ? substr($propertyName, 4) : $propertyName;
    }
}

自定义normalizer的第二个参数,可用来传递“命名转换器”,它可以是任何继承了 AbstractNormalizer的类,包括 GetSetMethodNormalizerPropertyNormalizer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
use Symfony\Component\Serializer\Encoder\JsonEncoder
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
 
$nameConverter = new OrgPrefixNameConverter();
$normalizer = new ObjectNormalizer(null, $nameConverter);
 
$serializer = new Serializer(array($normalizer), array(new JsonEncoder()));
 
$obj = new Company();
$obj->name = 'Acme Inc.';
$obj->address = '123 Main Street, Big City';
 
$json = $serializer->serialize($obj);
// {"org_name": "Acme Inc.", "org_address": "123 Main Street, Big City"}
$objCopy = $serializer->deserialize($json);
// Same data as $obj

驼峰到蛇型 

在很多格式中,使用下划线来分隔单词是再普通不过的(也被称作snake_case)。但是,PSR-1指定了首选的PHP属性和方法之风格,却是CamelCase(驼峰)。

Symfony提供了内置的命名转换器,被设计为在序列化和反序列化进程中对snake_case和CamelCase两种风格进行转换:

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
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
 
$normalizer = new ObjectNormalizer(null, new CamelCaseToSnakeCaseNameConverter());
 
class Person
{
    private $firstName;
 
    public function __construct($firstName)
    {
        $this->firstName = $firstName;
    }
 
    public function getFirstName()
    {
        return $this->firstName;
    }
}
 
$kevin = new Person('Kévin');
$normalizer->normalize($kevin);
// ['first_name' => 'Kévin'];
 
$anne = $normalizer->denormalize(array('first_name' => 'Anne'), 'Person');
// Person object with firstName: 'Anne'

序列化布尔值属性 

如果你使用了isser方法 (就是以 is 作为前缀的方法,比如 Acme\Person::isSportsman()),这时Serializer 组件将自动侦测并使用它来对相关属性进行序列化。

ObjectNormalizer 还会自动处理那些由 has, addremove开头的方法。

使用回调来对含有对象实例的属性序列化 

在序列化时,你可以设置一个回调,来对特定对象的属性进行格式化:

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
use Acme\Person;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer;
use Symfony\Component\Serializer\Serializer;
 
$encoder = new JsonEncoder();
$normalizer = new GetSetMethodNormalizer();
 
$callback = function ($dateTime) {
    return $dateTime instanceof \DateTime
        ? $dateTime->format(\DateTime::ISO8601)
        : '';
};
 
$normalizer->setCallbacks(array('createdAt' => $callback));
 
$serializer = new Serializer(array($normalizer), array($encoder));
 
$person = new Person();
$person->setName('cordoval');
$person->setAge(34);
$person->setCreatedAt(new \DateTime('now'));
 
$serializer->serialize($person, 'json');
// Output: {"name":"cordoval", "age": 34, "createdAt": "2014-03-22T09:43:12-0500"}

Normalizers 

下面是一些可用的normalizer类型:

ObjectNormalizer

这个normalizer利用了 PropertyAccess组件 来对对象进行读写。意味着它可以直接地或通过getters, setters, hassers, adders 以及 removers来访问到属性。它支持在denormalization进程中调用构造器。

对象经normalize而成为一个“属性名 - 属性值”的映射关系(方法名则被去除了"get"/"set"/"has"/"remove"前缀并被转换为小写)。

ObjectNormalizer 是最为强大的normalizer。当使用Symfony标准版并且开启了serializer时,它被默认配置好了。

GetSetMethodNormalizer

这个normalizer通过"getters"(以"get"开始的公有方法)来读取类的内容。它可以通过调用constructor和"setters" ("set"开始的公有方法)来对数据denormalize。

对象经normalize而成为一个“属性名 - 属性值”的映射关系(方法名则被去除了"get"/"set"/"has"/"remove"前缀并被转换为小写)。

PropertyNormalizer

这个normalizer可以直接对public属性和 private 以及 protected 属性进行读写。它支持在denormalization进程中调用构造器。

对象经normalize而成为一个“属性名 - 属性值”的映射关系。

JsonSerializableNormalizer

这个normalizer与实现了 JsonSerializable的类一起工作。

它将调用 JsonSerializable::jsonSerialize() 方法,然后进一步对结果进行normalize。这意味着嵌套的 JsonSerializable 类同样会被normalize。

当你想要从一个使用了 json_encode 的既存代码库逐步迁移到 Symfony Serializer时,这个normalizer极为有用,它可令你把“哪个normalizer用于哪个类”给混合起来。

并不同于能够被解决的 json_encode 循环引用。

DateTimeNormalizer
这个normalizer把 DateTimeInterface 对象 (如 DateTimeDateTimeImmutable) 转换成字符串。它默认使用 RFC3339 格式。
DataUriNormalizer
这个normalizer把 SplFileInfo 对象转换成data URI字符串 (data:...),这样该文件就可以被嵌入到序列化的数据中。

3.1 JsonSerializableNormalizer, DateTimeNormalizerDataUriNormalizer从Symfony 3.1开始被引入。

应对循环引用 

当处理entity之间的关系时,circular references(循环引用)十分常见:

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
49
50
51
class Organization
{
    private $name;
    private $members;
 
    public function setName($name)
    {
        $this->name = $name;
    }
 
    public function getName()
    {
        return $this->name;
    }
 
    public function setMembers(array $members)
    {
        $this->members = $members;
    }
 
    public function getMembers()
    {
        return $this->members;
    }
}
 
class Member
{
    private $name;
    private $organization;
 
    public function setName($name)
    {
        $this->name = $name;
    }
 
    public function getName()
    {
        return $this->name;
    }
 
    public function setOrganization(Organization $organization)
    {
        $this->organization = $organization;
    }
 
    public function getOrganization()
    {
        return $this->organization;
    }
}

为了避免无限循环, GetSetMethodNormalizer 会在遇到下述情况时抛出一个 CircularReferenceException 异常:

1
2
3
4
5
6
7
8
9
10
11
$member = new Member();
$member->setName('Kévin');
 
$org = new Organization();
$org->setName('Les-Tilleuls.coop');
$org->setMembers(array($member));
 
$member->setOrganization($org);
 
echo $serializer->serialize($org, 'json'); // Throws a CircularReferenceException
                                           // 抛出一个CircularReferenceException异常

这个normalizer 的setCircularReferenceLimit()方法,设置的是“在认定一个循环引用之前,它要对同一对象进行序列化的时间”之数字,默认值是1

不同于抛出异常,循环引用还可以通过自定义回调(custom callables)来控制。当对“拥有unique id”的entity序列化时格外有用:

1
2
3
4
5
6
7
8
9
10
11
$encoder = new JsonEncoder();
$normalizer = new ObjectNormalizer();
 
$normalizer->setCircularReferenceHandler(function ($object) {
    return $object->getName();
});
 
$serializer = new Serializer(array($normalizer), array($encoder));
var_dump($serializer->serialize($org, 'json'));
// {"name":"Les-Tilleuls.coop","members":[{"name":"K\u00e9vin", organization: "Les-Tilleuls.coop"}]}
 

操作数组 

Serializer组件有能力对“对象数组”进行操作。序列化数组时,类似于对单一对象序列化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
use Acme\Person;
 
$person1 = new Person();
$person1->setName('foo');
$person1->setAge(99);
$person1->setSportsman(false);
 
$person2 = new Person();
$person2->setName('bar');
$person2->setAge(33);
$person2->setSportsman(true);
 
$persons = array($person1, $person2);
$data = $serializer->serialize($persons, 'json');
 
// $data contains [{"name":"foo","age":99,"sportsman":false},{"name":"bar","age":33,"sportsman":true}]
 

如果你希望deserialize这样一个结构,你应该添加 ArrayDenormalizer 到normalizers的数组中。通过对 deserialize() 方法的type参数附着 [],你就已经做出了“预期是一个数组,而不是单一对象”的暗示。

1
2
3
4
5
6
7
8
9
10
11
12
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer;
use Symfony\Component\Serializer\Serializer;
 
$serializer = new Serializer(
    array(new GetSetMethodNormalizer(), new ArrayDenormalizer()),
    array(new JsonEncoder())
);
 
$data = ...; // The serialized data from the previous example
$persons = $serializer->deserialize($data, 'Acme\Person[]', 'json');

Symfony Serializer组件的一个“受到欢迎”的替代之选,是第三方类库,JMS serializer(基于Apache license发布,不兼容GPLv2项目)。

本文,包括例程代码在内,采用的是 Creative Commons BY-SA 3.0 创作共用授权。

登录symfonychina 发表评论或留下问题(我们会尽量回复)