绝大多数场景下不应单独写memento类,而应在业务对象中用savestate()和restorestate()方法以数组存关键属性;因90%回滚只需改几个字段,标准三角色结构反而增加耦合、隐藏序列化风险,且pdo、closure、datetime等不可靠项无法安全序列化。

PHP 实现备忘录模式,绝大多数场景下不该写 Memento 类,而应直接在业务对象里加 saveState() 和 restoreState() 方法,用数组存关键属性。 因为真实项目里 90% 的“回滚需求”只是改几个字段值,不是要完整冻结整个对象图;强行套标准三角色结构(Originator / Memento / Caretaker)反而增加耦合、隐藏序列化风险、且无法规避 PDOStatement、Closure、DateTime 等不可靠项。
为什么不要直接 serialize($this)
看似最省事,但实际踩坑率极高:
• PDO 连接、fopen() 返回的 resource 在反序列化后失效,甚至报 Serialization of 'PDO' is not allowed
• Closure 无法序列化,会直接抛出致命错误
• DateTime 对象虽可序列化,但还原后时区指针可能错位,var_dump() 看不出问题,->format('c') 却输出错误时间
• 循环引用(如父子对象互持)会让 serialize() 进入无限递归或静默截断
• 所有 private 属性若含资源句柄或只读逻辑,恢复后对象处于半损坏状态,调用方法可能崩溃
怎么安全地保存和恢复状态
核心原则:只存你明确知道需要回滚的字段,其他一律忽略。
• 在业务类中定义 saveState(),返回一个纯数组,例如:
public function saveState(): array
{
return [
'name' => $this->name,
'status' => $this->status,
'updated_at' => $this->updated_at instanceof DateTime ? $this->updated_at->format('Y-m-d H:i:s') : null,
];
}
• 对应的
restoreState(array $state) 用 foreach 直接赋值,绕过 __set() 或 setter 方法触发副作用:public function restoreState(array $state): void
{
foreach ($state as $key => $value) {
if (property_exists($this, $key)) {
$this->$key = $value;
}
}
}
• 时间字段统一转字符串存储;资源型属性(如
$this->pdo)根本不上数组;计算属性(如 getFullName())不存
多级撤销用 SplStack 而不是数组堆栈
如果需要支持多次 undo,用 SplStack 存数组快照比用普通数组更可靠:
• SplStack 是值拷贝,不是引用共享,修改当前对象不会意外污染历史快照
• 不依赖 array_push()/array_pop() 的索引管理,避免越界或顺序错乱
• 示例:
$history = new SplStack(); $history->push($obj->saveState()); // 保存 $obj->doSomething(); $obj->restoreState($history->pop()); // 恢复最近一次
• 注意:不要往
SplStack 里 push DateTime 实例或任何对象,只 push 数组、字符串、数字、null
哪些情况才值得写完整 Memento 类
仅当同时满足以下全部条件:
• 对象存在强不变性约束(比如金额 + 账户状态必须同步变更,拆开存会破坏一致性)
• 多个方法调用之间需跨生命周期保持中间态(如分步表单提交,每步都可能回退)
• 业务逻辑明确要求“快照即对象”,且该对象不持有任何外部依赖(无 PDO、无文件句柄、无闭包)
• 团队已建立严格规范,确保所有 Memento 构造函数只接收数组并做白名单校验,不接受原始对象传入
即使满足,也建议把 Memento 设为 final,禁止继承,避免子类偷偷引入不可序列化属性
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











