PHP serialize进行序列化工作的完全指南
如果你和我一样第一次在 PHP 中看到序列化字符串时会觉得很困惑。我当时在做一个 Laravel 项目想搞清楚将任务推送到队列时到底发生了什么。我发现一些数据被序列化了但不知道为什么以及怎么工作的。不过在我花时间研究序列化后发现它其实没那么复杂。本文会介绍什么是序列化以及工作原理。然后会说明如何使用 PHP 的内置序列化函数让你能在应用中序列化和反序列化数据。最后会讲如何编写测试来确保序列化代码正常工作。读完这篇文章你应该能理解什么是序列化并且能放心地在项目中使用。什么是序列化序列化就是把变量、对象或数据结构转换成字符串格式的过程。这种字符串格式能表示原始数据方便存储或传输。反过来反序列化在 PHP 中通常叫unserialization就是把序列化的数据转换回原来的形式。序列化很重要常用于把数据存储到缓存、数据库或文件中。数据可以序列化成很多格式比如 JSON、XML甚至二进制格式比如 gRPC API 用的 Protocol Buffers。不过这篇文章主要讲 PHP 的内置序列化函数。举个例子如果你用过 Laravel应该注意到这个框架在把任务推送到队列时会序列化数据。比如下面这个 Laravel 中被推到队列的待处理任务为了好看分了行并去掉了一些属性12345678{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属性是个序列化字符串代表一个App\Jobs\SendOneTimePassword任务。队列工作器拿到这个任务时会把序列化字符串反序列化创建App\Jobs\SendOneTimePassword类的实例来处理。如果现在看不懂也没关系后面会有更多例子来解释。PHP 中的序列化如何工作PHP 中可以用serialize和unserialize函数来序列化和反序列化数据。serialize函数接受要序列化的数据返回字符串格式。unserialize函数接受序列化的数据返回原来的数据结构。看看怎么在 PHP 中序列化和反序列化不同类型的数据序列化字符串序列化字符串很简单直接传给serialize函数1$serialized serialize(Hello);这将返回一个序列化字符串s:5:Hello;这乍看起来有点奇怪但一旦你注意到模式你会发现它并不像看起来那么可怕。我们的序列化数据遵循格式data_type:string_length:string;。所以在上面序列化字符串的情况下s代表字符串并表示反序列化数据时的数据类型5是字符串的长度。然后我们可以将该序列化字符串传递给unserialize函数以获取原始字符串1$string unserialize(s:5:Hello;);序列化整数和浮点数我们也可以在 PHP 中序列化整数和浮点数。以下是序列化整数的方法1serialize(123);这将返回一个序列化字符串i:123;你可能已经注意到结构与我们之前看到的序列化字符串略有不同。整数使用格式data_type:data;进行序列化。注意这里我们没有像字符串那样的大小。在这种情况下序列化数据的数据类型是i表示整数。同样我们可以序列化浮点数1serialize(123.45);这将返回一个序列化字符串d:123.45;这个结构类似于整数序列化但数据类型是d表示双精度浮点数。序列化布尔值我们也可以在 PHP 中序列化布尔值。例如我们可以序列化true1serialize(true);这将返回一个序列化字符串其中b作为数据类型1表示 true作为值b:1;同样我们可以序列化false1serialize(false);这将返回一个序列化字符串其中b作为数据类型0表示 false作为值b:0;序列化数组我们可以这样在 PHP 中序列化数组1serialize([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;作为另一个例子让我们看看序列化的字符串数组可能是什么样子。我们可以序列化以下数组1serialize([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;同样我们也可以序列化关联数组1serialize([aA,bB,cC]);这将返回一个序列化字符串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 中序列化枚举。作为一个基本示例假设我们有以下表示博客文章状态的枚举12345678namespaceApp\Enums;enum PostStatus: string{casePublished published;caseDraft draft;casePending in_review;}让我们想象然后创建此枚举的新实例并像这样序列化它1serialize(PostStatus::Published);这将返回一个序列化字符串E:30:App\Enums\PostStatus:Published;序列化枚举的结构是data_type:size:enum_type:enum_value;。在这种情况下数据类型由E表示大小是30因为类名是App\Enums\PostStatus枚举值是Published。序列化对象到目前为止我们已经介绍了序列化如何适用于基本数据类型如字符串、整数、浮点数、布尔值、数组和枚举。但是对象呢默认情况下除了少数内置 PHP 类之外所有对象都是可序列化的。为了解释对象序列化的工作原理让我们以一个基本的App\User类为例它包含三个公共属性12345678910namespaceApp;classUser{publicfunction__construct(publicstring$name,publicstring$email,publicstring$apiToken,) { }}我们将创建此类的新实例并序列化它1234567$usernewUser(name:Ash Allen,email:mailashallendesign.co.uk,apiToken:secret,);serialize($user);这将返回一个序列化字符串O:8:App\User:3:{s:4:name;s:9:Ash Allen;s:5:email;s:25:mailashallendesign.co.uk;s:8:apiToken;s:6:secret;}让我们分解序列化对象的结构。我们有以下结构12345data_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类名是App\User属性计数是3因为对象有 3 个属性。然后我们可以看到{ }内的每个序列化属性。然后我们可以将此序列化字符串传递给unserialize函数以获取原始对象12345$serializedO:8:App\User:3:{s:4:name;s:9:Ash Allen.;s:5:email;s:25:mailashallendesign.co.uk;s:8:.apiToken;s:6:secret;};$user unserialize($serialized);这将返回App\User类的实例每个属性都像原始对象一样设置。属性可见性序列化对象时属性的可见性很重要因为它会影响返回的字符串。让我们更新我们的App\User类以具有公共、受保护和私有属性12345678910namespaceApp;classUser{publicfunction__construct(publicstring$name,protectedstring$email,privatestring$apiToken,) { }}然后我们将创建此类的新实例并序列化它1234567$usernewUser(name:Ash Allen,email:mailashallendesign.co.uk,apiToken:secret,);serialize($user);这将返回一个序列化字符串O:8:App\User:3:{s:4:name;s:9:Ash Allen;s:8:\0*\0email;s:25:mailashallendesign.co.uk;s:18:\0App\User\0apiToken;s:6:secret;}字符串格式与我们之前的序列化对象非常相似。但是email和apiToken属性的名称略有不同。当 PHP 序列化对象时它将为属性名添加前缀以指示属性的可见性。受保护的属性由*前缀指示私有属性由类名前缀指示。所以我们可以看到我们有\0*\0email和\0App\User\0apiToken\0表示空字节而不是email和apiToken。让我们将序列化字符串分成新行以帮助可视化结构s:4:name;s:9:Ash Allen;s:8:\0*\0email;s:25:mailashallendesign.co.uks:18:\0App\User\0apiToken;这意味着通过查看序列化对象我们可以确定属性的可见性。序列化包含其他对象的对象有时你可能需要序列化包含另一个对象的对象。我们将快速看一下包含另一个对象的序列化对象可能是什么样子。假设我们有一个简单的App\ValueObjects\Address类123456789namespaceApp\ValueObjects;classAddress{publicfunction__construct(publicint$number,publicstring$postalCode,) { }}然后我们假设我们的App\User类有一个App\ValueObjects\Address对象作为属性。我们可能想要创建一个新对象并序列化它12345678$usernewUser(name:Ash Allen,email:mailashallendesign.co.uk,apiToken:secret,address:newAddress(18,SW1A 2AA),);serialize($user);这将导致如下序列化字符串O:8:App\User:4:{s:4:name;s:9:Ash Allen;s:5:email;s:25:mailashallendesign.co.uk;s:8:apiToken;s:6:secret;s:7:address;O:24:App\ValueObjects\Address:2:{s:6:number;i:18;s:10:postalCode;s:8:SW1A 2AA;}}让我们将此对象的内容分解到单独的行上s:4:name;s:9:Ash Allen;s:5:email;s:25:mailashallendesign.co.uk;s:8:apiToken;s:6:secret;s:7:address;O:24:App\ValueObjects\Address:2:{s:6:number;i:18;s:10:postalCode;s:8:SW1A 2AA;}正如我们在这里看到的App\ValueObjects\Address对象只是作为App\User对象的属性序列化。反序列化时的错误处理处理尝试反序列化无效数据时可能发生的任何错误很重要。根据你尝试反序列化的无效数据PHP 8.3 将发出E_WARNING或抛出\Exception或\Error。例如让我们看看这个无效的序列化字符串它对字符串hello的长度为 10 而不是预期的 51unserialize(s:10:hello;);如果我们在 PHP 8.3 中运行此代码将发出E_WARNING错误消息如下Warning: unserialize(): Error at offset 2 of 13 bytes in/www/serialization.php on line 3为了处理警告以便我们可以在代码中捕获和处理它们我们可以使用set_error_handler函数设置自定义错误处理程序。这将允许我们捕获警告并将它们作为异常抛出。为此我们首先创建一个新的App\Services\Serializer类如下所示12345678910111213141516171819202122232425declare(strict_types1);namespaceApp\Services;finalreadonlyclassSerializer{publicfunctionunserialize(string$serialized): mixed{try{set_error_handler(staticfunction($severity,$message,$file,$line) {thrownew\ErrorException($message, 0,$severity,$file,$line);});$result unserialize($serialized);} finally {restore_error_handler();}return$result;}}在此类中我们添加了一个接受序列化字符串的unserialize方法。然后我们覆盖错误处理程序以便我们可以捕获任何警告并将它们作为异常抛出。然后我们尝试反序列化数据。如果发出警告它将作为异常抛出。然后我们在finally块内将错误处理程序恢复到其原始状态无论反序列化是否成功都会运行。假设成功我们然后返回反序列化的数据。然后我们可以使用此类来反序列化数据12345useApp\Services\Serializer;$result (newSerializer())-unserialize(serialized:s:10:hello;);运行上述代码将导致抛出\ErrorException消息如下1unserialize(): Error at offset 2 of 13 bytes或者我们可以运行以下代码12345useApp\Services\Serializer;$result (newSerializer())-unserialize(serialized:s:5:hello;);这将导致返回字符串hello。目前有一个 RFC部分实现旨在改进反序列化数据时的错误处理。RFC 包含一个提案从 PHP 9.0 开始改变unserialize的行为使其抛出\UnserializationFailedException而不是发出E_WARNING。因此如果实现了这一点我们不需要覆盖错误处理程序来捕获警告并将它们作为异常抛出就像我们上面所做的那样。在 PHP 中定义序列化逻辑正如我们在上面已经看到的PHP 默认提供序列化和反序列化对象的能力。但是有时你可能想要为对象定义自定义序列化逻辑。这可能有几个原因例如在序列化之前加密敏感数据或者你可能需要在反序列化对象时执行一些额外的逻辑。值得庆幸的是PHP 提供了两个魔术方法你可以使用它们来定义如何序列化和反序列化对象__serialize和__unserialize。为了了解这可能如何工作让我们看一个例子。坚持我们之前的App\User类假设我们想要在序列化对象之前加密apiToken属性并在反序列化对象时解密它。这可能是因为我们将序列化数据存储在缓存或队列中所以我们想要确保数据在受到威胁时是安全的。为了本文的目的我们将假设我们有两个可以调用来加密和解密数据的函数encrypt和decrypt。我们现在不需要担心这些函数的实现我们只是假设它们存在。如果你们中的任何人是 Laravel 开发者你们可能会认识这些函数因为它们都随 Laravel 一起提供。让我们更新我们的App\User类以包含__serialize和__unserialize方法然后讨论正在做什么12345678910111213141516171819202122232425262728declare(strict_types1);namespaceApp;classUser{publicfunction__construct(publicstring$name,publicstring$email,publicstring$apiToken,) { }publicfunction__serialize():array{return[name$this-name,email$this-email,apiToken encrypt($this-apiToken),];}publicfunction__unserialize(array$data): void{$this-name $data[name];$this-email $data[email];$this-apiToken decrypt($data[apiToken]);}}在__serialize方法中我们返回要序列化的属性数组。我们在返回之前加密apiToken属性。这意味着当我们对对象调用serialize时apiToken属性将被加密。让我们创建App\User类的新实例并序列化它1234567$usernewUser(name:Ash Allen,email:mailashallendesign.co.uk,apiToken:secret,);$serialized serialize($user);序列化字符串可能如下所示为简洁起见缩短了加密字符串O:8:App\User:3:{s:4:name;s:9:Ash Allen;s:5:email;s:25:mailashallendesign.co.uk;s:8:apiToken;s:200:eyJpdiI6Ikx0N3BDQwYzcwMzE1NGQy...sdfsfsfdssInRhZyI6IiJ9;}正如我们所看到的apiToken属性现在已加密没有加密密钥就无法解密数据。现在如果我们想从序列化字符串创建App\User类的实例我们可以对字符串调用unserialize将调用__unserialize方法。此__unserialize方法接受序列化数据的数组因此我们可以分配每个属性并解密apiToken属性。测试你的序列化代码就像应用程序的任何其他部分一样如果你正在自定义对象的序列化和反序列化方式你可能会想要为序列化逻辑编写测试。这是确保你的序列化代码按预期工作并且你可以捕获任何错误的好方法。例如让我们看看我们刚刚看过的前面的例子。如果我们意外地从apiToken属性的__unserialize()函数中删除了decrypt()函数调用会发生什么这将导致我们拥有一个具有加密令牌而不是我们期望的原始未加密值的对象。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2480995.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!