单例模式必须防住 clone 和 __wakeup;仅实现 getInstance() 不足,需私有化 __clone() 并抛异常、__wakeup() 中禁止反序列化,否则克隆或反序列化将绕过构造函数创建新实例。

单例模式必须防住 clone 和 __wakeup
PHP 单例不是写个 getInstance() 就完事。FPM 每次请求是独立进程,但攻击者或疏忽代码仍可能绕过单例——比如用 clone $obj 或 unserialize($payload) 造出第二个实例。一旦数据库连接类被克隆,就可能开两个连接却不共享事务状态。
-
__clone()必须显式抛异常,不能留空或注释掉 -
__wakeup()里要throw new \Exception('Unserializing singleton is not allowed.');,不能只写return; - 判空用
self::$instance === null,不用!isset(self::$instance),避免被unset()后误判为未初始化
工厂模式别硬写 switch-case,改用配置映射
硬编码 switch ($type) 的工厂在新增支付渠道、缓存驱动或数据库类型时就得改核心类,违反开闭原则。企业级项目里,工厂的职责是“按名取类”,不是“按名分支”。
- 把类型和类名的映射抽到配置数组里:
['redis' => RedisCache::class, 'apcu' => ApcuCache::class] - 工厂方法接收字符串标识后,直接查表并
new $class(),不写任何if或case - 若需依赖注入(比如构造时传
$config),工厂本身应由容器管理,而不是静态方法——否则测试时无法 mock,也无法预设参数
观察者注册别强求接口,优先用闭包或 callable
PHP 内置的 SplSubject/SplObserver 接口太窄:不支持事件名过滤、没优先级、不能异步、回调参数固定。真实业务中,你常需要监听 'order.paid' 而忽略 'order.cancelled',内置接口做不到。
- 自定义
EventDispatcher更实用:支持字符串事件名、任意数据传参、多个回调绑定 - 注册观察者时用
[$object, 'onPaid']或function ($event) { ... },而非强制实现某个接口 - 注意内存泄漏:闭包里若用了
use ($this),又没手动detach(),对象在请求结束前不会释放
单例只在单次请求内保唯一,跨请求复用是陷阱
很多人误以为单例能跨 FPM 请求复用,其实不能。PHP-FPM 每个请求启动新进程,static 属性随进程销毁。所谓“全局唯一”,仅限当前请求生命周期。
立即学习“PHP免费学习笔记(深入)”;
真正容易被忽略的是边界问题:单例里的 PDO 连接要不要 close()?工厂返回的临时对象要不要 unset()?观察者回调里 throw 异常会不会中断主流程?这些细节不厘清,模式就从解药变成隐患。



















