PHP备忘录模式核心是存状态而非对象,应手动实现saveState()/restoreState()方法,仅保存业务属性,用数组而非serialize(),配合SplStack支持多级撤销,避免资源句柄、只读属性等不可序列化项。

PHP 备忘录模式的核心不是“存对象”,而是“存状态”
直接序列化整个对象(serialize())看似简单,但多数场景下会出问题:资源句柄、闭包、PDO 连接、循环引用都会导致失败或不可恢复。备忘录模式的本意是让对象自己决定哪些字段可被快照,不暴露内部结构,也不依赖外部序列化机制。
关键判断:如果你只是想“回滚到上一个值”,用数组或 stdClass 存关键属性就够了;只有当对象有复杂不变性约束、需跨多个方法调用保持一致性时,才值得实现完整备忘录类。
如何手动实现轻量备忘录(无第三方库)
不写 Memento 类,改用对象自身提供 saveState() 和 restoreState() 方法,最简可控。
- 只保存业务相关属性,跳过
$this->db、$this->logger等依赖项 - 用
array_keys(get_object_vars($this))辅助识别可存字段,但别直接全量 dump - 状态快照建议用
array而非serialize(),便于调试和兼容性(如 PHP 8.1+ 的只读属性限制) - 恢复时用
foreach ($state as $k => $v) { $this->$k = $v; },避免__set()触发副作用
// 示例:订单编辑器的简易备忘录
class OrderEditor {
public $id;
public $status;
public $items = [];
private $backup = [];
public function saveState(): void {
$this->backup = [
'status' => $this->status,
'items' => $this->items,
];
}
public function restoreState(): void {
if (empty($this->backup)) return;
$this->status = $this->backup['status'];
$this->items = $this->backup['items'];
}
}
用 SplStack 管理多级撤销(Undo/Redo 场景)
单次备份不够?需要 Ctrl+Z 多步回退?别手写链表,用 SplStack 更可靠——它天然 LIFO,且不触发魔术方法,比数组 array_push()/array_pop() 更语义清晰。
立即学习“PHP免费学习笔记(深入)”;
- 每次
saveState()前先调用$stack->push($this->captureState()),而不是覆盖旧值 - 注意内存:长期运行的服务中,栈长度需设上限(如最多 50 步),超限时
array_shift()丢弃最早记录 -
SplStack存的是数组快照,不是对象引用,不存在意外共享修改的问题 - 不要把
PDOStatement或Closure放进栈里——它们无法被正确序列化或比较
常见崩溃点:DateTime、SplObjectStorage、__sleep() 干扰
哪怕你只存了 DateTime 实例,在反序列化或深拷贝时也可能失效——因为 DateTime 内部持有时区指针,PHP 不保证跨序列化还原一致性。
- 存
$dt->format('c')字符串,恢复时用new DateTime($str) -
SplObjectStorage必须手动遍历导出键值对,不能直接serialize() - 如果类定义了
__sleep(),确保它只返回真实可序列化的属性名,漏掉会导致Notice: serialize(): "xxx" returned as member variable from __sleep() but does not exist - PHP 8.2+ 的只读属性(
readonly)在恢复时会报Fatal error: Cannot assign to readonly property,必须用反射绕过或改用构造器重建
真正难的不是“怎么存”,而是“哪些不该存”——多数线上事故,都源于把本该由外部管理的状态(如数据库连接、HTTP 客户端)硬塞进了备忘录。











