首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP 單例模式完整實作:資料庫連線、配置管理、日誌器的最佳實踐

     編輯:PHP 相關知識

單例模式確保類別在全域僅有一個實例,並提供全域存取點。PHP 中常用於資料庫連線池、配置管理、日誌器等「共享資源」場景。本文提供標準實作、執行緒安全說明、常見反模式、測試策略與現代替代方案 (依賴注入容器)。

標準單例基類 (PHP 7.4+)

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];
    }
}

實戰範例:PDO 資料庫單例

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);

日誌器單例 (PSR-3 相容)

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

現代最佳實踐:DI 容器管理單例

// 容器設定 (以 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);

何時仍適合使用單例?

  • 簡單腳本、CLI 命令、無 DI 容器的舊專案重構過渡期。
  • 真正的「全域唯一資源」:如單一檔案鎖、硬體裝置介面。
  • 效能極度敏感、無狀態工具類 (如 Str::uuid() 靜態方法即可,無需單例)。

單例模式是雙面刃:正確使用能簡化共享資源存取,濫用會導致難以測試、維護的義大利麵代碼。現代 PHP 專案應優先採用 依賴注入容器管理單例生命週期,保持類別純粹、可測試、可替換。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved