數據庫連接、配置管理器、日誌記錄器、緩存客戶端——這些資源在進程生命周期內有且僅有一個實例最合適。單例模式通過私有構造函數、靜態工廠方法、靜態實例變量三要素,確保全局唯一訪問點。本文深度解析 PHP 實現細節、線程安全(偽)、依賴注入友好寫法、測試策略與現代替代方案。
<?php
final class DatabaseConnection {
// 1. 靜態實例持有器
private static ?self $instance = null;
// 2. 私有構造函數,防外部 new
private function __construct() {
$dsn = 'mysql:host=localhost;dbname=app;charset=utf8mb4';
$this->pdo = new PDO($dsn, 'user', 'pass', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
}
// 3. 防止克隆
private function __clone() {}
// 4. 防止反序列化創建新實例
public function __wakeup() {
throw new Exception("Cannot unserialize singleton");
}
// 5. 靜態工廠方法(全局訪問點)
public static function getInstance(): self {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
// 業務方法
public function getPdo(): PDO {
return $this->pdo;
}
private PDO $pdo;
}
// 使用
$pdo = DatabaseConnection::getInstance()->getPdo();
$stmt = $pdo->query('SELECT 1');
防止繼承破壞單例性(子類可創建多實例)。
序列化/反序列化、克隆操作均可能繞過構造函數創建新實例,必須顯式禁止。
PHP 單請求單線程(FPM/CLI),天然線程安全。Swoole/RoadRunner 多進程/協程環境下,進程隔離導致各進程擁有獨立單例,非跨進程共享。
// 延遲(首次調用創建)
public static function getInstance(): self {
return self::$instance ??= new self(); // PHP 7.4+ nullsafe 賦值
}
// 預加載 (opcache.preload)
/* preload.php */
DatabaseConnection::getInstance(); // 啟動時預熱
硬編碼 ::getInstance() 導致難以 Mock、單元測試耦合。解決方案:工廠注入 + 介面綁定。
<?php
// 1. 定義介面
interface DatabaseInterface {
public function getPdo(): PDO;
}
// 2. 單例實現介面
final class DatabaseConnection implements DatabaseInterface {
// ... 同上,實現 DatabaseInterface ...
// 允許測試時注入替換
public static function setInstance(self $instance): void {
self::$instance = $instance;
}
public static function resetInstance(): void {
self::$instance = null;
}
}
// 3. 服務容器綁定 (PSR-11)
$container->bind(DatabaseInterface::class, fn() => DatabaseConnection::getInstance());
// 4. 控制器構造函數注入
class UserController {
public function __construct(private DatabaseInterface $db) {}
public function index() {
$users = $this->db->getPdo()->query('SELECT * FROM users')->fetchAll();
}
}
// 5. 單元測試輕鬆 Mock
class DatabaseConnectionTest extends TestCase {
public function testQuery() {
$mockDb = $this->createMock(DatabaseInterface::class);
$mockPdo = $this->createMock(PDO::class);
$mockDb->method('getPdo')->willReturn($mockPdo);
DatabaseConnection::setInstance($mockDb); // 注入 Mock
$controller = new UserController($mockDb);
// ...
DatabaseConnection::resetInstance(); // 清理
}
}
Laravel/Symfony 等框架不使用單例模式,而是由服務容器管理單例生命周期:
<?php
// Laravel 服務提供者
class DatabaseServiceProvider extends ServiceProvider {
public function register() {
$this->app->singleton('db', function () {
return new PDO(config('database.dsn'), ...);
});
}
}
// 使用:容器解析自動返回同一實例
$pdo1 = app('db');
$pdo2 = app('db');
assert($pdo1 === $pdo2); // true
// 測試時輕鬆替換
$this->app->instance('db', $mockPdo);
| 維度 | 傳統單例 | 容器管理單例 |
|---|---|---|
| 測試性 | 困難(需反射/靜態重置) | 極簡(容器替換綁定) |
| 依賴方向 | 具體類耦合 | 介面依賴 |
| 生命周期控制 | 進程級 | 請求級/會話級/單例級可配置 |
| 擴展性 | 差 | 優(裝飾器、代理、AOP) |
按參數區分實例(如多數據庫連接):
final class DatabasePool {
private static array $instances = [];
public static function get(string $name = 'default'): PDO {
if (!isset(self::$instances[$name])) {
$config = Config::get("database.$name");
self::$instances[$name] = new PDO($config['dsn'], $config['user'], $config['pass']);
}
return self::$instances[$name];
}
}
單例模式核心是「受控的全局訪問」。PHP 中通過 private __construct、static $instance、static getInstance() 三件套實現。但現代架構強烈建議將單例管理權交給服務容器,實現介面依賴、易測試、生命周期可控。若必須手寫單例,請務必final、禁用__clone/__wakeup、提供setInstance/resetInstance供測試。記住:單例是「全局變量」的禮儀包裝,濫用即技術債。