單例模式確保類別在全域僅有一個實例,並提供全域存取點。PHP 中常用於資料庫連線池、配置管理、日誌器等「共享資源」場景。本文提供標準實作、執行緒安全說明、常見反模式、測試策略與現代替代方案 (依賴注入容器)。
abstract class Singleton
{
private static array $instances = [];
// 禁止外部 new
final private function __construct() {
$this->init();
}
// 禁止 clone
final private function __clone() {}
// 禁止反序列化建立新實例
final public function __wakeup() {
throw new \Exception("Cannot unserialize singleton");
}
// 子類別可覆寫初始化邏輯
protected function init(): void {}
// 取得實例 (支援多子類別各自單例)
final public static function getInstance(): static
{
$class = static::class;
if (!isset(self::$instances[$class])) {
// 雙重檢查鎖定 (PHP 無真正執行緒,但防止遞迴初始化)
self::$instances[$class] = new static();
}
return self::$instances[$class];
}
}
class Database extends Singleton
{
private PDO $pdo;
protected function init(): void
{
$dsn = "mysql:host={$_ENV['DB_HOST']};dbname={$_ENV['DB_NAME']};charset=utf8mb4";
$this->pdo = new PDO($dsn, $_ENV['DB_USER'], $_ENV['DB_PASS'], [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_PERSISTENT => false, // 單例已複用連線,無需持久連線
]);
}
public function getPdo(): PDO
{
return $this->pdo;
}
// 代理常用方法
public function query(string $sql): PDOStatement
{
return $this->pdo->query($sql);
}
public function prepare(string $sql): PDOStatement
{
return $this->pdo->prepare($sql);
}
public function lastInsertId(string $name = null): string
{
return $this->pdo->lastInsertId($name);
}
public function beginTransaction(): bool
{
return $this->pdo->beginTransaction();
}
public function commit(): bool
{
return $this->pdo->commit();
}
public function rollBack(): bool
{
return $this->pdo->rollBack();
}
}
// 使用
$db = Database::getInstance();
$stmt = $db->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([1]);
$user = $stmt->fetch();
class Config extends Singleton
{
private array $data = [];
protected function init(): void
{
$this->data = require __DIR__ . '/../config/app.php'; // 回傳陣列的 PHP 檔
// 或解析 .env、YAML、JSON
}
public function get(string $key, $default = null)
{
return $this->data[$key] ?? $default;
}
public function set(string $key, $value): void
{
$this->data[$key] = $value;
}
public function all(): array
{
return $this->data;
}
}
// 使用
$debug = Config::getInstance()->get('app.debug', false);
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
class Logger extends Singleton implements LoggerInterface
{
private array $handlers = [];
protected function init(): void
{
$this->handlers[] = new StreamHandler(storage_path('logs/app.log'));
// 可加入 SlackHandler, SyslogHandler 等
}
public function log($level, $message, array $context = []): void
{
$record = ['level' => $level, 'message' => $message, 'context' => $context, 'datetime' => new DateTimeImmutable()];
foreach ($this->handlers as $handler) {
$handler->handle($record);
}
}
// PSR-3 便利方法
public function emergency($msg, array $ctx = []) { $this->log(LogLevel::EMERGENCY, $msg, $ctx); }
public function alert($msg, array $ctx = []) { $this->log(LogLevel::ALERT, $msg, $ctx); }
public function critical($msg, array $ctx = []) { $this->log(LogLevel::CRITICAL, $msg, $ctx); }
public function error($msg, array $ctx = []) { $this->log(LogLevel::ERROR, $msg, $ctx); }
public function warning($msg, array $ctx = []) { $this->log(LogLevel::WARNING, $msg, $ctx); }
public function notice($msg, array $ctx = []) { $this->log(LogLevel::NOTICE, $msg, $ctx); }
public function info($msg, array $ctx = []) { $this->log(LogLevel::INFO, $msg, $ctx); }
public function debug($msg, array $ctx = []) { $this->log(LogLevel::DEBUG, $msg, $ctx); }
}
// 使用
Logger::getInstance()->info('User logged in', ['user_id' => 123]);
| 缺點 | 影響 | 替代方案 |
|---|---|---|
| 全域狀態,難以單元測試 | 測試間相互干擾,難以 Mock | 依賴注入 (DI) 容器:PHP-DI, Symfony DI, Laravel Container |
| 隱式依賴,程式碼耦合度高 | 類別內部直接呼叫 ::getInstance() | 建構子注入介面:__construct(LoggerInterface $logger) |
| 違反單一職責 (自身管理生命週期) | 類別同時負責業務邏輯與實例管理 | 交由容器管理生命週期 (Singleton scope) |
| 多執行緒環境 (Swoole/RoadRunner) 不安全 | 靜態變數跨 Worker 共享,可能導致資料競爭 | 使用容器的 prototype 或 request scope |
// 容器設定 (以 PHP-DI 為例)
$container = new \DI\Container();
$container->set(PDO::class, function () {
return new PDO($dsn, $user, $pass, $options);
})->singleton(); // 容器保證單例
$container->set(LoggerInterface::class, function () {
return new Logger();
})->singleton();
// 控制器/服務自動注入
class UserService
{
public function __construct(
private PDO $db,
private LoggerInterface $logger
) {}
public function createUser(array $data): int
{
$this->logger->info('Creating user', $data);
// ...
}
}
// 解析
$userService = $container->get(UserService::class);
Str::uuid() 靜態方法即可,無需單例)。單例模式是雙面刃:正確使用能簡化共享資源存取,濫用會導致難以測試、維護的義大利麵代碼。現代 PHP 專案應優先採用 依賴注入容器管理單例生命週期,保持類別純粹、可測試、可替換。