PHP装饰器模式本质是手动实现的包装器类,通过组合与魔术方法(如__call)零侵入地动态增强对象行为,核心在于接口契约、委托调用、缓存键安全生成(如sha1(get_class().serialize($args)))及合理失效策略。

PHP 里没有原生装饰器语法,但可以用组合 + 魔术方法模拟
PHP 不支持 Python 那种 @cache 语法糖,所谓“装饰器模式”在 PHP 中本质是手动实现的包装器(Wrapper)类。核心思路是:让缓存逻辑藏在另一个对象里,它持有原始对象实例,并在调用目标方法前/后介入处理——不碰原类代码,也不用继承。
关键不是名字叫不叫“装饰器”,而是能否做到「零侵入」和「可复用」。下面这个结构最稳妥:
- 定义一个通用缓存包装器类,比如
CacheDecorator - 它通过构造函数接收任意实现了特定接口(如
CacheableInterface)或至少有约定方法签名的对象 - 重写
__call()拦截所有方法调用,查缓存 → 命中则返回,未命中则执行原方法 + 写缓存 - 缓存键生成必须包含类名、方法名、参数序列化值(注意
serialize()对资源/闭包会失败)
缓存键怎么生成才安全?别直接拼接 json_encode() 参数
常见错误是用 md5(json_encode($args)) 当缓存键——json_encode() 会丢顺序(关联数组键序不保证)、忽略浮点精度、对 NaN/Infinity 处理不一致,还可能因编码失败返回 false。
更可靠的做法是用 sha1(serialize($args)),前提是确保参数不含资源句柄、匿名函数或不可序列化对象。如果业务中确实存在这类参数,就得提前过滤或抛出异常。
立即学习“PHP免费学习笔记(深入)”;
示例片段:
// 在 __call() 中
$cacheKey = sha1(get_class($this->wrapped) . '::' . $method . serialize($args));
$value = $this->cache->get($cacheKey);
if ($value !== null) {
return $value;
}
$result = $this->wrapped->$method(...$args);
$this->cache->set($cacheKey, $result, $this->ttl);
return $result;
为什么不能直接代理所有方法?小心 __toString() 和 __get() 等魔术方法
__call() 只捕获未定义或不可访问的**方法调用**,对属性读写($obj->prop)、类型转换((string) $obj)、数组访问($obj['key'])完全无效。如果你的原类依赖这些行为,单纯靠 __call() 就会漏掉缓存机会,甚至引发 Notice。
可行方案只有两个:
- 要求被包装类显式实现接口(如
CacheableInterface),只允许缓存标记过的方法(例如加public function expensiveCalculation(): int),其余一律不拦截 - 若必须支持全量代理,就得同时实现
__get()、__set()、__toString()、__invoke()等——但代价高、易出错,且无法覆盖isset()或empty()的行为
Redis 缓存失效策略要匹配业务,别无脑设 TTL
简单设个 3600 秒 TTL 看似省事,但会导致脏数据:比如用户刚更新了配置,缓存却还要等一小时才刷新。真实场景中,缓存失效往往需要联动事件。
推荐做法是:给每个缓存键加业务标签(tag),比如 user:123:profile,再配合 Redis 的 DEL 或第三方库(如 phpredis 的 scan + del)批量清理。或者更轻量地,在关键更新操作后主动 delete 相关 key。
另外注意:PHP-FPM 下的 APCu 是进程级缓存,不适合多 worker 共享;而 Redis 虽然共享,但网络延迟和序列化开销会影响高频小数据场景——这时候得权衡是否值得缓存。
真正难的从来不是写个 CacheDecorator,而是决定哪些方法该缓存、缓存多久、失效时机怎么触发。这些决策没法靠模式自动完成,得看你的数据变更频率和一致性要求。



















