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

PHP 單例模式完整實作指南:資料庫連線、配置管理、日誌記錄器實戰範例

     編輯:PHP 綜合知識

單例模式核心要素

  1. 私有靜態實例變數:儲存唯一實例
  2. 私有建構子:防止外部 new 建立實例
  3. 私有克隆方法:防止 clone 複製實例
  4. 私有反序列化方法:防止 unserialize() 重建實例 (PHP 7.4+)
  5. 公有靜態取得方法:getInstance() 提供全域存取點

標準單例基底類 (PHP 8.1+)

<?php
abstract class Singleton
{
    private static ?self $instance = null;

    final private function __construct() {}
    final private function __clone() {}
    final public function __wakeup() {
        throw new \Exception("不能反序列化單例");
    }

    final public static function getInstance(): self
    {
        if (self::$instance === null) {
            self::$instance = new static();
        }
        return self::$instance;
    }
}

// 使用:繼承即可
class Database extends Singleton {
    private PDO $pdo;

    protected function __construct() {
        $this->pdo = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');
        $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    }

    public function getConnection(): PDO {
        return $this->pdo;
    }
}

// 取得實例
$db = Database::getInstance()->getConnection();
?>

實戰一:資料庫連線單例 (PDO)

<?php
class Db
{
    private static ?self $instance = null;
    private PDO $pdo;

    private function __construct(array $config)
    {
        $dsn = "mysql:host={$config['host']};dbname={$config['dbname']};charset={$config['charset']}";
        $this->pdo = new PDO($dsn, $config['user'], $config['pass'], [
            PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES   => false,
            PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES {$config['charset']}",
        ]);
    }

    public static function getInstance(array $config = []): self
    {
        if (self::$instance === null) {
            if (empty($config)) {
                throw new \LogicException('首次呼叫需提供配置');
            }
            self::$instance = new self($config);
        }
        return self::$instance;
    }

    // 禁止克隆與反序列化
    private function __clone() {}
    public function __wakeup() {
        throw new \Exception("不支援反序列化");
    }

    // 代理 PDO 方法
    public function __call(string $method, array $args)
    {
        return $this->pdo->$method(...$args);
    }

    public function getPdo(): PDO
    {
        return $this->pdo;
    }
}

// 使用
// 啟動時初始化 (如 bootstrap.php)
Db::getInstance([
    'host' => 'localhost',
    'dbname' => 'myapp',
    'user' => 'dbuser',
    'pass' => 'secret',
    'charset' => 'utf8mb4',
]);

// 業務代碼任意處取得
$stmt = Db::getInstance()->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([1]);
$user = $stmt->fetch();
?>

實戰二:配置管理單例

<?php
class Config
{
    private static ?self $instance = null;
    private array $data = [];

    private function __construct(string $envFile = '.env')
    {
        $this->loadEnv($envFile);
        // 可再合併 config/*.php 陣列檔
    }

    public static function getInstance(string $envFile = '.env'): self
    {
        if (self::$instance === null) {
            self::$instance = new self($envFile);
        }
        return self::$instance;
    }

    private function loadEnv(string $file): void
    {
        if (!is_readable($file)) return;
        $lines = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
        foreach ($lines as $line) {
            $line = trim($line);
            if ($line === '' || str_starts_with($line, '#')) continue;
            if (strpos($line, '=') === false) continue;
            [$key, $value] = explode('=', $line, 2);
            $key = trim($key);
            $value = trim($value, " \t\n\r\0\x0B'\"");
            // 支援 ${VAR} 參考
            $value = preg_replace_callback('/\$\{([^}]+)\}/', function($m) {
                return $_ENV[$m[1]] ?? $_SERVER[$m[1]] ?? getenv($m[1]) ?? '';
            }, $value);
            $_ENV[$key] = $_SERVER[$key] = $value;
            $this->data[$key] = $value;
        }
    }

    public function get(string $key, $default = null)
    {
        return $this->data[$key] ?? $_ENV[$key] ?? $_SERVER[$key] ?? getenv($key) ?? $default;
    }

    public function set(string $key, $value): void
    {
        $this->data[$key] = $value;
    }

    public function all(): array
    {
        return $this->data;
    }

    private function __clone() {}
    public function __wakeup() { throw new \Exception("不支援"); }
}

// 使用
$appEnv = Config::getInstance()->get('APP_ENV', 'production');
$dbHost = Config::getInstance()->get('DB_HOST');
?>

實戰三:日誌記錄器單例 (PSR-3 相容)

<?php
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;

class Logger implements LoggerInterface
{
    private static ?self $instance = null;
    private string $logDir;
    private int $maxFiles = 30;

    private function __construct(string $logDir = 'logs')
    {
        $this->logDir = $logDir;
        if (!is_dir($logDir)) mkdir($logDir, 0755, true);
    }

    public static function getInstance(string $logDir = 'logs'): self
    {
        if (self::$instance === null) {
            self::$instance = new self($logDir);
        }
        return self::$instance;
    }

    public function log($level, $message, array $context = []): void
    {
        $level = is_string($level) ? strtoupper($level) : $level;
        $timestamp = date('Y-m-d H:i:s.u');
        $contextStr = $context ? ' ' . json_encode($context, JSON_UNESCAPED_UNICODE) : '';
        $line = "[$timestamp] [$level] $message$contextStr\n";

        $file = $this->logDir . '/' . date('Y-m-d') . '.log';
        file_put_contents($file, $line, FILE_APPEND | LOCK_EX);

        // 簡易輪轉:刪除 30 天前日誌
        $this->rotate();
    }

    private function rotate(): void
    {
        $files = glob($this->logDir . '/*.log');
        $cutoff = time() - ($this->maxFiles * 86400);
        foreach ($files as $file) {
            if (filemtime($file) < $cutoff) @unlink($file);
        }
    }

    // PSR-3 便利方法
    public function emergency($message, array $context = []) { $this->log(LogLevel::EMERGENCY, $message, $context); }
    public function alert($message, array $context = []) { $this->log(LogLevel::ALERT, $message, $context); }
    public function critical($message, array $context = []) { $this->log(LogLevel::CRITICAL, $message, $context); }
    public function error($message, array $context = []) { $this->log(LogLevel::ERROR, $message, $context); }
    public function warning($message, array $context = []) { $this->log(LogLevel::WARNING, $message, $context); }
    public function notice($message, array $context = []) { $this->log(LogLevel::NOTICE, $message, $context); }
    public function info($message, array $context = []) { $this->log(LogLevel::INFO, $message, $context); }
    public function debug($message, array $context = []) { $this->log(LogLevel::DEBUG, $message, $context); }

    private function __clone() {}
    public function __wakeup() { throw new \Exception("不支援"); }
}

// 使用
Logger::getInstance()->info('用戶登入', ['user_id' => 123, 'ip' => '1.2.3.4']);
Logger::getInstance()->error('資料庫連線失敗', ['error' => $e->getMessage()]);
?>

依賴注入容器整合 (現代框架做法)

現代框架 (Laravel、Symfony、Hyperf) 使用 **DI 容器管理單例生命週期**,而非手寫單例類:

<?php
// Laravel 服務提供者註冊
$this->app->singleton(Db::class, function ($app) {
    return new Db(config('database'));
});

// 控制器自動注入
class UserController {
    public function __construct(private Db $db) {}
    public function show(int $id) {
        return $this->db->prepare('SELECT * FROM users WHERE id=?')->execute([$id])->fetch();
    }
}
?>
  • 容器保證單例、管理依賴順序、支援介面綁定、便於測試 (Mock)。
  • 手寫單例類適合:無框架專案、舊代碼遷移、獨立套件、CLI 工具。

單例模式爭議與替代方案

爭議點說明替代
全域狀態難以測試、隱性耦合、併發風險DI 容器、工廠模式、服務定位器
單一職責違反類別同時負責業務邏輯與實例控制分離 InstanceManager
繼承困難final 方法阻止繼承覆寫組合優於繼承
記憶體洩漏長驅動程式中單例累積資源實作 reset()/shutdown() 釋放資源

總結

  • 單例模式核心:**私有建構子 + 靜態實例 + 靜態取得方法**。
  • 必要防禦:**private __clone、__wakeup** 防止複製/反序列化破壞單一性。
  • 實戰場景:資料庫連線、配置管理、日誌記錄器、快取客戶端、API 客戶端。
  • 現代框架專案:**優先使用 DI 容器管理單例**,手寫單例類作為備選或獨立元件。
  • 記得實作 resetInstance() (測試用) 與 shutdown() (資源釋放) 方法。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved