登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  php教程

PHP 反序列化后为什么得到 __PHP_Incomplete_Class

来源:17golang原创

时间:2026-11-08 00:22:15 199浏览 收藏

PHP 反序列化后出现 __PHP_Incomplete_Class,通常不是对象属性突然丢失,而是 PHP 找不到序列化数据所指向的原类,或者 allowed_classes 明确把这个类排除在外。先记住判断顺序:看序列化数据里的类名,再看当前请求是否加载了这个类,最后看反序列化选项是否允许它。

官方手册:https://www.php.net/manual/en/class.php-incomplete-class.php

这个占位对象表示“对象数据读到了,但原来的业务类没有按当前规则恢复”。修复重点是补齐类加载或调整明确的白名单;如果数据来自用户或外部系统,不要为了恢复对象而放宽到任意类,优先改用 JSON 等数据格式。
  • 类未定义、命名空间不一致或自动加载没有生效,会让对象退化为不完整对象。
  • allowed_classes 为 false,或数组里没有目标类,同样会产生不完整对象。
  • unserialize() 返回 false 并伴随 Warning,属于输入格式失败,和不完整对象不是同一条分支。

先看它为什么不是原来的业务类

序列化对象时,字符串中会带有类名。反序列化时,PHP 需要根据这个名字找到当前进程中的类定义;如果类不存在,就只能保留已读到的属性,并用 __PHP_Incomplete_Class 表示“没有完成业务类恢复”。这个类不是应该由业务代码主动创建的模型类,而是 PHP 为异常恢复状态提供的占位。

PHP 反序列化时原类对象与不完整对象的关系说明图
图1:对象类型关系说明图;类未加载或不在白名单时,PHP 会保留对象数据但使用不完整对象占位。

可以先用一个不存在的类名观察这个现象。下面的字符串只用于说明诊断边界,不代表生产代码应该手工拼接序列化数据。

__PHP_Incomplete_Class_Name);
}
?>

如果输出的原始类名是 MissingProfile,说明排查方向应放在类文件、命名空间和自动加载,而不是先去修改属性名。实际项目中也可能是发布顺序造成的:写入数据的服务已经部署了新类,读取数据的服务还没有加载对应版本。

把类未加载和白名单排除分开

第二个常见原因是调用方主动限制了可恢复的类。allowed_classes 可以是布尔值或类名数组;设置为 false 时不接受对象类,设置为数组时只接受列出的类。目标类没有出现在数组里,即使类文件已经被加载,也会得到 __PHP_Incomplete_Class。

 false]);
var_dump(get_class($blocked));

// 只允许名单中的类;名单不包含 UserProfile 时同样会被排除。
$limited = unserialize($payload, ['allowed_classes' => [DateTimeImmutable::class]]);
var_dump(get_class($limited));
?>

这一步最容易误判:如果把所有问题都归因于自动加载,就会漏掉调用方的安全配置;如果为了让旧代码继续运行而直接把白名单改成“全部允许”,又会失去类过滤的意义。诊断时应把“类是否已加载”和“规则是否允许”分别记录。

按三个证据点定位原因

排查不需要猜测,可以固定看三个证据点:

  1. 序列化数据中的类名:从 __PHP_Incomplete_Class_Name 或数据生产端的类定义确认完整类名,特别注意命名空间大小写和反斜杠。
  2. 当前进程的类加载状态:检查 Composer 自动加载入口是否在反序列化之前执行,或者确认手工 require_once 的文件确实定义了同名类。
  3. allowed_classes 规则:沿调用链找到真正执行 unserialize() 的位置,确认是否为 false,或数组中是否漏掉了目标类。
PHP 反序列化三类证据点排查关系说明图
图2:排查关系说明图;先核对数据里的类名,再核对加载状态,最后核对 allowed_classes。

如果三项都正常,却仍然出现不完整对象,再检查读取端和写入端是否使用了不同命名空间、不同版本的类名,或者当前请求是否走了另一条缓存/队列消费路径。不要只在数据库里搜索属性值;这个问题首先是类解析问题。

修复时优先恢复明确的类边界

对于可信的内部存量数据,修复通常有两步:在反序列化之前完成自动加载,并只把业务上确实需要恢复的类放入白名单。下面示例用类型判断阻止不完整对象继续进入业务逻辑。

 [UserProfile::class],
]);

// 类型不符合预期时立即停在边界,避免继续访问业务属性。
if (!$value instanceof UserProfile) {
    throw new UnexpectedValueException('对象类型不是 UserProfile');
}

echo $value->name;
?>

如果数据来自请求参数、Cookie、上传文件或不受信任的外部系统,不应把“恢复成功”当成目标。PHP 手册明确提醒不要对不可信输入使用 unserialize();这类场景应把数据模型改成 JSON,并在读取后做字段和类型校验。allowed_classes 是缩小对象恢复范围的措施,不是把不安全输入变安全的替代品。

最后用类型和原始类名复查

修复后不要只看页面没有报错,而要确认返回值已经是预期类。可以保留一段很小的边界记录:业务对象进入服务层前,使用 instanceof 判断;遇到不完整对象时记录原始类名和数据来源,不要把它重新序列化后继续传递。

name;
} elseif ($value instanceof __PHP_Incomplete_Class) {
    // 原始类名用于定位加载或白名单问题,不用于绕过限制。
    $className = $value->__PHP_Incomplete_Class_Name ?? 'unknown';
    error_log('Incomplete serialized class: ' . $className);
} else {
    // 其他类型可能来自错误输入或版本不兼容,需要单独处理。
    throw new UnexpectedValueException('反序列化结果类型不符合预期');
}
?>

常见问题

出现 __PHP_Incomplete_Class 是不是说明序列化字符串损坏?

不一定。它更常见地表示类未定义或被 allowed_classes 排除;字符串格式损坏通常会走返回 false 和 Warning 的错误分支。

把 allowed_classes 改成 true 就能解决吗?

它可能绕过白名单排除,但不应该作为通用修复。先确认目标类、自动加载和数据来源,再按最小范围列出允许恢复的类。

能不能直接 new __PHP_Incomplete_Class?

不要这样设计业务代码。它是 unserialize() 在特定条件下创建的占位状态,正确做法是修复类加载/白名单边界,或改用适合外部数据的格式。

为什么开发环境正常,线上却出现这个对象?

重点比较两边的 Composer 自动加载入口、命名空间、部署版本和 allowed_classes 配置。写入端和读取端的类名不一致,也会造成同样现象。

小结

__PHP_Incomplete_Class 可以理解为 PHP 在反序列化过程中的“保留数据、暂停恢复”状态。先拿到原始类名,再确认类是否已加载,最后检查 allowed_classes;对可信内部数据修复明确的加载和白名单,对不可信外部数据则优先停止使用 unserialize(),改用 JSON 等数据交换格式。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>