如果你和我一样,第一次在 PHP 中看到序列化字符串时会觉得很困惑。我当时在做一个 Laravel 项目,想搞清楚将任务推送到队列时到底发生了什么。我发现一些数据被序列化了,但不知道为什么以及怎么工作的。不过在我花时间研究序列化后,发现它其实没那么复杂。
本文会介绍什么是序列化以及工作原理。然后会说明如何使用 PHP 的内置序列化函数,让你能在应用中序列化和反序列化数据。最后会讲如何编写测试来确保序列化代码正常工作。
读完这篇文章,你应该能理解什么是序列化,并且能放心地在项目中使用。
序列化就是把变量、对象或数据结构转换成字符串格式的过程。这种字符串格式能表示原始数据,方便存储或传输。反过来,反序列化(在 PHP 中通常叫"unserialization")就是把序列化的数据转换回原来的形式。
序列化很重要,常用于把数据存储到缓存、数据库或文件中。
数据可以序列化成很多格式,比如 JSON、XML,甚至二进制格式(比如 gRPC API 用的 Protocol Buffers)。不过这篇文章主要讲 PHP 的内置序列化函数。
举个例子,如果你用过 Laravel,应该注意到这个框架在把任务推送到队列时会序列化数据。比如下面这个 Laravel 中被推到队列的待处理任务(为了好看,分了行并去掉了一些属性):
{
"uuid": "3d05be68-8cd0-4c3a-8d05-71e86871713a",
"data": {
"commandName": "App\Jobs\SendOneTimePassword",
"command": "O:28:"App\Jobs\SendOneTimePassword"
:1:{s:15:"oneTimePassword";s:6:"123456";}"
}
}
在这个待处理任务的 JSON 例子中,data.command
属性是个序列化字符串,代表一个 AppJobsSendOneTimePassword
任务。队列工作器拿到这个任务时,会把序列化字符串反序列化,创建 AppJobsSendOneTimePassword
类的实例来处理。如果现在看不懂也没关系,后面会有更多例子来解释。
PHP 中可以用 serialize
和 unserialize
函数来序列化和反序列化数据。
serialize
函数接受要序列化的数据,返回字符串格式。unserialize
函数接受序列化的数据,返回原来的数据结构。
看看怎么在 PHP 中序列化和反序列化不同类型的数据:
序列化字符串很简单,直接传给 serialize
函数:
$serialized = serialize('Hello');
这将返回一个序列化字符串:
s:5:"Hello";
这乍看起来有点奇怪,但一旦你注意到模式,你会发现它并不像看起来那么可怕。我们的序列化数据遵循格式:data_type:string_length:string;
。
所以在上面序列化字符串的情况下,s
代表字符串并表示反序列化数据时的数据类型,5
是字符串的长度。
然后我们可以将该序列化字符串传递给 unserialize
函数以获取原始字符串:
$string = unserialize('s:5:"Hello";');
我们也可以在 PHP 中序列化整数和浮点数。以下是序列化整数的方法:
serialize(123);
这将返回一个序列化字符串:
i:123;
你可能已经注意到结构与我们之前看到的序列化字符串略有不同。整数使用格式 data_type:data;
进行序列化。注意这里我们没有像字符串那样的大小。在这种情况下,序列化数据的数据类型是 i
表示整数。
同样,我们可以序列化浮点数:
serialize(123.45);
这将返回一个序列化字符串:
d:123.45;
这个结构类似于整数序列化,但数据类型是 d
表示双精度浮点数。
我们也可以在 PHP 中序列化布尔值。例如,我们可以序列化 true
:
serialize(true);
这将返回一个序列化字符串,其中 b
作为数据类型,1
(表示 true)作为值:
b:1;
同样,我们可以序列化 false
:
serialize(false);
这将返回一个序列化字符串,其中 b
作为数据类型,0
(表示 false)作为值:
"b:0;"
我们可以这样在 PHP 中序列化数组:
serialize([1,2,3]);
这将返回一个序列化字符串:
a:3:{i:0;i:1;i:1;i:2;i:2;i:3;}
现在,你可能已经注意到这比我们已经看过的其他序列化数据要复杂一些。让我们分解一下。
字符串具有 data_type:size:{key_data_type:key_data;value_data_type:value_data;...}
的结构。在这种情况下,data_type
是 a
表示数组,size
是 3
,因为数组有 3 个元素。
如果我们然后查看 { }
内的数据,我们可以看到键由 i
表示整数,值也由 i
表示整数。通过将它们分成新行来可视化结构可能会有所帮助:
i:0;i:1;
i:1;i:2;
i:2;i:3;
作为另一个例子,让我们看看序列化的字符串数组可能是什么样子。我们可以序列化以下数组:
serialize(['a','b','c']);
这将返回一个序列化字符串:
a:3:{i:0;s:1:"a";i:1;s:1:"b";i:2;s:1:"c";}
正如我们在上面的序列化字符串中看到的,键仍然由 i
表示,而值由 s
表示字符串。为了帮助可视化结构,我们可以将数据分成新行:
i:0;s:1:"a";
i:1;s:1:"b";
i:2;s:1:"c";
同样,我们也可以序列化关联数组:
serialize(['a' => 'A', 'b' => 'B', 'c' => 'C']);
这将返回一个序列化字符串:
a:3:{s:1:"a";s:1:"A";s:1:"b";s:1:"B";s:1:"c";s:1:"C";}
正如我们所看到的,结构与我们已经看过的序列化数组非常相似。但是,在这种情况下,键由 s
表示字符串。为了帮助可视化结构,我们可以将数据分成新行:
s:1:"a";s:1:"A";
s:1:"b";s:1:"B";
s:1:"c";s:1:"C";
我们也可以在 PHP 中序列化枚举。作为一个基本示例,假设我们有以下表示博客文章状态的枚举:
namespace AppEnums;
enum PostStatus: string
{
case Published = 'published';
case Draft = 'draft';
case Pending = 'in_review';
}
让我们想象然后创建此枚举的新实例并像这样序列化它:
serialize(PostStatus::Published);
这将返回一个序列化字符串:
E:30:"AppEnumsPostStatus:Published";
序列化枚举的结构是 data_type:size:"enum_type:enum_value";
。在这种情况下,数据类型由 E
表示,大小是 30
,因为类名是 AppEnumsPostStatus
,枚举值是 Published
。
到目前为止,我们已经介绍了序列化如何适用于基本数据类型,如字符串、整数、浮点数、布尔值、数组和枚举。但是对象呢?
默认情况下,除了少数内置 PHP 类之外,所有对象都是可序列化的。
为了解释对象序列化的工作原理,让我们以一个基本的 AppUser
类为例,它包含三个公共属性:
namespace App;
class User
{
public function __construct(
public string $name,
public string $email,
public string $apiToken,
) { }
}
我们将创建此类的新实例并序列化它:
$user = new User(
name: 'Ash Allen',
email: '[email protected]',
apiToken: 'secret',
);
serialize($user);
这将返回一个序列化字符串:
O:8:"AppUser":3:{s:4:"name";s:9:"Ash Allen";s:5:"email";s:
25:"[email protected]";s:8:"apiToken";s:6:"secret";}
让我们分解序列化对象的结构。我们有以下结构:
data_type:class_name_size:class_name:property_count:{
property_name_type:property_name_size:property_name;
property_value_type:property_value_size:property_value;
...
}
因此,从这个结构中,我们可以看到数据类型是 O
表示对象,类名大小是 8
,类名是 AppUser
,属性计数是 3
,因为对象有 3 个属性。然后我们可以看到 { }
内的每个序列化属性。
然后我们可以将此序列化字符串传递给 unserialize
函数以获取原始对象:
$serialized = 'O:8:"AppUser":3:{s:4:"name";s:9:"Ash Allen'
.'";s:5:"email";s:25:"[email protected]";s:8:'
.'"apiToken";s:6:"secret";}';
$user = unserialize($serialized);
这将返回 AppUser
类的实例,每个属性都像原始对象一样设置。
序列化对象时,属性的可见性很重要,因为它会影响返回的字符串。
让我们更新我们的 AppUser
类以具有公共、受保护和私有属性:
namespace App;
class User
{
public function __construct(
public string $name,
protected string $email,
private string $apiToken,
) { }
}
然后我们将创建此类的新实例并序列化它:
$user = new User(
name: 'Ash Allen',
email: '[email protected]',
apiToken: 'secret',
);
serialize($user);
这将返回一个序列化字符串:
O:8:"AppUser":3:{s:4:"name";s:9:"Ash Allen";s:8:
" * email";s:25:"[email protected]";s:18:
" AppUser apiToken";s:6:"secret";}
字符串格式与我们之前的序列化对象非常相似。但是,email
和 apiToken
属性的名称略有不同。
当 PHP 序列化对象时,它将为属性名添加前缀以指示属性的可见性。受保护的属性由 *
前缀指示,私有属性由类名前缀指示。所以我们可以看到,我们有