PHP序列化的四种实现方法与横向对比
前言先说清楚一件事「PHP 序列化的四种实现方法」并不是官方给出的分类。PHP 手册里没有这样一节官方只有在「对象序列化」章节里介绍了serialize()/unserialize()这一套机制。这个「四种」的说法来自社区的口头归纳通常指的是下面四条互不相同的路线内置的serialize()/unserialize()内置的json_encode()/json_decode()var_export()配合include注意它产出的是PHP 代码不是数据需要额外安装的第三方扩展比如 igbinary、msgpack。把它当成一份「候选清单」来看是对的当成「官方标准」来看就会出错。这四者的差异不只是速度更关键的是三件事类型是否保真、能不能跨语言读、以及还原时会不会执行代码。第 3 条的答案最危险——include一段var_export()产物本质上是在执行代码只要那段文本能被外部影响就是代码执行漏洞。本文按「机制 → 横向对比 → 可跑的实测脚本 → 选择清单」展开所有函数签名都以官方手册为准。一、四条路线各自的机制1. serialize() / unserialize()PHP 亲儿子。它保留完整的类型信息int和123不会混支持数组、对象、引用和循环引用输出是可能含空字节的二进制串。代价是只有 PHP 能读且还原对象时会调用类里的魔术方法因此对不可信输入极其危险。官方手册对它的定位是「生成可存储的表示」并且明确建议如果需要把数据交给用户请改用 JSON。?php // 适用于 PHP 7.0$value [id 1001, vip true, name ann];$raw serialize($value);echo $raw, PHP_EOL;// a:3:{s:2:id;i:1001;s:3:vip;b:1;s:4:name;s:3:ann;}2. json_encode() / json_decode()输出纯文本、UTF-8、跨语言通用是接口之间的默认选择。它的边界也很清楚只有标量、数组、对象能编码官方手册写明「值可以是除 resource 之外的任意类型」对象默认只输出 public 属性想让类自己决定怎么变成 JSON需要实现JsonSerializable接口数字类型有损超过PHP_INT_MAX的整数解码回来会变成 float需要JSON_BIGINT_AS_STRING浮点输出受serialize_precision影响手册明确写了这一点它不会按类名实例化你的类因此对不可信输入要安全得多。?php // 适用于 PHP 7.3JSON_THROW_ON_ERROR 需要 7.3$json json_encode([id 1001, vip true], JSON_THROW_ON_ERROR);echo $json, PHP_EOL; // {id:1001,vip:true}// 第二个参数决定 JSON 对象解码成关联数组true还是 stdClassfalse/nullvar_dump(json_decode($json, true));3. var_export() 配合 include签名是var_export(mixed $value, bool $return false): ?string第二个参数传true时返回字符串而不直接输出。它产出的是合法的 PHP 代码常见于生成配置文件、写缓存文件。官方手册在 Notes 里给了三条重要限制资源类型无法被导出它不处理循环引用因为几乎不可能为循环结构生成可解析的 PHP 代码要处理这种数据请用serialize()而且所有被导出的对象都必须实现静态方法__set_state()唯一的例外是stdClass它用「数组转对象」的形式导出。另外PHP 8.2.0 之前的版本在导出命名空间类时不会带上开头的反斜杠跨命名空间求值时容易出错。?php // 适用于 PHP 8.0$config [debug false, level 3, hosts [a.test, b.test]];$code var_export($config, true);$file sys_get_temp_dir() . DIRECTORY_SEPARATOR . demo_config.php;file_put_contents($file, ?php return . $code . ;);$loaded include $file;var_dump($loaded $config);unlink($file);4. 第三方扩展igbinary、msgpackigbinary 是一个把 PHP 结构编成紧凑二进制流的扩展手册里有igbinary_serialize(mixed $value): string|false与igbinary_unserialize(string $str): mixed两个函数它同时可以作为 session 与 Memcached/Redis 的序列化器使用。msgpack 走的是跨语言的二进制路线函数名以该扩展自己的文档为准它不是 PHP 内置的一部分。这两者的共同点是必须先安装扩展在php.ini里开启或用包管理器装部署环境、版本升级都要考虑用之前先function_exists()判断缺扩展时要有降级路径。?php // 适用于 PHP 7.0需要安装 igbinary 扩展$value [id 1001, name ann];if (!function_exists(igbinary_serialize)) {exit(当前环境没有安装 igbinary 扩展 . PHP_EOL);}$packed igbinary_serialize($value);var_dump(is_string($packed), igbinary_unserialize($packed) $value);二、横向对比维度serialize()json_encode()var_export() includeigbinary / msgpack输出形态二进制串UTF-8 文本PHP 代码二进制流类型保真完整int/float/string 区分数字只有一种大整数会退化完整还原后就是原值与 serialize 接近跨语言否是否msgpack 是igbinary 否对象支持保存属性触发魔术方法默认只输出 public 属性需要__set_state()保存属性循环引用支持不支持不支持igbinary 支持还原时执行代码会实例化类、跑魔术方法不会会执行文件里的代码同 serialize外部依赖无无JSON 扩展默认开启无需要额外安装扩展适合场景PHP 内部缓存、队列、session接口、日志、跨语言交换可信来源的配置/代码生成追求体积与速度的 PHP 内部缓存三、可跑的实测脚本下面这段代码把同一个数据结构用四条路线各走一遍打印体积并逐个验证「能还原成原样」。体积那一列是你自己机器上跑出来的结果不同 PHP 版本和扩展版本会有差异请以实际输出为准。?php // 适用于 PHP 8.0JSON_THROW_ON_ERROR 需要 7.3declare(strict_types1);$order [id 1001,user [name ann, vip true],total_cents 1999,tags [php, json],];$payloads [];// 1) serialize()$payloads[serialize] serialize($order);// 2) json_encode()$payloads[json] json_encode($order, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);// 3) var_export()注意它产出的是代码$payloads[var_export] var_export($order, true);// 4) 第三方扩展装没装决定了这一行是不是降级提示$payloads[igbinary] function_exists(igbinary_serialize)? igbinary_serialize($order): (未安装 igbinary 扩展);foreach ($payloads as $name $payload) {printf(%-12s %5d 字节\n, $name, strlen((string)$payload));}// 逐个还原并比对var_dump(serialize , unserialize($payloads[serialize]) $order);var_dump(json , json_decode($payloads[json], true, 512, JSON_THROW_ON_ERROR) $order);$file sys_get_temp_dir() . DIRECTORY_SEPARATOR . exported_order.php;file_put_contents($file, ?php return . $payloads[var_export] . ;);var_dump(var_export , (include $file) $order);unlink($file);体积差异的原因值得记住但不要记具体倍数serialize()要为每个元素写类型标记和长度var_export()要写成带引号和缩进的 PHP 代码通常最臃肿JSON 去掉了类型标记但要把非 ASCII 字符转义加JSON_UNESCAPED_UNICODE可以避免而 igbinary 是为紧凑设计的二进制格式。真正的性能差异取决于数据形态、是否命中 Opcache、以及缓存后端是否走网络不实测就不要下结论。常见坑点❌ 认为var_export()的输出是「数据」把它和用户输入拼在一起再include或eval。✅ 它产出的是 PHP 代码include等于执行代码只能用于完全可信的来源。跨系统的数据交换用 JSON。❌ 用json_encode()编码一个对象后发现部分属性不见了。✅ 默认只输出 public 属性需要输出全部则实现JsonSerializable::jsonSerialize()不要靠(array)强制转换转换后私有属性名会带上\0前缀。❌ 用var_export()保存带循环引用的结构或者保存对象后直接include回来。✅ 手册明确说var_export()不处理循环引用导出对象还要求类实现静态__set_state()只有stdClass例外它导出成数组转对象的形式。❌ 把serialize()的产物当作「可安全传给前端」的字符串。✅ 它可能在还原时实例化类并触发魔术方法手册的警告是「无论选项怎么设都不要把不可信输入交给unserialize()」。对外一律用 JSON。❌ 在没确认扩展是否安装的情况下直接调用igbinary_serialize()。✅ 先function_exists(igbinary_serialize)并在部署清单里写明依赖扩展缺失时应降级到serialize()而不是让页面 500。❌ 用md5()/sha1()给序列化串「盖个章」当作防篡改。✅ 需要防篡改用带密钥的hash_hmac()比较用hash_equals()md5()/sha1()都不适合做完整性校验更绝对不能用来存密码存密码用password_hash()/password_verify()。❌ 为了「体积小」手动截断 JSON 或序列化串。✅ 两者都是有结构的编码截断必然损坏限制体积要靠精简字段、加JSON_UNESCAPED_UNICODE之类的正确手段或者换更紧凑的格式。❌ 在同一个项目里混用多种格式缓存键却不带格式标识。✅ 缓存键里带上格式名与版本号例如order:v2:json否则改格式那天会读出一堆「反序列化失败」。总结需求首选理由对外接口、跨语言交换json_encode()/json_decode()文本、通用、不会按类名实例化对象PHP 内部缓存、队列、sessionserialize()或 igbinary类型保真、支持引用与循环结构生成配置或代码文件var_export()产物是可直接include的 PHP 代码仅限可信来源追求体积与速度igbinary / msgpack二进制紧凑格式但需自行安装与维护扩展四者之间的关系不是「谁取代谁」而是「谁适合哪条边界」只要数据要跨越你的信任边界面向用户、面向其他语言、面向其他团队答案就是 JSON只要数据只在你的 PHP 代码之间往返serialize()或 igbinary 才是更保真的选择而var_export()输出的是代码而不是数据这一点决定了它只能待在极窄的可信场景里。