PHP设计模式-单例模式
一、单例模式核心定义单例模式Singleton Pattern是创建型设计模式的典型代表核心目标是确保一个类在当前运行生命周期内有且仅有一个实例并对外提供统一的全局访问点。在不同 PHP 运行模式下单例的作用域不同FPM 运行模式单例作用域为单次请求请求结束后所有实例销毁无法跨请求复用CLI 长驻进程含 Swoole/Workerman单例贯穿整个进程生命周期进程不退出则实例一直存在可跨请求 / 协程共享。二、单例模式核心三要素实现单例的核心是从「创建、复制、反序列化」三个维度封闭实例生成入口同时提供全局唯一访问方式唯一实例存储类内部用静态属性保存自身唯一实例封闭创建入口构造函数私有化禁止外部通过new直接创建同时禁止克隆、禁止反序列化多维度保障唯一性全局访问方法提供静态方法作为唯一获取实例的入口。三、两种经典实现方式单例分为懒汉式和饿汉式两种核心区别在于实例创建的时机。但受 PHP 语言特性限制两种实现的适用场景和 Java 等语言有明显差异。3.1 懒汉式单例最常用实例在第一次调用时才创建属于延迟加载适合实例初始化成本高、不一定会用到的场景是 PHP 生态的主流实现。版本要求PHP 7.4支持可空类型声明?php class Singleton { // 保存唯一实例的静态属性初始为null表示尚未创建 // ?self 表示变量只能是当前类实例 或 null private static ?self $instance null; /** * 私有构造禁止外部 new 直接创建实例 */ private function __construct() { // 实例初始化逻辑 } /** * 全局唯一获取入口 * 注意这里用 self子类继承时仍返回父类实例如需支持子类单例请改用 static */ public static function getInstance(): self { if (self::$instance null) { self::$instance new self(); } return self::$instance; } /** * 禁止克隆外部克隆时抛出自定义异常 * 设为 public 是为了拦截 clone 操作并抛出友好提示若设为 private 会直接触发 PHP 致命错误 */ public function __clone(): void { throw new \RuntimeException(禁止克隆单例对象); } /** * 禁止反序列化防止 unserialize 绕过构造函数创建实例 * 兼容 PHP 7.4 以下的反序列化魔术方法 */ public function __wakeup(): void { throw new \RuntimeException(禁止反序列化单例对象); } /** * 禁止反序列化PHP 7.4 新增的反序列化魔术方法 * 双重防护避免通过实现 __unserialize 绕过 __wakeup */ public function __unserialize(array $data): void { throw new \RuntimeException(禁止反序列化单例对象); } } // 验证多次调用返回同一个对象 $instance1 Singleton::getInstance(); $instance2 Singleton::getInstance(); var_dump($instance1 $instance2); // bool(true)关键说明self vs static核心差异self绑定在定义时的类static绑定在调用时的类晚静态绑定Late Static BindingsLSB。上面代码用self绑定子类继承后调用getInstance()仍返回父类实例适合不需要继承的最终单例类如果需要基类复用、支持子类单例需将self替换为static利用 PHP 后期静态绑定public static function getInstance(): static { if (static::$instance null) { static::$instance new static(); } return static::$instance; }注意用static时子类必须重新声明$instance静态属性否则多个子类会共享同一个实例。3.2 饿汉式单例PHP 场景不推荐饿汉式的核心是「类加载时自动创建实例」但 PHP 语法存在限制类的静态属性初始化仅支持常量表达式不允许直接实例化对象因此无法实现 Java 式的原生饿汉。PHP 生态中所谓的 “饿汉式” 本质是手动预初始化需要在程序入口主动调用初始化方法并非类加载时自动创建。?php class EagerSingleton { // 静态属性仅声明无法直接 new self() 初始化 private static self $instance; /** * 程序入口处手动调用完成实例初始化 * 必须在 getInstance() 之前调用否则会返回未初始化对象 */ public static function init(): void { self::$instance new self(); } private function __construct() { // 初始化逻辑 } public static function getInstance(): self { return self::$instance; } public function __clone(): void { throw new \RuntimeException(禁止克隆单例对象); } public function __wakeup(): void { throw new \RuntimeException(禁止反序列化单例对象); } public function __unserialize(array $data): void { throw new \RuntimeException(禁止反序列化单例对象); } } // 程序入口处手动初始化 EagerSingleton::init();为什么 PHP 不推荐饿汉式Java 场景JVM 启动一次类只加载一次饿汉一次性创建全局复用天生线程安全PHP-FPM 场景即使开启 OPcache 缓存字节码每次请求结束后实例仍会销毁“一次创建” 的优势完全消失只要类被加载不管用不用都提前创建反而白白消耗本次请求内存CLI 长驻场景本质和 “先调用一次懒汉” 没有区别没有额外收益。四、使用场景举例数据库连接单例单例最典型的应用是资源连接管理避免重复创建连接、减少资源开销。?php class SingletonDatabase { private static ?self $instance null; private PDO $connection; /** * 构造函数私有化初始化数据库连接 */ private function __construct() { $this-connect(); } /** * 封装连接逻辑构造函数和重连逻辑共用 */ private function connect(): void { $dsn sprintf( mysql:host%s;dbname%s;charsetutf8mb4, 127.0.0.1, test ); try { $this-connection new PDO($dsn, root, 123456, [ PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES false, // 禁用模拟预处理防SQL注入 PDO::ATTR_PERSISTENT false, // 非持久连接长驻进程建议关闭 ]); } catch (\PDOException $e) { throw new \RuntimeException(数据库连接失败: . $e-getMessage(), $e-getCode(), $e); } } /** * 获取数据库单例 */ public static function getInstance(): self { if (self::$instance null) { self::$instance new self(); } return self::$instance; } /** * 获取 PDO 连接对象 */ public function getConnection(): PDO { return $this-connection; } /** * 长驻进程专用检测连接是否存活断线则自动重连 */ public function ping(): bool { try { $this-connection-query(SELECT 1); return true; } catch (\PDOException $e) { // 断线重连 $this-connect(); return true; } } public function __clone(): void { throw new \RuntimeException(禁止克隆单例对象); } public function __wakeup(): void { throw new \RuntimeException(禁止反序列化单例对象); } public function __unserialize(array $data): void { throw new \RuntimeException(禁止反序列化单例对象); } } // 全程只建立一次数据库连接 $db SingletonDatabase::getInstance()-getConnection(); // 执行业务查询1 $stmt $db-prepare(SELECT id, user_name FROM user WHERE status ? LIMIT 10); $stmt-execute([1]); $userList $stmt-fetchAll(); print_r($userList); // 执行业务查询2 $stmt $db-prepare(SELECT id, user_name FROM user WHERE status ? LIMIT 10); $stmt-execute([2]); $userList $stmt-fetchAll(); print_r($userList);长驻进程注意事项CLI/Swoole 等长驻场景下MySQL 连接会因wait_timeout超时断开建议在每次查询前调用ping()检测并重连协程场景风险Swoole/Workerman 协程环境下单例是进程级共享的同一连接会被多个协程共用存在数据串扰风险高并发场景下推荐使用连接池替代单例。五、适用场景与局限性5.1 适用场景数据库 / Redis 连接避免重复创建连接减少资源消耗配置管理器全局统一读取和管理配置保证配置一致性日志写入器统一日志写入入口避免多次打开文件句柄请求上下文FPM 场景单次请求内的全局状态管理对象工厂工厂类本身通常做成单例统一对象创建逻辑。5.2 局限性与缺点测试困难单例是全局状态无法轻松 mock不利于单元测试和解耦隐藏依赖调用方直接通过getInstance()获取实例依赖关系不透明不利于代码维护违反单一职责单例类既要管理自身实例又要承担业务逻辑职责不单一长驻进程风险全局状态贯穿整个进程容易引发数据串扰、资源过期问题协程场景风险更突出无法绝对防护通过 PHP 反射机制仍可创建新实例单例的唯一性是工程层面的约定无法从语法上绝对禁止现代替代方案现代化 PHP 开发更推荐使用 DI依赖注入容器实现 “单例效果”既保留全局唯一实例的优势又解决了测试困难、依赖隐藏的问题。